test(asyncify): red-green race harness + ablation flags, unwind-catch shim, spec tightening, decisions docs
The asyncify single-slot work, executed red-green (full ledger: docs/features/asyncify-arbiter/redgreen.md; decisions record: docs/features/async/07-decisions-and-outcome.md): - tests/apps/standalone/asyncify-races/ + tests/asyncify/ + dedicated playwright config: 8 scenarios reproducing the KiCad asyncify failure family with the kicad-faithful startup topology (pre-park fiber swap → park throw through the live trampoline). Built in 3 variants; the SHIM_DISABLE_TRAMPOLINE_HEAL / SHIM_DISABLE_HANDLESLEEP ablation builds keep the historical hang and index-out-of-bounds crash reproducible forever (mutation-style pins for the existing shims). - scripts/common/shims/handlesleep.js: catch the "unwind" park sentinel in the wakeUp path — when main's last pre-park suspension was a sleep, the main-loop park throw escaped through that sleep's promise reaction as an uncaught rejection (the calculator/gerbview console errors). - scripts/common/inject-dyncall-shims.sh: SHIM_DISABLE_* ablation knobs. - Spec tightening (the acceptance bar): 'uncaught exception: unwind' tolerance DELETED from pcbnew/eeschema specs; load-pcb gained a hard clean-console gate over 5 asyncify corruption signatures. - wxwidgets pointer bump: modal LIFO resolvers, pump resolve-on-error, sync clipboard IsSupported (014f67e6c1). Final state: asyncify suite 7/7, wx e2e 291/292 (1 skip), KiCad e2e 40 passed / 2 skipped with ZERO corruption signatures in any log across all six apps. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
66ce367703
commit
14ca16cbd3
16 changed files with 1593 additions and 13 deletions
59
docs/features/asyncify-arbiter/baseline.md
Normal file
59
docs/features/asyncify-arbiter/baseline.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# Baseline — pre-fix state (2026-06-12)
|
||||
|
||||
Artifacts under test for the KiCad baseline run:
|
||||
- `output/pcbnew.js` + `output/eeschema.js` from `docker/build.sh pcbnew,eeschema`
|
||||
started 14:20 (container rsyncs source at start → consistently PRE-fix wx:
|
||||
async clipboard IsSupported, single-slot `Module._endModal`, silently-stalling
|
||||
modal/nested pumps). Shims as of the same moment: §3c trampoline heal INCLUDED,
|
||||
handlesleep.js WITHOUT the wakeUp "unwind" catch.
|
||||
- This is the first build of these apps since `18a9de0` (trampoline heal) —
|
||||
the previously deployed artifacts predated it, which is why the load-pcb UI
|
||||
froze post-load in the user's earlier runs.
|
||||
|
||||
## Standalone harness reds recorded pre-fix
|
||||
|
||||
See `redgreen.md` — 3 reds (sleep-park unwind rejection; modal_in_modal_in_modal
|
||||
stall; nested pump-error stall) + 2 ablation pins reproducing the historical
|
||||
hang and clobber crash.
|
||||
|
||||
## KiCad e2e baseline run (npm run test:kicad, firefox)
|
||||
|
||||
- Full playwright summary: **40 passed, 2 skipped, 0 failed, 0 flaky** (1.5 m).
|
||||
Green-at-baseline because this is the first deployed build containing the
|
||||
§3c trampoline heal (`18a9de0`) — the freeze-after-load the user saw came
|
||||
from pre-heal artifacts.
|
||||
- Failures classified pre-existing/unrelated: none (2 skips are by design).
|
||||
- Asyncify errors present in logs DESPITE passing (the "before" evidence the
|
||||
post-fix rebuild must eliminate, currently tolerated by spec filters):
|
||||
- `logs/kicad/load-pcb/...pic-programmer....errors.log`:
|
||||
`RuntimeError: index out of bounds` — the clipboard 2 s `IsSupported`
|
||||
sleep clobber (wx fix: sync IsSupported).
|
||||
- `logs/kicad/calculator/...loads-calculator-frame.errors.log` and
|
||||
`...switch-to-color-code-panel.errors.log`: `uncaught exception: unwind` —
|
||||
the park throw escaping through a sleep's wakeUp (shim fix: handlesleep.js
|
||||
unwind catch).
|
||||
|
||||
## wxWidgets e2e regression (against the FIXED wx — the regression gate for
|
||||
## the dialog.cpp/evtloop.cpp/clipbrd.cpp changes)
|
||||
|
||||
- Coroutine suite (firefox): 13 passed, 0 failed (49.8 s)
|
||||
- Asyncify races suite (firefox): 7 passed, 0 failed (18.8 s)
|
||||
- Full wx e2e (bundled chromium): **291 passed, 1 skipped, 0 failed, 0 flaky** (1.6 m)
|
||||
|
||||
No regressions from the modal LIFO resolver stack, pump resolve-on-error
|
||||
changes, or the sync clipboard IsSupported.
|
||||
|
||||
## FINAL verification (post-fix rebuild of ALL 6 apps, tightened specs)
|
||||
|
||||
Specs tightened first: `'uncaught exception: unwind'` tolerance DELETED from
|
||||
pcbnew.spec.ts/eeschema.spec.ts; load-pcb.spec.ts gained a hard clean-console
|
||||
gate over 5 asyncify corruption signatures (before AND after board render).
|
||||
|
||||
- `npm run test:kicad` (firefox): **40 passed, 2 skipped, 0 failed, 0 flaky** (1.5 m)
|
||||
- Signature sweep over every `tests/logs/kicad/` log
|
||||
(index out of bounds / indirect call to null / uncaught exception: unwind /
|
||||
invalid state / is not a function / Aborted(): **zero matches**
|
||||
- `.errors.log` files produced by the run: **zero** (baseline had 5+, including
|
||||
load-pcb's `RuntimeError: index out of bounds` and calculator/gerbview's
|
||||
`uncaught exception: unwind` — all gone)
|
||||
- Screenshot baselines: unchanged (no rendering impact).
|
||||
65
docs/features/asyncify-arbiter/redgreen.md
Normal file
65
docs/features/asyncify-arbiter/redgreen.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# Asyncify red-green ledger
|
||||
|
||||
Harness: `tests/apps/standalone/asyncify-races/` (3 build variants: full shims /
|
||||
`SHIM_DISABLE_TRAMPOLINE_HEAL=1` / `SHIM_DISABLE_HANDLESLEEP=1`), specs in
|
||||
`tests/asyncify/asyncify-races.spec.ts`, run via `npm run test:asyncify:firefox`.
|
||||
Built with `-sASSERTIONS=0` to match production asyncify semantics (the debug
|
||||
assert "We cannot start an async operation when one is already flight" forbids
|
||||
the multi-parked-sleep states the production shims are designed to handle).
|
||||
|
||||
The harness reproduces KiCad's real startup topology: a fiber swap during OnInit
|
||||
means main() is resumed via Fibers.trampoline() when the
|
||||
emscripten_set_main_loop(...,1) `throw "unwind"` park fires — the precondition
|
||||
for the trampoline-guard wedge (this is what coroutine-nested never modeled).
|
||||
|
||||
## Board after the initial red run (2026-06-12, all pre-fix)
|
||||
|
||||
| Spec | State | Recorded failure mode |
|
||||
|---|---|---|
|
||||
| battery: post_park_fiber_swap | GREEN | §3c trampoline heal works (pinned by ablation below) |
|
||||
| battery: sleep_inside_fiber_inside_modal | GREEN | 3 concurrent buffers (modal+fiber+sleep) survive under handlesleep.js |
|
||||
| battery: out_of_order_sleep_resolution | GREEN | FIFO resolution of 2 parked sleeps survives (shim associates per-sleep buffers) |
|
||||
| battery: long_parked_sleep_clobbered_by_swap | GREEN | 1.2s parked sleep + 2 fiber-swap cycles survives (pinned by ablation below) |
|
||||
| wakeup_during_transition | GREEN | modal teardown from fresh stack over 2 parked sleeps survives current shims |
|
||||
| **modal_in_modal_in_modal** | **RED** | watchdog timeout, all-quiet state: wx `dialog.cpp` keeps the modal resolver in a single slot (`Module._endModal = fn`, `delete` after use) — the middle EndModal(102) resolves nothing, its ShowModal parks forever. NEW product bug found by the harness (KiCad nests dialogs). Fix: Stage-3 LIFO resolver stack. |
|
||||
| **nested_quasi_modal_pump_error** | **RED** | watchdog timeout with `currData=1390336` left parked — c27fe8bf's `wxWasmRunNestedLoop` pump catches the ProcessEvents rejection and stops WITHOUT resolving; nested DoRun leaks forever. Fix: Stage-3 resolve-on-error. |
|
||||
| **sleep-park: unwind_through_promise** | **RED** | `uncaught exception: unwind` at `handleSleep/< ... promise callback*handleAsync` — the park throw escapes through the last pre-park sleep's wakeUp promise reaction. Fix: Stage-2 shim catches the `"unwind"` sentinel in the wakeUp path (the same class pcbnew.spec.ts/eeschema.spec.ts currently FILTER OUT with `'uncaught exception: unwind'`). |
|
||||
| ablation noheal: post_park swap hangs | GREEN (reproduces) | watchdog: `state=0 currData!=0 trampolineRunning=true nextFiber=0`, suite never completes — the exact traced mechanism: the park throw tears through the live trampoline do/while, `trampolineRunning=false` reset skipped, guard wedged forever. Pins §3c (`18a9de0`). |
|
||||
| ablation nosleepfix: parked sleep clobbered | GREEN (reproduces) | `RuntimeError: index out of bounds` (the KiCad clipboard crash signature) — fiber swap clobbers `Asyncify.currData` while a sleep is parked; wakeUp rewinds garbage. Pins `handlesleep.js`. |
|
||||
|
||||
## Notes
|
||||
|
||||
- out_of_order_sleep_resolution and wakeup_during_transition could not be made
|
||||
red under the current shims at production semantics — the existing
|
||||
handlesleep.js per-sleep buffer capture handles them. They stay as regression
|
||||
pins. The KiCad-side "ENTER at state=2" diagnostic remains the only evidence
|
||||
for a residual wakeup race; the Stage-4 KiCad e2e run (clean-console
|
||||
assertions) is the judge of whether more shim work (deferred wakeups) is needed.
|
||||
- Earlier harness iterations hit two environment gotchas worth remembering:
|
||||
`EM_JS` bodies take C parameter NAMES (not `$0` — that's EM_ASM), and
|
||||
`npx serve`'s cleanUrls redirect DROPS query strings — harness params travel
|
||||
in the URL hash.
|
||||
|
||||
## Green transitions (2026-06-12, same day — suite 7/7 green in 21.7s)
|
||||
|
||||
- [x] sleep-park unwind_through_promise → GREEN via `scripts/common/shims/handlesleep.js`:
|
||||
the wakeUp wrapper catches the `"unwind"` sentinel (the main-loop park
|
||||
escaping through a sleep's promise reaction) and swallows it exactly like
|
||||
callMain does on the direct path. Shim-only; no rebuild of wx needed.
|
||||
- [x] modal_in_modal_in_modal → GREEN via `wxwidgets/src/wasm/dialog.cpp`:
|
||||
`Module._endModal` is now a stable LIFO dispatcher over
|
||||
`Module._wxModalResolvers` (was: single slot + delete). Also: the modal
|
||||
pump now CANCELS the modal (resolves `wxID_CANCEL`) on a ProcessEvents
|
||||
rejection instead of silently stopping with the stack parked.
|
||||
- [x] nested_quasi_modal_pump_error → GREEN via `wxwidgets/src/wasm/evtloop.cpp`:
|
||||
`wxWasmRunNestedLoop`'s pump resolves (exits the nested loop, loudly) on a
|
||||
ProcessEvents rejection instead of stopping with the nested DoRun parked.
|
||||
- [also] `wxwidgets/src/wasm/clipbrd.cpp` `IsSupported`: no longer calls the 2 s
|
||||
`js_clipboardHasText` EM_ASYNC_JS — answers from the sync capability
|
||||
probe. Its red lives at the KiCad level (CLIP-DIAG unwind/OOB lines in
|
||||
tests/logs/kicad/load-pcb); verified by the Stage-4 clean-console runs.
|
||||
|
||||
Harness learning recorded for posterity: to make the *pump* fail you must throw
|
||||
from a PENDING EVENT (ProcessEvents -> ProcessPendingEvents); wx timers on wasm
|
||||
fire via emscripten_async_call/callUserCallback and bypass the pump's awaited
|
||||
ccall entirely.
|
||||
Loading…
Reference in a new issue