pcbjam/docs/features/async/04-decisions-tests-open-questions.md
Viktor Vaczi 66ce367703 docs: asyncify currData + wasm-exceptions research dossiers
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>
2026-06-12 17:02:59 +02:00

7.9 KiB
Raw Blame History

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 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.logs 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, 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).