pcbjam/docs/features/async
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Gergő Törcsvári f734d700a2
fix(asyncify): retire the deferral family — guard-layer road closed
v0.1.24 in prod, doubly convicted the same morning: the Leonardo open
crawled/hung (open:settled result=failed at the 60s escape, heap never past
256MB — every main-loop iteration runs INSIDE its yield-wake extent, so the
"root-owned wake" scope matched thousands of legitimate nested coroutine
Call/returns per open, each paying a deferred macrotask, throttled to ≥1s in
a background tab), AND the Nano crashed 22ms after deferrals=1 fired.
Harmful and insufficient: the fatal nested-rewind interleave and the benign
bulk are observationally identical at this layer — no discriminator exists.

Retired (second and final retraction, async/16 round 5). What stays shipped
and clean: consume-once root suspensions, the internally-parked quarantine +
laundering check, the flight recorder + beacons, the WSOD floor, the
pendingSleeps leak fix (confirmed by pendingSleeps=[] in the Nano dump). The
rare nested-rewind crash is ACCEPTED and fully observable until the
structural fix — the design-B fiber-first runtime (async/06,12,13), where
one scheduler owns every suspension and this interleave cannot exist.

.ci-cache-epoch 6→7.

Local: fiber 2/2 (one refusal beacon) + timer + firefox sweep 21 passed,
chromium scenarios 11 passed/4 quarantine-skips, web fatal+follow 2/2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-02 08:18:08 +02:00
..
01-background-and-findings.md docs: asyncify currData + wasm-exceptions research dossiers 2026-06-12 17:02:59 +02:00
02-asyncify-internals.md feat(wasm-eh): migrate the WASM build to native wasm exceptions (+ 3D viewer default-on) 2026-06-30 09:40:26 +02:00
03-solutions-and-prior-art.md docs: asyncify currData + wasm-exceptions research dossiers 2026-06-12 17:02:59 +02:00
04-decisions-tests-open-questions.md feat(wasm-eh): migrate the WASM build to native wasm exceptions (+ 3D viewer default-on) 2026-06-30 09:40:26 +02:00
05-design-a-js-asyncify-arbiter.md feat(wasm-eh): migrate the WASM build to native wasm exceptions (+ 3D viewer default-on) 2026-06-30 09:40:26 +02:00
06-design-b-fiber-first-runtime.md feat(wasm-eh): migrate the WASM build to native wasm exceptions (+ 3D viewer default-on) 2026-06-30 09:40:26 +02:00
07-decisions-and-outcome.md feat(wasm-eh): migrate the WASM build to native wasm exceptions (+ 3D viewer default-on) 2026-06-30 09:40:26 +02:00
08-dom-port-regression.md fix(wasm): bump wxwidgets for the wxMenuBar Detach() UAF fix 2026-06-13 14:10:05 +02:00
09-dom-window-lifetime-hypothesis.md fix(wasm): bump wxwidgets for the wxMenuBar Detach() UAF fix 2026-06-13 14:10:05 +02:00
10-resolution-menubar-uaf.md fix(wasm): bump wxwidgets for the wxMenuBar Detach() UAF fix 2026-06-13 14:10:05 +02:00
11-asyncify-nesting-raytracer.md test: 💍 deadlock test + readme update 2026-06-19 09:22:58 +02:00
12-design-b-asyncify-implementation-plan.md feat(wasm-eh): migrate the WASM build to native wasm exceptions (+ 3D viewer default-on) 2026-06-30 09:40:26 +02:00
13-design-b-engineering-spec.md feat(wasm-eh): migrate the WASM build to native wasm exceptions (+ 3D viewer default-on) 2026-06-30 09:40:26 +02:00
14-open-settle-gate.md fix(load): open-settle gate — kicadOpenFileBusy probe + collab entry guards for the parked-open embind trap (indirect call signature mismatch) + deterministic collab-load-fuzz e2e 2026-07-30 14:17:48 +02:00
15-timer-park-repro.md fix(async): fiber resume guard — the prod board-load trap, red/green 2026-07-31 23:37:05 +02:00
16-fiber-resume-guard.md fix(asyncify): retire the deferral family — guard-layer road closed 2026-08-02 08:18:08 +02:00
async-research.md docs: asyncify currData + wasm-exceptions research dossiers 2026-06-12 17:02:59 +02:00
README.md test: 💍 deadlock test + readme update 2026-06-19 09:22:58 +02:00

Asyncify currData contention in KiCad-WASM — research dossier

