pcbjam/docs/features/async/04-decisions-tests-open-questions.md

123 lines
8.4 KiB
Markdown
Raw Permalink Normal View History

# 04 — How the fixes relate, the test matrix, open questions
feat(wasm-eh): migrate the WASM build to native wasm exceptions (+ 3D viewer default-on) Replace the legacy Emscripten JS-exceptions model with native wasm-EH (legacy encoding) across the whole build, keeping Asyncify coroutines working via a from-source Binaryen --hoist-cpp-catches pre-pass. Net result: native-EH is the only build mode, the 3D viewer is on by default, and pcbnew shrinks substantially. Highlights: - Binaryen submodule everywhere + --hoist-cpp-catches integration in apply-asyncify; post-link Asyncify covers every app wasm (not just standalone test wasm). - Build deps (incl. OpenCASCADE without OCC_CONVERT_SIGNALS) and all KiCad apps with -fwasm-exceptions; emscripten_sleep added to the post-link asyncify-imports. - libcontext fiber entry wired under native exceptions; while-loop main loop + currData shim injected into all wx apps. - Native-EH collab apply fixed: DEBUG-define the embind TU + match all out-of-CMake C++ TUs' ABI flags to the core, fixing the vtable-layout skew / mis-dispatch. - 3D viewer enabled by default (real raytracer linked, not the stub). - Retire the EH-spike scaffolding; flip the asyncify-races ablation pins to shim-redundancy pins (native-EH stays clean with the legacy shims ablated). - Fix the asyncify-races quiescence check to not require Asyncify.currData==0: under the native-EH per-frame-yield top loop the main stack is asyncify-suspended every frame, so currData legitimately churns (a freed-but-not-yet-nulled buffer, not a leak). Refresh the pcbnew toolbar screenshot baseline for the new kicad. - CI: drop the obsolete binaryen_version input/env (the build uses the binaryen submodule fork's wasm-opt, not a version download); key the wasm-output cache on the binaryen submodule SHA instead. Bumps the wxwidgets + binaryen submodules to their squashed feature commits. Validated green: all 7 apps native-EH (real 3D in pcbnew); KiCad e2e 63/63 Firefox + Chromium (3D viewer renders); wx 336; coroutine 34/34 both engines; asyncify 7/7 both engines. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 19:50:18 +02:00
> **STATUS (2026-06-23):** the top-level `set_main_loop(...,1)` `throw "unwind"` treated as current here **is gone** — replaced by the Asyncify **de-park** rAF pump (fatal under native wasm-EH; see [`../wasm-exceptions/09`](../wasm-exceptions/09-event-loop-deparking-plan.md)). The de-park regressed the coroutine suite, and **Design B is now being built to fix it** ([`12`](12-design-b-asyncify-implementation-plan.md) + [`13`](13-design-b-engineering-spec.md)). Read below as the pre-de-park analysis.
> The goal is **one universal mechanism**, not patches scattered around. This file classifies the
> candidate fixes by *root cause* so it's clear what is part of the one solution, what is
> subsumed, and what is genuinely separate.
## §1. The candidate levers, by root cause
| Lever | Root cause it addresses | Relationship to the universal fix |
|---|---|---|
| **Sync clipboard `IsSupported`** | A *separate* semantic/UX bug: a cheap predicate doing a 2 s permission-gated async read on the idle path. | **NOT needed for correctness.** A correct per-context authority makes the 2 s sleep *safe* even when overlapped. Keep it only as an independent UX/perf improvement, or drop it. |
| **Trampoline self-heal** (`try/finally` around `Fibers.trampoline`) | A *different register*, `Fibers.trampolineRunning`, getting stuck `true` after a mid-loop unwind. | **Subsumed.** A real authority must own the trampoline anyway; "always reset the guard" becomes one of its invariants, not a standalone bolt-on. |
| **De-park `main()`** (`simulate_infinite_loop=0` + suppress teardown) | The `throw "unwind"` abandoning Asyncify state outside any scheduler's control, plus the null-`IsModal` lifecycle crash. | **Orthogonal** to `currData`. Whether it's required is decided by one measurement (§2). If required, it is *part of* the universal design (the main loop becomes a normal scheduler participant), not a hack. |
| **Per-context `currData` authority** | The single global slot shared by overlapping suspensions. | **The core.** This is the one universal mechanism. |
### Why de-parking doesn't automatically fall out of the authority
The authority governs `currData` during *normal* unwind/rewind transitions. But
`simulate_infinite_loop=1` is a **`throw "unwind"`** (`pcbnew.js:11392`) — a JS exception that
**bypasses all asyncify bookkeeping, including the authority's.** Two orthogonal design axes:
- **D1 — how does `main()` avoid returning into destructive cleanup?** (throw, vs. de-park +
suppress cleanup)
- **D2 — how is `currData` managed across overlaps?** (single slot, vs. unified authority)
The authority is purely D2; the throw is purely D1. They don't subsume each other. You *can*
build the authority while keeping the throw — its safety then hinges entirely on §2.
## §2. The one measurement that sets the scope
> **Is `Asyncify.currData` clean (null, no context parked) at the instant `wxGUIEventLoop::DoRun()`
> executes the `throw "unwind"`?**
- **If yes** → the throw abandons nothing; the **per-context authority alone is the complete
universal solution.** No de-parking needed.
- **If no** (a startup coroutine or the main fiber is still parked at the throw) → the throw
orphans a live buffer *outside the authority's control*. Then the clean answer is to **remove
the throw** (de-park), so the authority never has to recover from an abnormal teardown. (The
alternative — "reconcile/reset `currData` at the next tick" — is exactly the kind of paper-over
we want to avoid.)
**The diagnostic (cheap, JS-only, no full rebuild):** add `console.log`s of
`Asyncify.currData` / `Asyncify.state` / `Fibers.trampolineRunning` at:
1. the last line of `DoRun` *before* `emscripten_set_main_loop(...,1)` (C side, `evtloop.cpp`),
2. the first `MainLoop.runner` rAF tick (`pcbnew.js:11342`),
3. the entry of the first post-startup `_emscripten_fiber_swap` (`pcbnew.js:11557`).
Compare `currData` to `g_main_context`'s buffer (libcontext) and to any live coroutine's
`fiber+20`. This disambiguates §5 mechanism #1 (orphaned buffer) vs. #2 (stuck guard) in
[`02-asyncify-internals.md`](02-asyncify-internals.md), and answers the yes/no above.
**Run this before designing anything.**
## §3. The universal, hack-free framing
The cleanest single design is **one cooperative Asyncify scheduler in which *every* suspendable
thing is a registered context with its own buffer** — tool coroutines, modals, clipboard, fonts,
**and the main loop itself** — where the scheduler is the sole owner of `currData` *and* the
fiber trampoline, and the main loop participates normally instead of being parked via a
state-abandoning `throw`.
In that framing:
- the per-context `currData` authority is the **core** (D2),
- the trampoline self-heal is an **internal invariant** of it,
- making the main loop a normal scheduler participant **is** de-parking (D1) — a consequence of
"no special-case suspension may abandon state," not a bolt-on,
- the clipboard sync change is **out of scope** — a separate UX fix.
The §2 measurement decides whether the scheduler must own the main loop from the start (cleaner)
or can be scoped to coroutines + sleeps and leave the main loop alone.
## §4. Tests — enumerating "every possible case"
A strong harness already exists: `tests/apps/standalone/coroutine*/` (nine probes:
`main/nested/nested_ex/embind/mainloop/gl/gl_pt/vcall/wxpt`) built by `scripts/build-wasm-test.sh`
via `tests/apps/Makefile.wasm` (each links `-sASYNCIFY=1
-sASYNCIFY_IMPORTS=['emscripten_fiber_swap']` then the same `inject-dyncall-shims.sh`), plus
`tests/e2e/coroutine-nested.spec.ts` (8 modal×fiber scenarios) and `coroutine-pthread.spec.ts`,
all asserting **no `index out of bounds`** and polling a `SUMMARY total/passed/failed` line.
**Gaps:** no systematic coverage of *out-of-order* and *long-parked* overlaps, and it asserts
crash-freedom but **not liveness** (so it would not catch a hang).
Make it a **generated combinatorial product** and assert three outcomes per cell:
- **Primitives (cells):** `S1`=EM_ASYNC_JS sleep (modal/clipboard/font), `S2`=fiber swap (tool
coroutine), `S3`=parked main loop, `S4`=pthread boundary.
- **Overlap shape:** none / nested-LIFO / **interleaved out-of-order** / **long-parked outer**
(the 2 s clipboard shape). The last two are under-tested.
- **Host context:** direct / rAF main-loop tick / embind dispatch / WebGL2 frame / deep stack /
`-fexceptions` invoke wrappers.
- **Resume target:** continuation / **virtual call** (`invoke_vi→dynCall_vi`, the `vcall_repro`
smoking gun).
- **Assert per cell:** (1) no `index out of bounds` / `indirect call to null` / `unwind`
rejection (no crash); (2) **completes within a timeout** (no hang — the missing assertion
today); (3) returned value correct (no silent wrong-buffer rewind).
- **Two must-add named scenarios** that pin our exact bugs deterministically:
`long_parked_sleep_clobbered_by_swap` (the clipboard crash) and `fiber_swap_after_main_park`
(the §5 hang — the harness must install a `simulate_infinite_loop=1` main loop, then swap a
fiber post-park).
## §5. Open questions to resolve before any implementation
1. **Run the §2 diagnostic.** Is idle `currData` the orphaned-from-park buffer (§5#1 in 02), a
stuck trampoline guard (§5#2), or both? This decides whether the authority alone suffices or
the main loop must be de-parked, and whether the trampoline self-heal is meaningful on its own.
2. **For de-parking, which suppression shape** (2a UnloadCallback-driven cleanup vs. 2b guarded
`wxEntryCleanupReal`) is least invasive given our `wxApp`/`init.cpp` fork delta? Check
`scripts/kicad-diff-stats.sh` and current wx fork divergence first.
3. **Does sync-clipboard alone stop the load-pcb route hanging, or only stop crashing?** If the
open hangs even with clipboard synchronous, the authority (and likely de-park) is required, not
optional.
---
## Provenance
This dossier was reconstructed from: Claude session
`3301ab3a-c457-4359-be9a-dcf4dcfc9fbc.jsonl`; the in-repo
`docs/docs/features/fix-asyncify-O2-and-modal-promise-rejection/{rtree,clipboard}-*-findings.md` and
`research/threading_*.md`; direct reading of the generated `tests/apps/kicad/pcbnew.js` runtime,
the `wxwidgets/src/wasm/` and `kicad/thirdparty/libcontext/` sources, and the
`scripts/common/` build pipeline; plus web research into Emscripten/Binaryen Asyncify and fibers
(sources listed in [`03-solutions-and-prior-art.md`](03-solutions-and-prior-art.md)).