Two research dossiers under docs/features: - async/: Asyncify currData contention — the clipboard crash and post-idle tool hang as one disease (single global suspension slot, three producers), asyncify internals walkthroughs, prior art, arbiter designs A/B. - wasm-exceptions/: -fexceptions vs -fwasm-exceptions — measured invoke-driven share of the asyncify tax on pcbnew (59% raw / 64% gzip; download 64.5->36 MB if migrated), KiCad dialog-in-catch audit (85 direct / 93 review of 636, catch_audit.py included), binaryen toolchain status (partial EH support since v125; TryTable unsupported), and a catch-arm-hoisting pre-pass design that would obsolete the KiCad-side refactor. Cross-linked with the parked KICAD_WASM_EH end-to-end experiment (docs/wasm-exceptions-experiment.md). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
7.9 KiB
04 — How the fixes relate, the test matrix, open questions
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
currDatamanaged 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.currDataclean (null, no context parked) at the instantwxGUIEventLoop::DoRun()executes thethrow "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
currDataat the next tick" — is exactly the kind of paper-over we want to avoid.)
The diagnostic (cheap, JS-only, no full rebuild): add console.logs of
Asyncify.currData / Asyncify.state / Fibers.trampolineRunning at:
- the last line of
DoRunbeforeemscripten_set_main_loop(...,1)(C side,evtloop.cpp), - the first
MainLoop.runnerrAF tick (pcbnew.js:11342), - the entry of the first post-startup
_emscripten_fiber_swap(pcbnew.js:11557). ComparecurrDatatog_main_context's buffer (libcontext) and to any live coroutine'sfiber+20. This disambiguates §5 mechanism #1 (orphaned buffer) vs. #2 (stuck guard) in02-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
currDataauthority 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 /
-fexceptionsinvoke wrappers. - Resume target: continuation / virtual call (
invoke_vi→dynCall_vi, thevcall_reprosmoking gun). - Assert per cell: (1) no
index out of bounds/indirect call to null/unwindrejection (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) andfiber_swap_after_main_park(the §5 hang — the harness must install asimulate_infinite_loop=1main loop, then swap a fiber post-park).
§5. Open questions to resolve before any implementation
- Run the §2 diagnostic. Is idle
currDatathe 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. - For de-parking, which suppression shape (2a UnloadCallback-driven cleanup vs. 2b guarded
wxEntryCleanupReal) is least invasive given ourwxApp/init.cppfork delta? Checkscripts/kicad-diff-stats.shand current wx fork divergence first. - 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).