Status: research / understanding only. No implementation has been chosen. Authored 2026-06. All line numbers are against the artifacts current at that time (tests/apps/kicad/pcbnew.js, wxwidgets/src/wasm/*.cpp, kicad/thirdparty/libcontext/libcontext.cpp, wxwidgets/src/common/init.cpp).

Why this exists

While bringing up tests/kicad/load-pcb.spec.ts (load a real .kicad_pcb through File→Open), three distinct errors surfaced. One is fixed; two remain and turned out to be the same underlying disease: Emscripten Asyncify has a single global suspension register (Asyncify.currData + Asyncify.state), but KiCad-WASM has three independent subsystems that all drive it (tool coroutines, modal/clipboard sleeps, and the parked main loop). When any two overlap, one reads a buffer that no longer belongs to it → crash (index out of bounds) or hang (a swap unwinds but is never rewound).

TL;DR

  • The single slot is not a WebAssembly/Binaryen law — it's a choice in Emscripten's JS runtime. The Binaryen Asyncify pass is multi-buffer by design; asyncify_start_unwind/ asyncify_start_rewind take the buffer pointer as an argument.
  • "Give each context its own buffer" is the standard, supported solution — it's literally what Emscripten fibers are, and what QEMU/TinyGo/Pyodide do. We already do it for tool coroutines (each wasm_fcontext owns a buffer). The bug is that the fiber path and the handleSleep path both blindly overwrite the one global currData register.
  • We are not switching to JSPI. The fix stays within Asyncify, in the wasm/shim layer.
  • The achievable universal fix is a single cooperative scheduler that owns currData (and the fiber trampoline) and treats every suspendable thing — coroutines, modals, clipboard, fonts, and the main loop itself — as a registered context with its own buffer.

Document index

File Contents
01-background-and-findings.md The originating session, the e2e test + logs, and the three concrete bugs (rtree=fixed; clipboard crash; tool-open hang).
02-asyncify-internals.md The machine, in detail: what "sleeps", the single currData/state, the three producers, handleSleep/fiber_swap/trampoline, the #9153 shim, and full control-flow walkthroughs (startup→park, the hang, the crash, de-parking down to the JS line).
03-solutions-and-prior-art.md Is there a real solution? Per-context buffers, who has done it, why we're not switching to JSPI, and the unified-authority recipe with failure modes. Sources/URLs included.
04-decisions-tests-open-questions.md How the fix options relate (what's subsumed vs. genuinely separate), the one diagnostic that decides scope, the combinatorial test matrix, and open questions.
05-design-a-js-asyncify-arbiter.md Incremental design: keep current EM_ASYNC_JS sleeps and fibers, but put one JS arbiter in charge of currData, transition queueing, and the trampoline. Includes concept explanations.
06-design-b-fiber-first-runtime.md Cleaner long-term design: make modals, clipboard, fonts, nested loops, and tools all scheduler-owned fiber-like contexts. Explains how this relates to de-parking and app lifetime.
07-decisions-and-outcome.md What was decided and shipped (2026-06-12): root cause, red-green ledger, the arbiter NOT built and why, roads not taken with revisit triggers.
08-dom-port-regression.md DOM-port regression investigation after rebasing onto the async hardening: traces, symbolized crash, ruled-out Asyncify/table/removelist hypotheses, and current stale-window diagnosis.
09-dom-window-lifetime-hypothesis.md Concrete failure story and first fix experiment for the DOM-port stale wxWindow hypothesis: destructor ordering, DOM event reentry, and validation plan.
10-resolution-menubar-uaf.md RESOLVED: the regression was a freed wxMenuBar left in a live frame's child list by wxMenuBarBase::Detach() (DOM-port only — the bar is a real child there). One-line fix in wxMenuBar::Detach(); full kicad suite green, zero corruption signatures.
11-asyncify-nesting-raytracer.md Finding + decision: the WASM 3D raytracer is single-core because emscripten_sleep can't nest on an already-unwinding Asyncify context — yielding to join worker threads aborts with invalid state: 1 since the viewer renders inside a suspended wx modal pump. Multi-core pool (~67×) built + parked; unpark needs a nestable yield (fiber/JSPI).

The single decisive next step

Before designing anything, measure whether Asyncify.currData is clean (null) at the moment wxGUIEventLoop::DoRun() throws "unwind" (and at the first rAF tick, and at the first post-startup emscripten_fiber_swap). That one fact determines whether the universal fix must also reshape the main loop ("de-parking") or whether a per-context currData authority alone suffices. Details in 04-decisions-tests-open-questions.md.


RESOLUTION (2026-06-12) — see 07-decisions-and-outcome.md and docs/features/asyncify-arbiter/

The decisive measurement was answered by code trace and then pinned by a deterministic test (tests/asyncify/asyncify-races.spec.ts + tests/apps/standalone/asyncify-races/):

  • At the park throw, Asyncify state IS clean (currData==null, state==Normal) — but the JS stack is necessarily still inside Fibers.trampoline()'s do/while (any OnInit-era fiber swap means main is trampoline-resumed from then on). The throw skips the trampolineRunning = false reset → the guard wedges → the first post-idle swap hangs. That IS the §5 hang; "orphaned currData" (mechanism #1) is structurally impossible. The self-heal (inject-dyncall-shims.sh §3c, commit 18a9de0) is therefore the structural cure, not a band-aid — no de-parking needed.
  • When main's last pre-park suspension is a sleep, the same throw instead escapes through the sleep's wakeUp promise reaction → the long-mystifying uncaught exception: unwind rejections. Fixed in scripts/common/shims/handlesleep.js (catches the sentinel like callMain does).
  • The full Design-A arbiter was NOT needed: at production semantics (-sASSERTIONS=0), out-of-order and overlapping-sleep scenarios are already handled by the per-sleep buffer capture in handlesleep.js. The remaining bugs were wx-layer: single-slot Module._endModal broke 3-deep nested modals (now a LIFO resolver stack), and the modal/nested-loop pumps stalled silently on ProcessEvents rejection (now resolve-on-error). Clipboard IsSupported no longer runs the 2 s async probe.
  • De-parking (02 §7) and Design B remain documented options, unneeded for correctness today.
  • Upstream status (researched): the Fibers/Asyncify code is unchanged since 2020; the single-slot family is WONTFIX (#9153, #12270, #13302, #16291, #18412). The trampoline try/finally would be a good upstream PR.