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>
5.7 KiB
01 — Background: the session, the test, and the three bugs
How we got here
The work began with: "load a real PCB design in the PCB editor … use the open menu inside
KiCad and open it … create a test, make screenshots." That investigation lives in Claude
session 3301ab3a-c457-4359-be9a-dcf4dcfc9fbc.jsonl (started 2026-05-28). It produced:
- The e2e test:
tests/kicad/load-pcb.spec.ts— drives File→Open, injects demo boards into MEMFS, waits forLoadBoard, screenshots. Parametrized over two demos:microwave(RF-polygon heavy) andpic_programmer(~698 KB real layout). Probe variant:tests/kicad/load-pcb-probe.spec.ts. - Logs:
tests/logs/kicad/load-pcb/…microwave….log. - Findings docs:
docs/docs/features/fix-asyncify-O2-and-modal-promise-rejection/rtree-debug-findings.mdand…/clipboard-asyncify-findings.md.
Three distinct errors appeared along the way. They are easy to conflate but have different root causes.
Bug 1 — rtree Classify assert during LoadBoard() — FIXED
A WASM-only 32-bit integer overflow in KiCad geometry:
kicad/libs/kimath/src/geometry/shape_poly_set.cpp:1927 instantiated
RTree<intptr_t, intptr_t, 2, intptr_t>. The 4th template arg (ELEMTYPEREAL) is the wide
float type for RectSphericalVolume's sumOfSquares. Every other RTree in KiCad uses double;
this one used intptr_t. On native (intptr_t==int64_t) the bug is latent; on wasm32
(intptr_t==int32_t) a microwave polygon's area (~1.79e14) wraps int32 to garbage
(coverSplitArea = -2099823776), breaking PickSeeds → seed0==seed1==0 →
Classify(0,1) trips ASSERT(!m_taken[0]) at rtree.h:1771 → WASM Aborted() during load.
Fix (committed): intptr_t → double.
- kicad
07d8130d— "shape_poly_set: use double instead of intptr_t for RTree ELEMTYPEREAL" - root
31ff88e— "tests: load-pcb e2e for microwave + pic_programmer demos" (bumps submodule)
This bug is not part of the async story. Listed only so it isn't re-confused with the others.
Bug 2 — clipboard index out of bounds crash, after the board renders
Symptoms (Firefox): RuntimeError: index out of bounds (microwave) / indirect call to null
(pic_programmer), preceded by [wxClipboard] Cannot check clipboard content: Clipboard operation timed out. The board is already fully painted; this is downstream noise that the
load-pcb test deliberately filtered out.
Mechanism (verified against current code): wxClipboard::IsSupported()
(wxwidgets/src/wasm/clipbrd.cpp:288-311) answers "is text available?" by calling
js_clipboardHasText() (clipbrd.cpp:118-142), an EM_ASYNC_JS that does
navigator.clipboard.readText() raced against a 2-second timeout. The just-loaded headless
page has no clipboard permission, so each call suspends via Asyncify for the full 2 s.
IsSupported is a synchronous-by-contract predicate that the UI-update / paste-enable path
calls repeatedly, so these suspends stack (pendingSleeps→3 in the diagnostics) and, crossed by
a modal teardown's fiber swap, the single Asyncify.currData slot is clobbered →
doRewind(null/garbage) → crash.
The
clipboard-asyncify-findings.mddoc labels its fix "Applied," but the code is still buggy —wxwidgetsHEAD6fb2eac257still calls the asyncIsSupported. The fix was designed but never committed.
Full control flow in 02-asyncify-internals.md §"The crash".
Bug 3 — the tool-open hang, after idle (the real blocker for rendering)
From the live trace (libcontext KICAD_DIAG_COROUTINE): the programmatic File→Open hangs
inside the first tool fiber swap after startup. Trace ends at a jump-swap with no
following entry-call/jump-resume — the swap unwinds but the target fiber never runs. Key
observations from that session:
- The same coroutine completes many swap cycles during the startup burst, then the first swap after startup hangs.
- It is not specific to the open — on the fileless
/p/mytest/eeschemaroute, awkeystroke after the startup burst triggers a fiber resume that hangs identically. So all post-idle tool interactivity is affected. - At idle, measured
Asyncify.state == NormalbutAsyncify.currDataset — i.e. a swap unwound and its rewind was never issued.
The session attributed this to the main loop "parking" main() in the single currData slot
via wxwidgets/src/wasm/evtloop.cpp:107
emscripten_set_main_loop(ProcessEvents, 0, /*simulate_infinite_loop=*/1).
Important correction (see §"De-parking" in 02):
simulate_infinite_loop=1is not an Asyncify park — it is a plainthrow "unwind"(pcbnew.js:11392) that discards the nativemain()stack. The dangling-currData-at-idle it produces is the leading suspect for the hang, but the precise mechanism (orphaned buffer vs. stuck trampoline guard) is an open empirical question answered by a one-line diagnostic.
There is also a related null-IsModal / windowClosing close crash: when beforeunload
fires during the suspended-open window, the close path crashes on a null vtable slot. That is a
lifecycle bug (teardown firing under the live loop), adjacent to the hang.
The connective insight
Bug 2 (crash) and Bug 3 (hang) are the same disease: one global currData/state register
shared by three producers. Crash vs. hang is just which producer loses the race for the slot:
- Crash: a long-parked sleep (the 2 s clipboard read) is clobbered by a fiber swap.
- Hang: a fiber swap is stranded because the slot is dirtied/occupied (by the parked main loop's abnormal teardown), so its rewind is never issued.
Everything else in this dossier follows from that one observation.