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
155
docs/features/async/07-decisions-and-outcome.md
Normal file
155
docs/features/async/07-decisions-and-outcome.md
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
# 07 — Decisions and outcome (2026-06-12)
|
||||
|
||||
> The dossier (01–06) ended with designs and open questions. This file records what was
|
||||
> actually decided, built, and deliberately NOT built — and the trigger conditions for
|
||||
> revisiting each road not taken. Working artifacts: `docs/features/asyncify-arbiter/`
|
||||
> (baseline.md, redgreen.md), harness in `tests/apps/standalone/asyncify-races/` +
|
||||
> `tests/asyncify/`, runnable via `npm run test:asyncify:firefox`.
|
||||
|
||||
## D1 — Root cause: the dossier's two hang mechanisms were ONE mechanism
|
||||
|
||||
The §5 (doc 02) question "orphaned currData vs stuck trampoline guard" was settled by code
|
||||
trace, then pinned by a deterministic test. At the `emscripten_set_main_loop(...,1)`
|
||||
`throw "unwind"` park, Asyncify state is clean — but **any OnInit-era fiber swap means
|
||||
main() is, from then on, executing inside `Fibers.trampoline()`'s `do/while`** (resumed via
|
||||
`finishContextSwitch → doRewind`). The throw tears through that live frame; the
|
||||
`trampolineRunning = false` reset is skipped; the guard wedges forever; the first post-idle
|
||||
swap strands. "Orphaned currData" is structurally impossible (the throw only executes from
|
||||
straight-line Normal-state code). Corollary: when main's *last* pre-park suspension was a
|
||||
**sleep**, the same throw instead escapes through that sleep's wakeUp promise reaction —
|
||||
that was the long-standing `uncaught exception: unwind` rejection family.
|
||||
|
||||
Consequence: the trampoline self-heal (`inject-dyncall-shims.sh` §3c, commit `18a9de0`)
|
||||
is the **structural cure** for the hang, not a band-aid (doc 04 had it demoted), and
|
||||
**de-parking (02 §7) is not needed for correctness.**
|
||||
|
||||
## D2 — Red-green doctrine governed everything
|
||||
|
||||
Rule applied throughout: *no fix lands before a test has been observed failing for the
|
||||
exact disease, and the failing run is recorded* (`asyncify-arbiter/redgreen.md`). Two red
|
||||
flavors: **natural red** (bug unfixed today) and **ablation red** (fix exists; rebuild the
|
||||
harness with `SHIM_DISABLE_TRAMPOLINE_HEAL=1` / `SHIM_DISABLE_HANDLESLEEP=1` to prove the
|
||||
test detects the disease the fix prevents — mutation-testing style). The ablation flags are
|
||||
permanent injector features; the diseases stay reproducible on demand.
|
||||
|
||||
## D3 — Decision: the full Design-A arbiter was NOT built
|
||||
|
||||
Doc 05's arbiter (central context registry + deferred-wakeup queue + single-owner
|
||||
transitions) was the plan. It was dropped because **no scenario could be made red that it
|
||||
would fix**: at production asyncify semantics (`-sASSERTIONS=0`), the pre-existing
|
||||
per-sleep capture in `scripts/common/shims/handlesleep.js` (commit `a4ad694`) already
|
||||
implements Design A's core invariant — *`Asyncify.currData` is a transient register; every
|
||||
parked context owns its buffer pointer elsewhere* (fibers: in the `wasm_fcontext` struct;
|
||||
sleeps: in per-sleep closures, restored immediately before that sleep's own rewind). The
|
||||
scenarios predicted to need the arbiter (`wakeup_during_transition`,
|
||||
`out_of_order_sleep_resolution`, `sleep_inside_fiber_inside_modal`) are green under the
|
||||
existing shim and are kept as regression pins.
|
||||
|
||||
Why the residual race is structurally hard to hit: promise resolvers only queue
|
||||
microtasks; microtasks run only at JS-stack-empty; every transition (unwind chain,
|
||||
trampoline loop, rewind chain) completes synchronously within one task. The only mechanism
|
||||
that splits a transition across tasks is an `await` inside the chain — the async-ccall
|
||||
pumps — which are exactly what was hardened (D5).
|
||||
|
||||
**Trigger to revisit:** any reappearance of a wakeup-during-transition signature in
|
||||
`tests/logs/` (e.g. handleSleep entered at `state=2`, `invalid state` aborts), or any new
|
||||
red the existing shims can't turn green. The arbiter design in doc 05 remains the blueprint;
|
||||
the deferred-wakeup queue alone is a ~50-line `handlesleep.js` extension.
|
||||
|
||||
## D4 — Decision: keep the park throw (+ heal + sentinel catch); defer the asyncify-park
|
||||
|
||||
The alternative discussed ("Option C": park main in a never-resolving `EM_ASYNC_JS` sleep
|
||||
instead of the throw — clean unwind, no exceptional control flow, explicit teardown owner,
|
||||
natural step one of Design B) was deliberately **deferred**. With the heal (§3c) covering
|
||||
the fiber face and the `handlesleep.js` sentinel catch covering the sleep face, both damage
|
||||
classes of the throw are individually fixed and pinned; the asyncify-park would have landed
|
||||
green-on-green. Costs it would incur now: wx lifecycle change + rebuild, `MainLoop.pause()`
|
||||
interplay (handleSleep auto-pauses the loop), a permanently-parked buffer, divergence from
|
||||
stock emscripten idiom.
|
||||
|
||||
**Trigger to revisit:** recurring `"unwind"`-leak variants that the sentinel catch doesn't
|
||||
cover, or starting Design B (fiber-first runtime) in earnest.
|
||||
|
||||
## D5 — wx-layer fixes (the bugs that were wx's, not the shim's)
|
||||
|
||||
1. **`dialog.cpp` — modal resolver is a LIFO stack.** `Module._endModal` was a single slot
|
||||
that deleted itself after use; with 3+ nested modals the middle `EndModal` resolved
|
||||
nothing and its `ShowModal` parked forever. Found by the new `modal_in_modal_in_modal`
|
||||
scenario — a previously unknown product bug (KiCad nests dialogs routinely). Now:
|
||||
stable `Module._endModal` dispatcher popping `Module._wxModalResolvers` (LIFO — matches
|
||||
wx modal discipline and c27's `_wxNestedLoopExit` convention). If programmatic
|
||||
out-of-order EndModal ever matters, key the dispatcher per-dialog (small change, needs
|
||||
its own red test first).
|
||||
2. **Pumps must never stop without resolving.** Both `startModal` (modal pump) and
|
||||
`wxWasmRunNestedLoop` (c27 quasi-modal pump) caught ProcessEvents rejections and
|
||||
silently stopped, leaving the suspended C++ stack parked forever. Now both resolve on
|
||||
error, loudly (`console.error`): the modal cancels with `wxID_CANCEL` (passed in from
|
||||
C++), the nested loop exits. Self-cancel paths splice their own resolver out of the
|
||||
stack (they may not be topmost).
|
||||
3. **`clipbrd.cpp` — `IsSupported` is synchronous again.** It called the 2 s
|
||||
permission-gated `js_clipboardHasText` EM_ASYNC_JS from a synchronous-by-contract
|
||||
predicate on the idle path — creating the long-parked sleeps behind the
|
||||
`index out of bounds` family. Now answers optimistically from the sync
|
||||
`js_isClipboardAPIAvailable` probe; the real read stays in `GetData()` (user-gesture
|
||||
gated).
|
||||
|
||||
## D6 — Harness design decisions (tests/apps/standalone/asyncify-races/)
|
||||
|
||||
- **KiCad-faithful topology is the point:** ≥1 fiber swap in `OnInit` so main is
|
||||
trampoline-resumed at park time — the precondition `coroutine-nested` lacked, which is
|
||||
why it never reproduced the hang. `#mode=sleep-park` flips the last pre-park suspension
|
||||
to a sleep for the rejection face.
|
||||
- **`-sASSERTIONS=0` (LDFLAGS_RACES):** emscripten's debug assert ("cannot start an async
|
||||
operation when one is already in flight") forbids the very multi-parked-sleep states the
|
||||
production shims exist to handle; the harness must match production semantics.
|
||||
- **Params travel in the URL hash:** `npx serve`'s cleanUrls redirect for `*.html` drops
|
||||
query strings.
|
||||
- **EM_JS bodies use C parameter NAMES** (`aToken`), not `$0` — that is EM_ASM syntax.
|
||||
- **To make a pump fail, throw from a PENDING EVENT** (`CallAfter` → ProcessPendingEvents
|
||||
inside the pump's awaited ccall). wx timers fire via
|
||||
`emscripten_async_call`/`callUserCallback` and bypass pumps entirely.
|
||||
- **Quiescence probe** (state==Normal, currData==null, nextFiber==0) is checked
|
||||
synchronously between scenarios; `trampolineRunning` is deliberately excluded from the
|
||||
sync check (code resumed via a fiber legitimately runs inside the trampoline do/while) —
|
||||
a genuinely stuck guard is caught by the per-scenario JS watchdogs instead.
|
||||
- Wedge-prone scenarios run as `#only=` singles on separate page loads; the chained battery
|
||||
holds only scenarios that can't kill the chain.
|
||||
|
||||
## D7 — Attribution ledger (who fixed what, when)
|
||||
|
||||
Pre-existing (earlier sessions): per-sleep buffer capture/restore in `handlesleep.js`
|
||||
(`a4ad694`); trampoline self-heal §3c (`18a9de0`); per-fiber buffers (stock emscripten +
|
||||
libcontext). This session's code: the 13-line `"unwind"` sentinel catch in
|
||||
`handlesleep.js`; the three wx fixes (D5); `SHIM_DISABLE_*` ablation flags; the harness +
|
||||
specs + config; spec tightening (D8). This session's *proof*: before it, the shim and heal
|
||||
were unverified folklore — nothing failed if you deleted them. Now their diseases reproduce
|
||||
on demand and their absence fails tests.
|
||||
|
||||
## D8 — Acceptance bar moved into the specs
|
||||
|
||||
`'uncaught exception: unwind'` tolerance filters DELETED from `pcbnew.spec.ts` /
|
||||
`eeschema.spec.ts`; `load-pcb.spec.ts` gained a hard clean-console gate over five
|
||||
signatures (`index out of bounds`, `indirect call to null`, `uncaught exception: unwind`,
|
||||
`invalid state`, `is not a function`) covering before AND after board render.
|
||||
|
||||
## Outcome (final verification)
|
||||
|
||||
- Harness: 7/7 green (battery of 4 + three singles); both ablation builds still reproduce
|
||||
their diseases. wx e2e 291 passed / 1 skipped / 0 failed; coroutine 13/13.
|
||||
- KiCad e2e after rebuilding **all six apps**: 40 passed / 2 skipped / 0 failed / 0 flaky,
|
||||
and a sweep of every `tests/logs/kicad/` log finds **zero** corruption signatures and
|
||||
**zero** `.errors.log` files (baseline had load-pcb `index out of bounds` and
|
||||
calculator/gerbview `uncaught exception: unwind`).
|
||||
- Upstream (researched): Fibers/Asyncify JS runtime unchanged since 2020; the single-slot
|
||||
limitation family is WONTFIX (#9153, #12270, #13302, #16291, #18412). The §3c
|
||||
try/finally heal is a good candidate for an upstream PR.
|
||||
|
||||
## Roads not taken, with triggers
|
||||
|
||||
| Option | Status | Revisit when |
|
||||
|---|---|---|
|
||||
| Full Design-A arbiter (registry + wakeup queue) | not built | any wakeup-during-transition signature in logs, or a red the shims can't fix |
|
||||
| Park-via-unresolved-sleep (no throw) | deferred | recurring unwind-leak variants, or Design B work starts |
|
||||
| De-parking (02 §7, lifecycle surgery) | rejected | only as part of Design B |
|
||||
| Design B (fiber-first runtime) | long-term option | architectural appetite, not correctness need |
|
||||
| Per-dialog-keyed modal resolvers | not needed | a real out-of-order EndModal use case (write the red first) |
|
||||
|
|
@ -39,6 +39,7 @@ or **hang** (a swap unwinds but is never rewound).
|
|||
| [`04-decisions-tests-open-questions.md`](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`](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`](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`](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. |
|
||||
|
||||
## The single decisive next step
|
||||
|
||||
|
|
@ -47,3 +48,32 @@ Before designing anything, **measure whether `Asyncify.currData` is clean (null)
|
|||
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`](04-decisions-tests-open-questions.md).
|
||||
|
||||
---
|
||||
|
||||
## RESOLUTION (2026-06-12) — see [`07-decisions-and-outcome.md`](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.
|
||||
|
|
|
|||
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