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>
This commit is contained in:
parent
0cbb6738ec
commit
66ce367703
16 changed files with 3252 additions and 0 deletions
121
docs/features/async/04-decisions-tests-open-questions.md
Normal file
121
docs/features/async/04-decisions-tests-open-questions.md
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
# 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.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)).
|
||||
Loading…
Reference in a new issue