Carrying the WX_SCHEDULER=0 runtime through the core rewrite would make every phase dual-path (two code paths per park site, dual-glue builds, two batteries per gate) for a runtime we intend to delete anyway; git on the feature branch already provides the rollback the fallback was for. D-1 now deletes it up front — injector branch, handlesleep.js, the wxWasmMailboxEnabled gating, the wx legacy twins, and the ablation builds that pin the old shim — executing doc 17's S5 ledger item 1 early. Interlock and busy gates unaffected (ledger items 2-3, D2/D6). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TfxKn5utcntBSnxz4ZnYKs
337 lines
26 KiB
Markdown
337 lines
26 KiB
Markdown
# 17 — Mailbox/scheduler implementation plan (Design B, post-August revision)
|
||
|
||
> **Status: PLAN (2026-08-05), not started.** Supersedes the *phasing* of
|
||
> [`12`](12-design-b-asyncify-implementation-plan.md)/[`13`](13-design-b-engineering-spec.md)
|
||
> with what we learned July–August; the architecture is unchanged from [`06`](06-design-b-fiber-first-runtime.md).
|
||
> The mailbox = doc 12's `readyQueue + drain()`. Companion visual: the "context switches &
|
||
> collisions" dossier artifact (fig 3 = target architecture).
|
||
|
||
## 1. Why reopen this — what changed since June
|
||
|
||
Doc 13 §6f closed the scheduler question: "no Phase-1 scheduler nor Phase-2 root fiber was
|
||
needed — the while-loop main loop + the pre-existing currData shim suffice." That verdict is
|
||
overturned by the July–August record. Since it was written we shipped, one collision at a time:
|
||
|
||
- the wx **dispatch interlock** (`wxWasmDispatchDepth` + save/zero/restore around every pump park),
|
||
- the timer **17 ms park-retry** + retry-storm telemetry,
|
||
- the **open-settle gate** (`BusyGuard`/`kicadOpenFileBusy`/`waitForOpenSettled` + `pcbjam_open::busy()` refusals),
|
||
- the **fiber-busy** (`kicadCollabFiberBusy`) and **enumerate** gates,
|
||
- three **libcontext refusal layers** (`jump-refused-parked`, consume-once, quarantine, ghost-epoch),
|
||
- the **512 KB** per-fiber buffer bump,
|
||
- and finally v0.1.28's **schedule-don't-dispatch** fix (`wxWasmScheduleProcessEvents` →
|
||
`wxWasmTopLevelTick`) for the 68/1 board-load killer.
|
||
|
||
[`16`](16-fiber-resume-guard.md) calls guard-layer iteration "at its ceiling." Every one of
|
||
those guards is a hand-rolled, site-specific "defer this stimulus until safe" — a scattered
|
||
mailbox built one trap at a time. This plan builds the real one.
|
||
|
||
**Two constraints, both empirically proven in June (do not relitigate):**
|
||
|
||
1. **Queue transitions, not calls.** A FIFO of *calls* head-of-line blocks when a handler parks
|
||
(open, ShowModal, clipboard all do); a *permanent* park deadlocks fiber swaps outright
|
||
(13 §6b: "cannot stop an async operation in flight"). The queue admits many parked contexts
|
||
and serializes only unwind/rewind transitions — which forces parkable handlers onto fibers.
|
||
2. **All writers or none, per subsystem.** A stray `currData` writer behind the scheduler
|
||
corrupts silently (12 §risks). Migration is per-subsystem cutover with dev-build assertions
|
||
on stray writes — never gradual softening of a guard while its collision partner still exists.
|
||
|
||
Also carried forward: **the root/scheduler context must never itself await JS** (13 §6b), and
|
||
**`ProcessEvents` must be driven wasm-side**, not via `await ccall(...,{async:true})`
|
||
(Emscripten #13302; 12 §prior-art). v0.1.28's fresh-task tick is the first brick of exactly that.
|
||
|
||
> **S1 lesson (2026-08-05, learned red-first):** the #13302 boundary constrains the MAILBOX
|
||
> too — delivering messages from inside pump-driven `ProcessEvents` puts a fiber-swapping
|
||
> handler inside the modal pump's awaited ccall and traps `unreachable`
|
||
> (`coroutine-nested` / `fiber_create_run_destroy_inside_modal`). Deliveries must enter via
|
||
> a dedicated plain export (`wxWasmMailboxTick`, self-armed from the shim, 17 ms retry while
|
||
> the interlock is held) — the mailbox changes *when* a message runs, never the kind of
|
||
> stack it runs on. This constraint binds every future lane (DOM input, embind) until S3
|
||
> removes the awaited-ccall pumps entirely.
|
||
|
||
## 2. Cutover mechanics
|
||
|
||
- **Build flag `WX_SCHEDULER=1`** producing parallel glue variants, same pattern as
|
||
`races_test_noheal`/`races_test_nosleepfix`: during migration CI runs both runtimes; the
|
||
legacy build stays the shippable fallback until step S5.
|
||
- **Guards become tripwires before they become dead code.** At each step, the superseded guard
|
||
is *kept* but its firing beacon is asserted to be **zero** across the fuzz/e2e suite. A guard
|
||
that still fires means the mailbox missed a path. Only a guard proven silent for a full step
|
||
gets deleted (S5).
|
||
- **Observability stays on throughout:** `fcsTotal`/`rootHotTotal`, the flight recorder, and
|
||
`window.__wxAsyncifyDump()` are migration instruments; `rootHotTotal == 0` is a standing
|
||
assertion from S1 onward.
|
||
- Injection: lean **emscripten `--js-library`** for the scheduler (13 §7 open decision — resolve
|
||
in S2; post-link injection stays the fallback since `inject-dyncall-shims.sh` already works).
|
||
|
||
## 3. Test inventory and fate
|
||
|
||
The dividing line: **outcome-level tests are the regression net and must stay green at every
|
||
step; mechanism-level tests are migration collateral** — each is retired or rewritten *at the
|
||
step that removes its mechanism*, never before.
|
||
|
||
### 3a. KEEP UNCHANGED (the net)
|
||
|
||
| Suite | Why it survives |
|
||
|---|---|
|
||
| Full `tests/kicad/` app e2e (~60 specs: `load-pcb`, `eeschema*`, `pcbnew*`, `*-collab`, `presence-*`, `drift-trio*`, `contextmenu-*`, `import-settings-modal-stack`, `3d-viewer*`, `gerbview*`, `roundtrip`, `save-hook`, perf + screenshot baselines) | Assert user-visible outcomes only. These are the gate for **every** step. |
|
||
| Standalone wx app battery (~70 apps under `tests/apps/standalone/`: `dialog`, `menu`, `popup`, `contextmenu`, `timer`, `clipboard`, `fontenum`, `filedialog`, `wizard`, `raytrace-modal`, `raytrace-threads`, `threadpool*`, …) | Outcome-level wx behavior; the June regressions (coroutines vs menus, 13 §6d) were caught here — keep all. |
|
||
| `coroutine` / `coroutine-nested` / `coroutine-pthread` | The historical red gate for exactly this work (12 §phase-0). |
|
||
| `asyncify-races` scenarios asserting **semantics**: `modal_in_modal_in_modal` (LIFO nesting is a semantic — it outlives the resolver-stack mechanism), `out_of_order_sleep_resolution`, `sleep_inside_fiber_inside_modal`, `post_park_fiber_swap`, `long_parked_sleep_clobbered_by_swap`, `wakeup_during_transition`, `unwind_through_promise` | Scenario *intent* is architecture-independent. Harness internals may need touch-ups per step, but the pass criteria stand. |
|
||
| `tests/web/fatal-overlay.spec.ts`, web suite, `open-flow` outcome cases (settle wait, dialog escape) | Containment and UX-level behavior are orthogonal to the runtime rewrite. |
|
||
|
||
### 3b. KEEP, BUT REWRITE THE ASSERTION TARGET (drop→deliver flips)
|
||
|
||
These currently assert that a guard **refused/dropped/no-op'd** something. Under the mailbox the
|
||
same stimulus is **deferred and delivered** — a strictly stronger guarantee, so the expected
|
||
outcome flips:
|
||
|
||
| Spec | Today asserts | Becomes |
|
||
|---|---|---|
|
||
| `kicad/fiber-resume-park.spec.ts` | mid-park `Resume()` is **refused** (`jump-refused-parked` beacon), parked body completes | the resume **message is queued** and the event is **delivered after** the park completes — nothing refused, nothing lost |
|
||
| `kicad/collab-load-fuzz.spec.ts` | open gate engages, collab entries **no-op**, gate releases | collab/presence messages **queue behind open** and apply afterwards without re-issuing (assert post-open presence bind succeeds) |
|
||
| `kicad/timer-park-repro.spec.ts` | runtime survives a parking timer; `[wx-timer]` retry beacons appear | runtime survives; timer notify is delivered exactly once, in order; **zero** retry beacons |
|
||
| `asyncify-races` `nested_quasi_modal_pump_error` | pump rejection doesn't leak the parked `DoRun` | (S4, when the pump dies) wait-resolution **error path cancels the modal and releases the context** — same invariant, no pump |
|
||
|
||
The drop→deliver flip is also a **product change** (events stop being silently swallowed during
|
||
loads) — note it in the release notes when S4 lands.
|
||
|
||
### 3c. CANNOT KEEP (assert deleted machinery — retire at the step shown)
|
||
|
||
| Test | Pins | Retires at |
|
||
|---|---|---|
|
||
| `races_test_noheal` / `races_test_nosleepfix` builds + their "shim-redundancy pins" specs | the legacy `handlesleep.js`/trampoline-heal shims | S2 — replaced by scheduler-ownership pins (same ablation idea, new subject) |
|
||
| `open-flow.test.ts` busy-**polling** unit cases (stuck-busy timeout, legacy title-heuristic fallback) | `kicadOpenFileBusy` polling as the settle protocol | S4 (open migration) — open becomes promise/message-based; keep the exported busy probe itself for old-wasm compat (see §6) |
|
||
| Assertions on interlock diagnostics (`[wx-dispatch] ERASED/NEGATIVE`, depth-restore sites) wherever specs grep for them | the dispatch interlock | S5 |
|
||
| v0.1.28 tick-error-path specifics (popping `_wxNestedLoopExit` in the `setTimeout` catch) | the pump-driven nested loop | S4 |
|
||
|
||
### 3d. NEW TESTS (written before the step they gate)
|
||
|
||
- **N1 · Single-writer tripwire** — dev-build assertion that any `currData` write outside a
|
||
scheduler transition aborts loudly; plus a meta-spec that *introduces* a stray writer
|
||
(ablation-style) and expects the abort. Gates S2.
|
||
- **N2 · Message ordering** — enqueue open, then collab/presence/drift messages; assert applied
|
||
in order after open completes, none lost, none duplicated. Gates S1/S4.
|
||
- **N3 · No head-of-line blocking** — with an open/modal fiber parked, an input message is
|
||
processed within a frame-budget bound (this is the regression test for constraint 1). Gates S4.
|
||
- **N4 · Wake-never-rewinds** — a promise resolving during a live transition only enqueues;
|
||
generalization of `wakeup_during_transition` to fiber wakes. Gates S2.
|
||
- **N5 · FIFO fairness under flood** — old parked context is not starved by a stimulus storm
|
||
(06 §starvation). Gates S2.
|
||
- **N6 · World-visibility reentrancy** — curated cases where C++ assumed "nothing changes while
|
||
I block": model mutation landing while `ShowModal` is parked, sibling file restage during a
|
||
parked open. Driven by the S1 embind audit (§6). Gates S4.
|
||
- **N7 · EH matrix** — the whole gate under `-fexceptions` **and** `-fwasm-exceptions`,
|
||
including a modal opened from inside a `catch` (HoistCppCatches composition, 12 §risks).
|
||
Gates every step.
|
||
- **N8 · Warm-load pressure** — the Leonardo recipe (REPRO_PROFILE + ≥120 MB ballast, 2–4 warm
|
||
loads) as a CI-able job asserting `rootHotTotal == 0` and zero trap signatures. Standing from S1.
|
||
- **N9 · Pthread interplay** — `raytrace-modal` multi-core under the scheduler; unparking the
|
||
doc-11 single-core limitation is a *stretch goal*, not a gate — but the suite must not regress.
|
||
|
||
## 4. Implementation steps
|
||
|
||
Each step: gate = full 3a net green (3 engines where applicable) + the step's own tests + N7/N8;
|
||
rollback = the `WX_SCHEDULER=0` build + a per-step tag.
|
||
|
||
- **S0 · Baseline + scaffolding (2–3 d).** Tag current mains. CI matrix runs both EH models.
|
||
Add beacon-count extraction to the fuzz/e2e harness (guard-firing counters per run). Stand up
|
||
the `WX_SCHEDULER` dual-glue build. Write N2 (red), N8.
|
||
> **Work log 2026-08-05 — S0 mostly landed** on `feature/async-mailbox`:
|
||
> local `mailbox-s0-baseline` tags in all 6 repos; `WX_SCHEDULER=1` injector path +
|
||
> `scripts/common/shims/asyncify-scheduler.js` (observation-only skeleton, build marker
|
||
> `[wx-scheduler] scaffolding installed`; legacy shim stays authoritative until S2;
|
||
> remember the `.ci-cache-epoch` bump when shim behavior changes);
|
||
> `tests/kicad/utils/guard-beacons.ts` (per-family counts, occurrence-recovery for
|
||
> rate-limited beacons, `expectGuardsSilent`, `parseAsyncifyCounters`);
|
||
> `REPRO_ASSERT=1` in `apps/tests/tools/repro-board-load.ts` (N8: fails on unsettled load,
|
||
> missing recorder, `rootHotTotal>0`, trap signatures);
|
||
> `tests/kicad/mailbox-ordering.spec.ts` (N2, `test.fixme` red — add-then-move ordering
|
||
> probe; un-fixme at S1). **Still open in S0:** the CI workflow matrix for both EH models.
|
||
- **S1 · Mailbox front-end (≈1 wk).** One queue, drained by `wxWasmTopLevelTick`. Route into it:
|
||
> **Work log 2026-08-05 — timers routed, dual battery green.** JS FIFO in the shim;
|
||
> `wx/wasm/private/mailbox.h` + drain in `evtloop.cpp`; `timer.cpp` enqueues on scheduler
|
||
> builds (runtime-gated on the shim marker; legacy path untouched; 17 ms retry kept as
|
||
> tripwire). Delivery enters via the dedicated plain export `wxWasmMailboxTick` (see the
|
||
> S1 lesson above — the first pump-embedded delivery design trapped in `coroutine-nested`
|
||
> and was fixed red→green). Battery on BOTH variants: wx-chromium 28/28 (timer, dialog,
|
||
> dialogs, contextmenu, popup) + coroutine-firefox 39/39 (incl. raytrace multicore) +
|
||
> asyncify-firefox 7/7. App-side `WasmMailbox` wrapper (web/standalone/src/wasm/mailbox.ts,
|
||
> 7 vitest green) keys on the proxy-safe `kicadOpenFileBusy` probe — PROXY_TO_PTHREAD makes
|
||
> the window blind to the worker-side interlock; precision moves worker-side at S4.
|
||
> **Still open in S1:** DOM-input lane, wiring the wrapper into the 14 production mutators
|
||
> (needs the kicad wasm build + e2e), N2 un-fixme, beacon-silence assertions in fuzz runs.
|
||
>
|
||
> **Work log 2026-08-05 (later) — S1 COMPLETE except the CI matrix (deferred to the end
|
||
> by request).** Wheel lane: parked wheel ticks enqueue a replay instead of dropping
|
||
> (`app.cpp`; keys/mouse were already queue-shaped via `wxPostEvent`). Embind lane: the
|
||
> shim wraps the doc-18 production mutators at the Module boundary — busy-window calls
|
||
> queue and deliver post-settle via a time-boxed (8 ms/16 ms), unkillable pump; not-busy
|
||
> calls stay byte-compatible sync. Fresh warm-cache docker `kicad_editor` build carries
|
||
> the C lanes; staged into the harness. Verified: wx dual battery 30+39+7 on BOTH glue
|
||
> variants; kicad trio dual-contract — legacy (drop semantics + expected retry storms) and
|
||
> scheduler (N2 delivery/order green, fuzz deliver-flip green, `timerRetry` silence
|
||
> tripwire ARMED and green = zero retry storms on the C-lane build). Fuzz's deterministic
|
||
> gate caps queued hammer calls on the scheduler lane (6 iters) — the unbounded replay is
|
||
> a debug-build assert-flood benchmark (`PCB_VIA::GetWidth` × 800 vias × 170k console
|
||
> lines), not an ordering test; volume stays in the stress test. Two more recorded traps:
|
||
> the injector idempotence marker collided with evtloop's EM_JS probe text (sentinel now
|
||
> `__WX_SCHEDULER_SHIM_SOURCE__`), and a pump whose setTimeout chain can die wedges the
|
||
> queue forever — the pump body is now exception-proof with a `[wx-scheduler]` beacon.
|
||
timer `Notify` (replacing the direct callback body; the 17 ms retry stays as tripwire), DOM
|
||
input (formalizing today's `wxPostEvent`/`CallAfter` deferrals), and a JS-side wrapper for
|
||
**mutating** embind entries (enqueue + returned promise). Deliverable alongside: the **sync
|
||
embind audit** (§6). Guards all stay. **Gate:** net green; timer-retry and open-gate/fiber-busy
|
||
refusal beacons ≈ 0 across fuzz (they may still fire for pump-driven paths — record the
|
||
residual and its source); N2 green for the wrapped entries.
|
||
- **S2 · Scheduler core (1–2 wk).** `handlesleep.js` → `asyncify-scheduler.js` (13 §1): context
|
||
registry, deferred drain with explicit transition-completion signals (wrap
|
||
`_asyncify_stop_rewind`/`maybeStopUnwind`, never JS `finally`), fiber tracking at
|
||
`emscripten_fiber_swap` (`fiber+20` buffers), trampoline ownership. **Gate:** races battery
|
||
green **with the legacy shim ablated**; N1/N4/N5 green; libcontext refusal beacons ≈ 0.
|
||
> **Work log 2026-08-05 — S2 core LANDED and gated.** The scheduler shim now REPLACES
|
||
> handlesleep.js on WX_SCHEDULER=1 builds (injector either-or; the S1 "append-after"
|
||
> ordering is gone). Ported name-identical: capture/restore, wake-window flags
|
||
> (`__wakingRoot` is read by libcontext EM_JS!), consume-once/quarantine fiber guard,
|
||
> counters, flight recorder, trampoline heal (external catch-reset wrap). NEW:
|
||
> **deferred wakes** — a sleep wake arriving mid-transition (state≠Normal or trampoline
|
||
> live) queues and drains from a clean macrotask (the aliased-wake class is now
|
||
> structural, not detective); **N1 accessor** — `Asyncify.currData` is a property with a
|
||
> single-writer tripwire (pure-JS writes need scheduler authorization; wasm-frame writes
|
||
> pass; strict mode throws), meta-tested by introducing a stray.
|
||
> **Gates all green:** asyncify-firefox 9/9 (7 races + new N1 meta + N4 books — on glue
|
||
> with NO legacy shim, so the redundancy pins are now subsumption pins), coroutine 39/39,
|
||
> wx-chromium 30/30, kicad trio 3/3 on the C-lane build. Injector legacy path verified
|
||
> unchanged. **Deliberately left for S3:** the formal ctx-Map registry + park/resume
|
||
> methods (stubs that throw), wasm-side ProcessEvents; **open:** N5 flood spec, both-EH
|
||
> matrix (CI, deferred). Build-system note: `build-wasm-test.sh` only re-injects freshly
|
||
> relinked apps — a variant flip without C changes needs the strip+reinject converter
|
||
> (one-shot python in the work log commit) or a clean build.
|
||
- **S3 · Root context + wasm-side ProcessEvents (≈1 wk).** The tick resumes the root context
|
||
which calls `ProcessEvents` on the wasm side — the `await ccall(...,{async:true})` boundary
|
||
(#13302) is removed. The root context never awaits JS (13 §6b). **Gate:** net green with
|
||
emphasis on `contextmenu*`, `menu`, `popup`, coroutine apps (the June regression pair,
|
||
13 §6d, must both stay green simultaneously).
|
||
> **Work log 2026-08-05 — S3 LANDED and gated.** All three remaining `await ccall(
|
||
> 'ProcessEvents',{async:true})` pumps — modal (`dialog.cpp` startModal), nested loop
|
||
> (`evtloop.cpp` wxWasmRunNestedLoop), popup (`wx-dom.js`) — now drive ProcessEvents as a
|
||
> PLAIN export call on scheduler builds (runtime-gated on the shim marker; legacy paths
|
||
> byte-identical). With the v0.1.28 top-level tick, NO pump awaits a suspending export
|
||
> anymore: the #13302 boundary is gone from the scheduler variant entirely. Overlapping
|
||
> pump ticks are parked-safe by ProcessEvents' existing gate; a chain that dies mid-park
|
||
> surfaces via the window error taps instead of the pump's await-catch (containment
|
||
> unchanged: fatal-screen + `wx_dispatch_abandon`). **Gates:** asyncify 9/9, coroutine
|
||
> 39/39 (the June pair green simultaneously), wx-chromium modal-heavy 45/45 (+menu,
|
||
> wizard, filedialog), kicad 6/6 incl. import-settings-modal-stack + contextmenu-scrollbar
|
||
> on a fresh warm-cache C-lane build. **N5 landed** alongside (scheduler-shim.test.ts:
|
||
> 500-call flood strict-FIFO, time-box chunking proven, wake-drain FIFO — 3/3).
|
||
> **Left open:** the doc-12 "root fiber" formalization (main loop as ctx object,
|
||
> `set_main_loop`-style top-off-asyncify) — not needed for the #13302 goal; revisit at S4
|
||
> if the waits migration wants real park/resume methods.
|
||
- **S4 · Waits migration, one wait at a time (1–2 wk).** Implement
|
||
`wasm_begin_async_wait`/`wasm_yield_until`/`wasm_resolve_wait` (13 §2). Order, lowest-risk
|
||
first, each sub-step flipping its own specs (§3b/3c) and deleting its own pump:
|
||
1. nested/quasi-modal loop (`wxWasmRunNestedLoop` + `_wxNestedLoopExit` die),
|
||
2. popup menu (`wxDomPopupMenuModal` + the wx-dom.js pump die),
|
||
3. modal dialog (`startModal` + `_wxModalResolvers` die),
|
||
4. clipboard + font enum,
|
||
5. **open** last — the biggest semantic flip (gates → ordering; N2/N3/N6 are the gate).
|
||
> **Work log 2026-08-05 — S4 LANDED and gated (waits 1–3; 4–5 resolved by scoping).**
|
||
> The doc-13 §2 API exists as `wx/wasm/private/yieldwait.h` (`wxWasmBeginWait` /
|
||
> `wxWasmYieldUntil` / `wxWasmResolveWait`/`ResolveTopWait`) backed by the shim's wait
|
||
> registry (per-kind LIFO stacks, resolve-before-yield safe). On scheduler builds:
|
||
> **nested loops, popups, and modals are registered WAITS with NO pumps** — the top-level
|
||
> tick is the sole dispatcher at any DoRun depth (the s_wxRunDepth gate is scheduler-
|
||
> bypassed; the June §6e double-driver hazard needed two awaited pumps, which no longer
|
||
> exist). Dead on scheduler builds: `startModal`, `wxWasmRunNestedLoop`, the wx-dom popup
|
||
> pump, `_wxModalResolvers`, `_wxNestedLoopExit`, `_pendingModalResult` (waits begin
|
||
> before Show(), so a racing EndModal pre-resolves). Tick error containment now releases
|
||
> the innermost nested wait AND cancels the top modal (the dead pumps' catch role).
|
||
> Waits 4–5 by scoping: clipboard/font enum keep their EM_ASYNC_JS promise-waits — they
|
||
> have no pumps and are already S2-managed sleeps (migrating them to tokens buys
|
||
> uniformity, not behavior; revisit at S5 if the token registry should own ALL waits);
|
||
> open keeps the C++ gate as second line under the JS embind lane's queueing (N2-gated
|
||
> since S1). **Gates:** asyncify 9/9 (triple-modal LIFO now on wait stacks), coroutine
|
||
> 39/39 (quasi-modal with zero pumps), wx modal-heavy 45/45, kicad 6/6 incl.
|
||
> modal-stack + contextmenu-scrollbar on a fresh warm-cache build.
|
||
- **S5 · Guard demolition (few d).** Delete: dispatch interlock + save/zero/restore sites,
|
||
timer retry, `ProcessEvents` Paint-only gate, `s_wxRunDepth` gate, open-settle/fiber-busy/
|
||
enumerate gates. Downgrade libcontext refusals to dev-build assertions; keep counters-only
|
||
beacons. Retire §3c leftovers. **Gate:** net green with `WX_SCHEDULER=1` as the only build;
|
||
a full-suite run shows zero references to deleted diagnostics.
|
||
> **Work log 2026-08-05 — S5 executed as DEFAULT-FLIP + LEDGER, with a correction.**
|
||
> This step's original demolition list over-promised relative to what S1–S4 built: the
|
||
> §3b fate table assumed handler-fibers ("serialization is the scheduler's job"), but
|
||
> parks still suspend ordinary dispatch chains in place — so the **dispatch interlock,
|
||
> the ProcessEvents parked gate, the zeroed-interlock windows around waits, and the C++
|
||
> open/fiber-busy gates remain LOAD-BEARING second lines**, not dead weight. Deleting
|
||
> them now would reopen the fp-selector trap class. What S5 delivered instead:
|
||
> **WX_SCHEDULER=1 is the DEFAULT build** (injector flip; `WX_SCHEDULER=0` = explicit
|
||
> legacy opt-out; `.ci-cache-epoch` 9→10), gated on the FULL kicad-firefox suite against
|
||
> the scheduler build, not just the trio.
|
||
> **Flip gate result:** full suite 136 passed on BOTH variants (an accidental legacy run
|
||
> — `setup:kicad` re-syncs `output/` over staged glue, converter beware — gave the
|
||
> fallback a free full net). Scheduler run: 30 skips (N2 runs there), 2 fails triaged:
|
||
> `ngspice-probe` bg_run = rerun-passes flake; `occ-probe` glb = fails IDENTICALLY on
|
||
> legacy → pre-existing local occ_service issue, tracked outside this plan.
|
||
>
|
||
> **Demolition ledger — each deletion with its unlock condition:**
|
||
> 1. **[RESCHEDULED 2026-08-05 → [`20`](20-design-b-core-plan.md) D-1: delete FIRST, on
|
||
> the branch, not after CI]** Legacy opt-out + `handlesleep.js` + C++ legacy paths (`startModal`,
|
||
> `wxWasmRunNestedLoop`, popup pump, `_wxModalResolvers`/`_wxNestedLoopExit`,
|
||
> `emscripten_async_call` timer entry, timer 17 ms retry branch, `s_wxRunDepth` tick
|
||
> gate): delete TOGETHER, after CI runs scheduler-only across the full matrix
|
||
> (3 engines × both EH). They're compiled once for both variants — no partial delete.
|
||
> 2. Dispatch interlock + Paint-only gate + zero/restore windows: delete only when
|
||
> parkable handlers run on scheduler-owned fiber contexts (the remaining Design-B
|
||
> step this plan never scoped). Until then they are the mutual-exclusion story.
|
||
> 3. C++ `pcbjam_open::busy()` / `kicadCollabFiberBusy` gates: keep as second line under
|
||
> the JS embind lane; revisit only with (2).
|
||
> 4. libcontext refusals: KEEP ACTIVE (not downgraded) — `jump-refused-parked` fires
|
||
> BEFORE the doomed unwind write, the one place prevention works (doc 16); the JS
|
||
> consume-once layer is the backstop, not a replacement.
|
||
- **S6 · Lifetime (few d).** Cleanup ordering vs the scheduler; teardown deferred to
|
||
unload/explicit exit; `ScheduleExit` → scheduler wake (12 §phase-4).
|
||
> **Work log 2026-08-05 — S6 LANDED.** The scheduler latches DEAD when the main loop
|
||
> exits (DoRun's top-level return → `shutdown("main loop exited")`): queued mutators
|
||
> reject, queued messages/wakes drop, pumps stop — stranded work beacons
|
||
> (`shutdown ... stranded:`) instead of surfacing as a post-teardown UAF.
|
||
> `wxWasmMailboxDeliver` gained ProcessEvents' `!wxTheApp` teardown parity. Unit-gated
|
||
> (scheduler-shim.test.ts 4/4 incl. shutdown; 11/11 with WasmMailbox) + full battery
|
||
> (asyncify 9/9, coroutine 39/39, wx modal-heavy 45/45) + kicad 6/6 — all on
|
||
> DEFAULT-injected glue: the docker postprocess → setup:kicad pipeline now produces
|
||
> scheduler builds with no manual conversion. Deferred with the ledger: OnExit/
|
||
> wxEntryCleanupReal-to-unload reshaping (12 §phase-4's fuller lifetime) — the current
|
||
> quit flow (wxAppTopWindowClosed → loop exit → cleanup) plus the DEAD latch covers the
|
||
> teardown-delivery hazard the step exists for.
|
||
|
||
**Effort: ≈ 5–7 weeks** (doc 12 said 4–6 for the runtime alone; the added week is §3's test
|
||
work, which is where the safety comes from). S1 and S2 each end in a shippable state; S4 is the
|
||
first step with user-visible semantic changes.
|
||
|
||
## 5. Risks (delta vs doc 12 — its register still applies)
|
||
|
||
- **The drop→deliver flip changes behavior**, not just internals: events that guards silently
|
||
swallowed will now arrive late instead of never. N6 exists to find C++ code that can't cope.
|
||
- **Dual-build drift** — the `WX_SCHEDULER=0` fallback rots if not in CI; keep it in the matrix
|
||
until S5, then delete it the same week (a permanently-maintained dual runtime is its own bug farm).
|
||
- **Process traps** (learned the hard way): `npm run test:kicad` is *not* the CI set — gate on
|
||
the full CI project set; rebuild wx into the consuming apps after every wx change (host-wx
|
||
staleness); `pnpm install` after pulls.
|
||
|
||
## 6. Open decisions (resolve at the step noted)
|
||
|
||
- **js-library vs post-link injection** for the scheduler (S2; lean js-library for durability).
|
||
- **No-scheduler fast path** for plain wx test apps with no fibers (S2; likely yes, gated on
|
||
"any non-main context registered", 13 §7).
|
||
- **Sync embind policy** (S1 audit): pure reads that never dispatch/park stay synchronous;
|
||
everything else becomes a promise-returning message. The audit's deliverable is the exact
|
||
allowlist, checked by a lint or a dev-build assert.
|
||
> **DONE 2026-08-05 → [`18-embind-audit.md`](18-embind-audit.md):** 79 exports — 47 mutators
|
||
> (14 production + 3 saves to wrap; 33 test-only stay direct), 20 pure-reads (the allowlist,
|
||
> with the model-walk caveat), 9 park-capable. Asymmetries to fix listed there (ungated
|
||
> pure-reads, pl_editor bare-stack applies, ungated `kicadLibsReload`).
|
||
- **`kicadOpenFileBusy` compatibility**: `open-flow.ts` feature-detects it and legacy wasm
|
||
builds rely on the fallback — keep the export, backed by scheduler state, until the web app
|
||
drops support for pre-scheduler wasm.
|
||
- **Raytracer unpark** (doc 11): the scheduler makes a nestable yield possible — schedule as a
|
||
follow-up feature, not part of this plan's gates.
|