Commit graph pcbjam/docs/features/async/16-fiber-resume-guard.md
Author SHA1 Message Date
Viktor Vaczi
9c475a804e jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.

Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
  could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
  (WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
  WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.

Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
  workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
  of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
  scaffolding (existed to parallelize the deleted wasm-opt phase; the
  postprocess is a seconds-long node script and now runs inline),
  build-monitor's dead asyncify rows, sched-context orphan build
  output, dead .gitignore entries, the .jspi-assets spike dir (the two
  wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
  called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
  main_stack_runner.h + 5 includes, the always-null context-sleep weak
  hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
  noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
  WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
  (gerber-demo keeps it: it loads the deployed CDN release, which
  predates emscripten 6 — noted inline).

Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).

Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).

Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).

Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).

Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
Gergő Törcsvári
37c5b7e414
fix(async): the board-load crash, cured at the dispatch site
Picks up the wxwidgets fix (main loop schedules its events into a fresh JS
task instead of dispatching them inside its own Asyncify wake continuation)
and documents the whole round in docs/features/async/16.

Local verification on the warm-load repro built yesterday — the case that
failed every warm load on every build since v0.1.12: 3/3 loads settle with a
fully rendered Leonardo board, rootHotTotal=0 (it was exactly 1 at every
death), fcsTotal=72, no traps. Full kicad e2e: 136 passed, 1 failed, and that
one (occ-probe's GLB format) fails identically on a build without this change
— a pre-existing OCC build-flag issue, tracked separately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-03 12:01:06 +02:00
Gergő Törcsvári
84a40d4492
diag(asyncify): write-time instrumentation + local warm-load repro findings
The prod differential ladder finished: staged byte VOLUME on a warm load is
the only trigger left (V1a siblings-without-lib-tables dies, V1b +120 files
survives, V1c sibling KiCad files renamed byte-for-byte dies, V1d Leonardo +
123MB of inert markdown dies on loads 3-4; 14MB never dies). 3D models,
collab/ydoc/presence, lib tables, sibling KiCad handling and file count are
all exonerated — volume only loads the dice on the underlying race.

That made the crash reproducible locally for the first time in six campaigns:
a persistent browser profile + a 110MB project fails every warm load with the
exact prod signature. Iteration is now ~12 minutes instead of a release cycle.

Shim: every fiber switch now records the departing side's remaining asyncify
buffer and its recorded rewind entry (rem=/rf=), which is what identified the
unrewindable capture and disproved buffer overflow. The deferral family is
closed for good — a microtask-deferred retry on a clean empty stack died
identically to the nested rewind, because the suspension is broken at write
time, not by nesting.

Shell: log the origin stack when wx reports the top window destroyed. That
notification fires from ~wxTopLevelWindowWasm for ANY top-level window, so a
transient frame dying mid-load navigates the user out of the editor — a real
bug in its own right, found while chasing the empty flight-recorder dumps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-03 11:05:10 +02:00
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
Gergő Törcsvári
210b079ed9
fix(asyncify): ownership-scoped root deferral — the recorded nested self-rewind, cured
The v0.1.23 flight recorder caught the kill live (console-export-2026-8-1_19-16-8):
dozens of benign fiber round-trips at w=0, the yield cycling healthily on its
buffer — then "fcs … ROOT w=1" and the trap, state frozen at Rewinding with
currData=root+20. The fatal condition, observed rather than inferred: a fiber
round-trip inside the ROOT's OWN sleep-wake continuation re-suspends and
re-rewinds the root nested inside its live wake rewind. Consume-once passed
correctly — it guards a different corruption and stays.

The round-3 deferral was aimed right but unscoped (taxed fiber-owned wakes,
flaked S4). Final form: every fresh sleep is tagged root- or fiber-owned
(fiber ⇔ started inside a finishContextSwitch fiber slice or a fiber-owned
wake; root entries don't count as slices); finishContextSwitch(root) defers
one macrotask ONLY while a root-owned wake is live (Asyncify.__wakingRoot).
Beacon: root-entry-deferred. Verified inert where it must be: zero beacons
across all 13 drift-trio-scenarios logs (26/26 + 25/26-then-26/26 stress —
the single miss carried no beacons, i.e. the pre-existing under-load flake).

Also: resume re-entries no longer push sleep contexts (the v0.1.23 dump
carried ~380 leaked zero-linked entries), and wake events in the recorder are
tagged R/f for ownership.

.ci-cache-epoch 5→6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 19:49:38 +02:00
Gergő Törcsvári
ae33a100c2
fix(asyncify): consume-once root suspensions — replaces the wake-window deferral
The deferral (9ca2ac1) modeled the wrong condition and taxed every parked
fiber completion with a macrotask hop: under CI load that stretched
three-client apply chains and flaked drift-trio S4 twice consecutively
(26/26 green locally under stress) — retracted.

The actual fatal state, readable in all four prod stacks once seen: a SECOND
rewind of the same root suspension. Root suspends once per fiber_swap out of
it; two parked fibers completing against one root suspension epoch (a tool
fiber + a collab fiber both waking around open:settled) each drive
finishContextSwitch(root) — the second rewinds already-consumed data →
"unreachable executed" → poisoned runtime, with the wake-side "index out of
bounds" as the sibling symptom.

Cure: stop exempting root from the validity check the shim already keeps.
First consumption proceeds synchronously — zero added latency anywhere; the
second is refused ([wx-asyncify] "root suspension already consumed") — the
yielded fiber stays properly suspended and resumable, root continues via its
real pending resume, libcontext's ghost-epoch contract enforced one layer
lower. Root remains exempt only from the internally-parked quarantine (its
yield park is routine).

.ci-cache-epoch 4→5 (the epoch-4 cache holds the retracted deferral shim).

Local: fiber 2/2 + timer 1/1, firefox sweep 20 passed, drift-trio-scenarios
kicad-chromium 26/26 under 3-worker stress, web fatal+follow 2/2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 15:53:18 +02:00
Gergő Törcsvári
eff5befd5d
fix(editor): DOM-level blue-screen floor — React can no longer white-screen a crash
v0.1.22's WasmErrorBoundary was still not enough: a commit-phase throw in
WasmTool's OWN effects unmounts the root, and no boundary below it helps.
fatal-screen.ts is the floor: plain-DOM blue screen with its own mirrored
log ring (append feeds recordFatalLog), installed at module import in
main.tsx — before and independent of React. It cooperates with the React
overlay: hidden while [data-testid="fatal-overlay"] exists, takes over via a
1Hz ensure-loop the moment it disappears. Fatal promotions also append the
asyncify flight-recorder dump so whichever screen survives carries the
targeting data.

fatal-overlay.spec.ts now also rips out the React root after the fatal and
asserts the DOM floor takes over with the mirrored [fatal] log.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 14:07:09 +02:00
Gergő Törcsvári
5333099810
test(kicad): poisoned-attribution lever — the laundering scenario, red/green
kicadTestFiberParkStartSecond/PokeSecond: a second coroutine started while
the first body is asyncify-parked reproduces the misattributed jump that
launders the parked fiber past the C++ guard (the v0.1.21 prod bypass).
Spec scenario 2 stages it and asserts the JS stale-rewind guard quarantines
the laundered resume (exactly one fiber-resume-refused beacon), the parked
body completes undisturbed, and both coroutines finish cleanly.

Doc: async/16 rounds 2 + WSOD section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 10:05:42 +02:00
Gergő Törcsvári
e561507898
fix(async): fiber resume guard — the prod board-load trap, red/green
Companion to kicad f0ce20ef64 (libcontext swap_suspended guard), which this
pins. The v0.1.20 diagnostics decoded the crash that survived v0.1.13–19:
TOOL_MANAGER Resume()s a coroutine whose body is asyncify-parked inside
handleSleep, the swap rewinds the stale fiber suspension, and the runtime is
poisoned. Full chain of evidence in docs/features/async/16-fiber-resume-guard.md
(+ round-3 addendum in 15-timer-park-repro.md).

- wasm/bindings/fiber_park.h + kicadTestFiberPark{Start,Prime,Poke,State}
  exports (pcbnew + merged kicad_editor): stages Call→yield→legitimate
  resume→sleep park→mid-park Resume, the exact prod state machine. The
  first yield matters: it primes a real (then stale) suspension, matching
  long-lived tool loops rather than a first-slice park.
- tests/kicad/fiber-resume-park.spec.ts: asserts the healthy contract on
  polled state only (embind returns across fiber swaps are unwind
  placeholders). RED on the unguarded build — fiber/sleep buffer
  cross-restores, a jump-ghost beacon, the parked body zombified. GREEN with
  the guard: mid-park poke refused ([collab-fcontext] jump-refused beacon),
  park completes, post-yield resume works, no trap signatures.
- Regression sweep green: timer-park-repro, collab-load-fuzz, load-pcb,
  pcbnew-collab, collab-undo, eeschema-collab (19 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-07-31 23:37:05 +02:00