jspi: migration phases 0-7 — build knob, scheduler shim, test successor suite
Toolchain: emsdk 6.0.6 (versions.sh; cache-hash keys on it). Build knob
PCBJAM_ASYNC_BACKEND=jspi|asyncify: build-kicad-target.sh links editors with
-sJSPI + -sJSPI_EXPORTS=@scripts/common/jspi-exports.txt + --pre-js
jspi-scheduler.js (no DYNCALLS, no post-link asyncify pipeline); wx build
stamps the backend and forces clean on flip or unknown provenance;
docker/build.sh passes the knob, seeds the emscripten ports cache from the
volume every launch, jspi postprocess = patch-env-shim only.
scripts/common/shims/jspi-scheduler.js: the JSPI successor scheduler —
token-wait registry, resume turnstile (one armed resume between engine
re-entries, SP swaps only at microtask boundaries), green-region spill
stacks (16-aligned tops), S1 embind mutator FIFO lane + parker wraps, S6
shutdown, libctx integration hooks (suspend/end/quarantine + g_current
arm/clear), SuspendError attributor, lost-wake + stuck-window watchdogs,
__wxWaitDump observability.
Embind: PARKER registrations get emscripten::async() under PCBJAM_JSPI
(wasm/bindings/pcbjam_async_policy.h). nanosleep yields route via the shim.
Tests: tests/asyncify -> tests/jspi successor suite (jspi-stack red/green
shadow-stack battery, jspi-coroutine MiniCoro harness, suspend-races
semantic scenarios + __wxWaitDump books coherence); projects jspi-firefox/
jspi-chrome (asyncify-webkit retired — no JSPI in WebKit); unconditional
Firefox JSPI pref; guard-beacons -> wait-beacons (+wxScheduler/libctxJspi
families); Makefile.wasm links test apps against JSPI with the shim as a
tracked link prerequisite.
Web: WasmTool setRo await + __wxWaitDump forensics, open-flow contained
promise, scheduler-shim.test.ts retargeted (8 green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDeBaKKhQztd8KiVtHuyXr
2026-08-13 07:06:24 +02:00
|
|
|
// Wait-beacon extraction (JSPI-era successor of guard-beacons.ts; the
|
|
|
|
|
// mailbox/scheduler migration doc is docs/features/async/17, step S0.3).
|
2026-08-05 13:43:23 +02:00
|
|
|
//
|
|
|
|
|
// Every legacy anti-collision guard announces itself on the console when it fires.
|
|
|
|
|
// During the migration each superseded guard is kept as a TRIPWIRE: the mailbox is
|
|
|
|
|
// only trusted once the guard it replaces is provably silent across the suite.
|
|
|
|
|
// This module turns a TestLogger's consoleLogs into per-family counts so specs can
|
|
|
|
|
// assert `expectGuardsSilent(...)` at the step that claims a family.
|
|
|
|
|
//
|
|
|
|
|
// Rate-limiting caveat: [wx-asyncify] and [collab-fcontext] beacons print the first
|
|
|
|
|
// 10 occurrences, then every 100th, embedding "(occurrence N)". `linesSeen` is what
|
|
|
|
|
// reached the console; `estimatedTotal` recovers the true count from the highest
|
|
|
|
|
// occurrence number when present (else it equals linesSeen). Assertions on SILENCE
|
|
|
|
|
// are exact either way: zero fires = zero lines.
|
|
|
|
|
|
|
|
|
|
export interface BeaconFamilyCount {
|
|
|
|
|
linesSeen: number;
|
|
|
|
|
estimatedTotal: number;
|
|
|
|
|
samples: string[]; // first few matching lines, for the failure message
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface GuardBeaconCounts {
|
|
|
|
|
// wx timer interlock (timer.cpp): parked-dispatch retries
|
|
|
|
|
timerRetry: BeaconFamilyCount;
|
|
|
|
|
// wx dispatch interlock bookkeeping anomalies (evtloop.cpp)
|
|
|
|
|
dispatchAnomaly: BeaconFamilyCount;
|
2026-08-05 20:02:18 +02:00
|
|
|
// asyncify-scheduler.js shim: nested-park / wake-aliasing / stale-fiber refusals
|
2026-08-05 13:43:23 +02:00
|
|
|
wxAsyncify: BeaconFamilyCount;
|
|
|
|
|
// libcontext swap-layer refusals + hot-main beacons ([collab-fcontext])
|
|
|
|
|
libcontext: BeaconFamilyCount;
|
|
|
|
|
// open-settle gate giving up (open-flow.ts)
|
|
|
|
|
openSettleFailed: BeaconFamilyCount;
|
jspi: migration phases 0-7 — build knob, scheduler shim, test successor suite
Toolchain: emsdk 6.0.6 (versions.sh; cache-hash keys on it). Build knob
PCBJAM_ASYNC_BACKEND=jspi|asyncify: build-kicad-target.sh links editors with
-sJSPI + -sJSPI_EXPORTS=@scripts/common/jspi-exports.txt + --pre-js
jspi-scheduler.js (no DYNCALLS, no post-link asyncify pipeline); wx build
stamps the backend and forces clean on flip or unknown provenance;
docker/build.sh passes the knob, seeds the emscripten ports cache from the
volume every launch, jspi postprocess = patch-env-shim only.
scripts/common/shims/jspi-scheduler.js: the JSPI successor scheduler —
token-wait registry, resume turnstile (one armed resume between engine
re-entries, SP swaps only at microtask boundaries), green-region spill
stacks (16-aligned tops), S1 embind mutator FIFO lane + parker wraps, S6
shutdown, libctx integration hooks (suspend/end/quarantine + g_current
arm/clear), SuspendError attributor, lost-wake + stuck-window watchdogs,
__wxWaitDump observability.
Embind: PARKER registrations get emscripten::async() under PCBJAM_JSPI
(wasm/bindings/pcbjam_async_policy.h). nanosleep yields route via the shim.
Tests: tests/asyncify -> tests/jspi successor suite (jspi-stack red/green
shadow-stack battery, jspi-coroutine MiniCoro harness, suspend-races
semantic scenarios + __wxWaitDump books coherence); projects jspi-firefox/
jspi-chrome (asyncify-webkit retired — no JSPI in WebKit); unconditional
Firefox JSPI pref; guard-beacons -> wait-beacons (+wxScheduler/libctxJspi
families); Makefile.wasm links test apps against JSPI with the shim as a
tracked link prerequisite.
Web: WasmTool setRo await + __wxWaitDump forensics, open-flow contained
promise, scheduler-shim.test.ts retargeted (8 green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDeBaKKhQztd8KiVtHuyXr
2026-08-13 07:06:24 +02:00
|
|
|
// jspi-scheduler.js turnstile/containment beacons (JSPI builds)
|
|
|
|
|
wxScheduler: BeaconFamilyCount;
|
|
|
|
|
// libcontext JSPI backend ghost/refused-transition census
|
|
|
|
|
libctxJspi: BeaconFamilyCount;
|
2026-08-05 13:43:23 +02:00
|
|
|
// scheduler build marker — identifies the dual-glue variant, not a guard
|
|
|
|
|
schedulerBuild: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const FAMILY_PATTERNS: Record<
|
|
|
|
|
Exclude<keyof GuardBeaconCounts, 'schedulerBuild'>,
|
|
|
|
|
RegExp
|
|
|
|
|
> = {
|
|
|
|
|
timerRetry: /\[wx-timer\] retry storm/,
|
|
|
|
|
dispatchAnomaly: /\[wx-dispatch\] (ERASED|NEGATIVE)/,
|
|
|
|
|
wxAsyncify:
|
|
|
|
|
/\[wx-asyncify\] (concurrent-park|reentrant-state|aliased-wake-live|overlapped-wake|fiber-resume-refused)/,
|
|
|
|
|
libcontext:
|
|
|
|
|
/\[collab-fcontext\] (jump-refused|jump-refused-hot-main|hot-main-swap-out|jump-hot-into-main|jump-ghost|entry-orphaned)/,
|
|
|
|
|
openSettleFailed: /\[open\] load chain never settled/,
|
jspi: migration phases 0-7 — build knob, scheduler shim, test successor suite
Toolchain: emsdk 6.0.6 (versions.sh; cache-hash keys on it). Build knob
PCBJAM_ASYNC_BACKEND=jspi|asyncify: build-kicad-target.sh links editors with
-sJSPI + -sJSPI_EXPORTS=@scripts/common/jspi-exports.txt + --pre-js
jspi-scheduler.js (no DYNCALLS, no post-link asyncify pipeline); wx build
stamps the backend and forces clean on flip or unknown provenance;
docker/build.sh passes the knob, seeds the emscripten ports cache from the
volume every launch, jspi postprocess = patch-env-shim only.
scripts/common/shims/jspi-scheduler.js: the JSPI successor scheduler —
token-wait registry, resume turnstile (one armed resume between engine
re-entries, SP swaps only at microtask boundaries), green-region spill
stacks (16-aligned tops), S1 embind mutator FIFO lane + parker wraps, S6
shutdown, libctx integration hooks (suspend/end/quarantine + g_current
arm/clear), SuspendError attributor, lost-wake + stuck-window watchdogs,
__wxWaitDump observability.
Embind: PARKER registrations get emscripten::async() under PCBJAM_JSPI
(wasm/bindings/pcbjam_async_policy.h). nanosleep yields route via the shim.
Tests: tests/asyncify -> tests/jspi successor suite (jspi-stack red/green
shadow-stack battery, jspi-coroutine MiniCoro harness, suspend-races
semantic scenarios + __wxWaitDump books coherence); projects jspi-firefox/
jspi-chrome (asyncify-webkit retired — no JSPI in WebKit); unconditional
Firefox JSPI pref; guard-beacons -> wait-beacons (+wxScheduler/libctxJspi
families); Makefile.wasm links test apps against JSPI with the shim as a
tracked link prerequisite.
Web: WasmTool setRo await + __wxWaitDump forensics, open-flow contained
promise, scheduler-shim.test.ts retargeted (8 green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDeBaKKhQztd8KiVtHuyXr
2026-08-13 07:06:24 +02:00
|
|
|
// JSPI-era families (jspi-scheduler.js + libcontext's JSPI backend):
|
|
|
|
|
wxScheduler:
|
jspi: fix the dead-tools ownership bug, emscripten-6 fallout, and green the full suite on Playwright 1.62
Live-app fix (Place Footprints / routing dead in Chrome): submodule
bumps carry the coroutine ownership fix (kicad 012d95ecb4) and the
handler-exception survival fix (wxwidgets 1b5f0e31f4).
Emscripten-6 fallout:
- occ/ngspice worker wrappers: mainScriptUrlOrBlob was removed
upstream; pthread children re-run the wrapper blob, so an em-pthread
realm now importScripts the glue and gets out of the way (before:
recursive service boots, pool never fills, silent 180s boot hangs —
every occ spec and ngspice bg_run).
- Makefile.wasm: -sASYNCIFY frankenlinks on the no-wx coroutine repro
targets ported to -sJSPI (the JSPI-only libcontext crashed at first
yield under them); mainloop/gl repro pages drive their tick through a
promising export (emscripten_set_main_loop callbacks cannot suspend);
retired inject-dyncall-shims lines removed (targets were unbuildable
since Phase 8); $stringToNewUTF8 force-included (the EM_ASM value
bridge aborted the runtime on the first decoded exception).
- fiber-park levers: neither embind shape can drive suspending levers
(plain throws on strict-JSPI Firefox; emscripten::async() re-executes
its invoker on settle) — kept sync for manual Chromium probing, spec
coverage moved to the jspi-coroutine harness (18 cases).
Suite work:
- Playwright 1.61.1 -> 1.62.1 (Firefox 153: JSPI on by default).
- fiber-resume-park.spec retired -> coroutine-lifecycle.spec: census
gate over boot / board load / chooser open / cancel (deterministically
red on the pre-fix build).
- Blind asyncify-era pins re-keyed: quasimodal-strand + wait-beacons
beacon regexes, footprint-chooser-close liveness -> wx parking-timer
heartbeat (scheduler counters idle flat on Firefox).
- occ/ngspice test providers: 60s boot timeout + worker error
surfacing (a worker death used to be a silent 180s timeout).
- Harness pages: stale 9.99 config dir -> 10.0 (library_manager wxCHECK
noise, chooser had no libraries).
- gal-webgl harness: missing artifacts rebuilt (boost/glm extracted to
the host sysroot), PgmOrNull stub added for the rebased GAL.
- jspi-scheduler: clean-shutdown console line restored (app-quit
contract), quarantine never yanks SP from a live window.
Gates: test:e2e 699 passed / 0 failed (wx-chromium, kicad-firefox,
kicad-chromium, jspi-firefox, coroutine-firefox); web ff/cr/mobile 71
passed; lint:ci-coverage 166, lint:determinism 163, screenshots
manifest 492 current, corpus 7/7, tools:contract green. Offline
screenshot baselines show expected mass drift from the engine bump —
re-baseline (screenshots:noise -> promote) is a follow-up.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-13 17:41:28 +02:00
|
|
|
/\[wx-scheduler\] (force-clearing stuck window|job tick error|untracked promising entry|activation stack imbalance|resume window misnested)/,
|
jspi: migration phases 0-7 — build knob, scheduler shim, test successor suite
Toolchain: emsdk 6.0.6 (versions.sh; cache-hash keys on it). Build knob
PCBJAM_ASYNC_BACKEND=jspi|asyncify: build-kicad-target.sh links editors with
-sJSPI + -sJSPI_EXPORTS=@scripts/common/jspi-exports.txt + --pre-js
jspi-scheduler.js (no DYNCALLS, no post-link asyncify pipeline); wx build
stamps the backend and forces clean on flip or unknown provenance;
docker/build.sh passes the knob, seeds the emscripten ports cache from the
volume every launch, jspi postprocess = patch-env-shim only.
scripts/common/shims/jspi-scheduler.js: the JSPI successor scheduler —
token-wait registry, resume turnstile (one armed resume between engine
re-entries, SP swaps only at microtask boundaries), green-region spill
stacks (16-aligned tops), S1 embind mutator FIFO lane + parker wraps, S6
shutdown, libctx integration hooks (suspend/end/quarantine + g_current
arm/clear), SuspendError attributor, lost-wake + stuck-window watchdogs,
__wxWaitDump observability.
Embind: PARKER registrations get emscripten::async() under PCBJAM_JSPI
(wasm/bindings/pcbjam_async_policy.h). nanosleep yields route via the shim.
Tests: tests/asyncify -> tests/jspi successor suite (jspi-stack red/green
shadow-stack battery, jspi-coroutine MiniCoro harness, suspend-races
semantic scenarios + __wxWaitDump books coherence); projects jspi-firefox/
jspi-chrome (asyncify-webkit retired — no JSPI in WebKit); unconditional
Firefox JSPI pref; guard-beacons -> wait-beacons (+wxScheduler/libctxJspi
families); Makefile.wasm links test apps against JSPI with the shim as a
tracked link prerequisite.
Web: WasmTool setRo await + __wxWaitDump forensics, open-flow contained
promise, scheduler-shim.test.ts retargeted (8 green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDeBaKKhQztd8KiVtHuyXr
2026-08-13 07:06:24 +02:00
|
|
|
libctxJspi: /\[libctx-jspi\] ghost\/refused/,
|
2026-08-05 13:43:23 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const OCCURRENCE_RE = /\(occurrence (\d+)\)/;
|
|
|
|
|
const SAMPLE_LIMIT = 3;
|
|
|
|
|
|
|
|
|
|
function emptyFamily(): BeaconFamilyCount {
|
|
|
|
|
return { linesSeen: 0, estimatedTotal: 0, samples: [] };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function countGuardBeacons(consoleLines: string[]): GuardBeaconCounts {
|
|
|
|
|
const counts: GuardBeaconCounts = {
|
|
|
|
|
timerRetry: emptyFamily(),
|
|
|
|
|
dispatchAnomaly: emptyFamily(),
|
|
|
|
|
wxAsyncify: emptyFamily(),
|
|
|
|
|
libcontext: emptyFamily(),
|
|
|
|
|
openSettleFailed: emptyFamily(),
|
jspi: migration phases 0-7 — build knob, scheduler shim, test successor suite
Toolchain: emsdk 6.0.6 (versions.sh; cache-hash keys on it). Build knob
PCBJAM_ASYNC_BACKEND=jspi|asyncify: build-kicad-target.sh links editors with
-sJSPI + -sJSPI_EXPORTS=@scripts/common/jspi-exports.txt + --pre-js
jspi-scheduler.js (no DYNCALLS, no post-link asyncify pipeline); wx build
stamps the backend and forces clean on flip or unknown provenance;
docker/build.sh passes the knob, seeds the emscripten ports cache from the
volume every launch, jspi postprocess = patch-env-shim only.
scripts/common/shims/jspi-scheduler.js: the JSPI successor scheduler —
token-wait registry, resume turnstile (one armed resume between engine
re-entries, SP swaps only at microtask boundaries), green-region spill
stacks (16-aligned tops), S1 embind mutator FIFO lane + parker wraps, S6
shutdown, libctx integration hooks (suspend/end/quarantine + g_current
arm/clear), SuspendError attributor, lost-wake + stuck-window watchdogs,
__wxWaitDump observability.
Embind: PARKER registrations get emscripten::async() under PCBJAM_JSPI
(wasm/bindings/pcbjam_async_policy.h). nanosleep yields route via the shim.
Tests: tests/asyncify -> tests/jspi successor suite (jspi-stack red/green
shadow-stack battery, jspi-coroutine MiniCoro harness, suspend-races
semantic scenarios + __wxWaitDump books coherence); projects jspi-firefox/
jspi-chrome (asyncify-webkit retired — no JSPI in WebKit); unconditional
Firefox JSPI pref; guard-beacons -> wait-beacons (+wxScheduler/libctxJspi
families); Makefile.wasm links test apps against JSPI with the shim as a
tracked link prerequisite.
Web: WasmTool setRo await + __wxWaitDump forensics, open-flow contained
promise, scheduler-shim.test.ts retargeted (8 green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDeBaKKhQztd8KiVtHuyXr
2026-08-13 07:06:24 +02:00
|
|
|
wxScheduler: emptyFamily(),
|
|
|
|
|
libctxJspi: emptyFamily(),
|
2026-08-05 13:43:23 +02:00
|
|
|
schedulerBuild: false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (const line of consoleLines) {
|
|
|
|
|
if (line.includes('[wx-scheduler] scaffolding installed')) {
|
|
|
|
|
counts.schedulerBuild = true;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
for (const family of Object.keys(FAMILY_PATTERNS) as Array<
|
|
|
|
|
keyof typeof FAMILY_PATTERNS
|
|
|
|
|
>) {
|
|
|
|
|
if (!FAMILY_PATTERNS[family].test(line)) continue;
|
|
|
|
|
const fam = counts[family];
|
|
|
|
|
fam.linesSeen += 1;
|
|
|
|
|
const occ = OCCURRENCE_RE.exec(line);
|
|
|
|
|
const occurrenceTotal = occ ? parseInt(occ[1], 10) : fam.linesSeen;
|
|
|
|
|
fam.estimatedTotal = Math.max(fam.estimatedTotal, occurrenceTotal, fam.linesSeen);
|
|
|
|
|
if (fam.samples.length < SAMPLE_LIMIT) fam.samples.push(line);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return counts;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Last-seen fcsTotal/rootHotTotal from a __wxAsyncifyDump()/STATE line, if any.
|
|
|
|
|
// rootHotTotal must stay 0 post-v0.1.28 — the standing N8 assertion.
|
|
|
|
|
export function parseAsyncifyCounters(
|
|
|
|
|
consoleLines: string[]
|
|
|
|
|
): { fcsTotal: number; rootHotTotal: number } | null {
|
|
|
|
|
let result: { fcsTotal: number; rootHotTotal: number } | null = null;
|
|
|
|
|
for (const line of consoleLines) {
|
|
|
|
|
const m = /fcsTotal=(\d+) rootHotTotal=(\d+)/.exec(line);
|
|
|
|
|
if (m) result = { fcsTotal: parseInt(m[1], 10), rootHotTotal: parseInt(m[2], 10) };
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Assert the named guard families never fired. Throws with the offending sample
|
|
|
|
|
// lines so the log points straight at the collision the mailbox failed to absorb.
|
|
|
|
|
export function expectGuardsSilent(
|
|
|
|
|
consoleLines: string[],
|
|
|
|
|
families: Array<Exclude<keyof GuardBeaconCounts, 'schedulerBuild'>>
|
|
|
|
|
): void {
|
|
|
|
|
const counts = countGuardBeacons(consoleLines);
|
|
|
|
|
const noisy = families
|
|
|
|
|
.map((f) => ({ family: f, count: counts[f] }))
|
|
|
|
|
.filter(({ count }) => count.linesSeen > 0);
|
|
|
|
|
if (noisy.length > 0) {
|
|
|
|
|
const detail = noisy
|
|
|
|
|
.map(
|
|
|
|
|
({ family, count }) =>
|
|
|
|
|
`${family}: ${count.estimatedTotal} fire(s)\n ${count.samples.join('\n ')}`
|
|
|
|
|
)
|
|
|
|
|
.join('\n ');
|
|
|
|
|
throw new Error(`guard beacons fired (expected silent):\n ${detail}`);
|
|
|
|
|
}
|
|
|
|
|
}
|