2026-07-30 14:17:48 +02:00
|
|
|
import type { Page } from "@playwright/test";
|
|
|
|
|
import { test, expect } from "./fixtures";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Collab-entry-during-load gate test + fuzz (docs/features/async/14-open-settle-gate.md).
|
|
|
|
|
*
|
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
|
|
|
* The prod trap (an asyncify-era discovery; the surface is unchanged):
|
|
|
|
|
* `kicadOpenFile` suspends while `OpenProjectFiles` runs; on a slow machine
|
|
|
|
|
* the chain parks mid-load (thread-pool futex waits), and any bare embind
|
|
|
|
|
* entry that walks the model during such a park (the collab seed snapshot, an
|
|
|
|
|
* adopt apply) can virtual-dispatch through half-mutated state and trap with
|
|
|
|
|
* "indirect call signature mismatch". The fix is two-layered: the shell
|
|
|
|
|
* defers the attach on `kicadOpenFileBusy` (open-flow.ts), and the
|
2026-07-30 14:17:48 +02:00
|
|
|
* snapshot/apply entries themselves early-return while the open is in flight
|
|
|
|
|
* (open_gate.h guards).
|
|
|
|
|
*
|
|
|
|
|
* Natural in-load parks are scheduler-dependent — on a fast idle machine the
|
|
|
|
|
* whole open runs synchronously and NO window exists — so the deterministic
|
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
|
|
|
* test arms `kicadTestSetOpenPark`: kicadOpenFile then suspends for a fixed
|
|
|
|
|
* time on entry AND after OpenProjectFiles returns (model fully loaded, gate
|
|
|
|
|
* still closed). Hammering the entries inside that window asserts the
|
|
|
|
|
* contract sharply (docs/features/async/17 §3b):
|
2026-07-30 14:17:48 +02:00
|
|
|
* - `kicadOpenFileBusy()` reads true during the parks, false after;
|
|
|
|
|
* - mid-load snapshots return the EMPTY delta (an unguarded build would
|
|
|
|
|
* return the full board — deterministic red);
|
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
|
|
|
* - mid-load applies are QUEUED by the scheduler's embind lane and
|
|
|
|
|
* DELIVERED in order after settle (legacy glue DROPPED them; that drop
|
|
|
|
|
* contract retired with it);
|
2026-07-30 14:17:48 +02:00
|
|
|
* - after settle the entries work normally (guard released).
|
|
|
|
|
*
|
|
|
|
|
* The second test is the scheduler-dependent stress fuzz (spinning-worker CPU
|
|
|
|
|
* starvation to force real futex-wait parks, hammering throughout the load).
|
|
|
|
|
* It is skipped unless PCBJAM_FUZZ_STRESS=1: engagement of the window is not
|
|
|
|
|
* guaranteed on a fast machine, so it cannot gate CI — it exists to hunt this
|
|
|
|
|
* reentrancy class by hand (loop it on a loaded box).
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const SEG_TARGET = "fa220000-0000-0000-0000-00000000cafe"; // apply probe
|
|
|
|
|
|
|
|
|
|
/** Deterministic large board (~13k items) — a realistic snapshot/apply load. */
|
|
|
|
|
function bigBoard(): string {
|
|
|
|
|
const lines: string[] = [];
|
|
|
|
|
lines.push("(kicad_pcb");
|
|
|
|
|
lines.push("\t(version 20241229)");
|
|
|
|
|
lines.push('\t(generator "pcbnew")');
|
|
|
|
|
lines.push('\t(generator_version "9.0")');
|
|
|
|
|
lines.push("\t(general (thickness 1.6))");
|
|
|
|
|
lines.push('\t(paper "A4")');
|
|
|
|
|
lines.push("\t(layers");
|
|
|
|
|
lines.push('\t\t(0 "F.Cu" signal)');
|
|
|
|
|
lines.push('\t\t(2 "B.Cu" signal)');
|
|
|
|
|
lines.push('\t\t(37 "F.SilkS" user)');
|
|
|
|
|
lines.push('\t\t(25 "Edge.Cuts" user)');
|
|
|
|
|
lines.push("\t)");
|
|
|
|
|
lines.push("\t(setup)");
|
|
|
|
|
lines.push('\t(net 0 "")');
|
|
|
|
|
const NETS = 40;
|
|
|
|
|
for (let i = 1; i <= NETS; i++) lines.push(`\t(net ${i} "N${i}")`);
|
|
|
|
|
const uuid = (n: number) => `fa2${(n + 1).toString(16).padStart(5, "0")}-0000-0000-0000-000000000000`;
|
|
|
|
|
let n = 0;
|
|
|
|
|
for (let i = 0; i < 12000; i++) {
|
|
|
|
|
const x = 20 + (i % 120) * 1.5;
|
|
|
|
|
const y = 20 + Math.floor(i / 120) * 1;
|
|
|
|
|
lines.push(
|
|
|
|
|
`\t(segment (start ${x} ${y}) (end ${x + 1.2} ${y}) (width 0.2) (layer "F.Cu") (net ${
|
|
|
|
|
(i % NETS) + 1
|
|
|
|
|
}) (uuid "${uuid(n++)}"))`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
for (let i = 0; i < 800; i++) {
|
|
|
|
|
const x = 21 + (i % 80) * 2;
|
|
|
|
|
const y = 21 + Math.floor(i / 80) * 10;
|
|
|
|
|
lines.push(
|
|
|
|
|
`\t(via (at ${x} ${y}) (size 1.4) (drill 0.6) (layers "F.Cu" "B.Cu") (net ${
|
|
|
|
|
(i % NETS) + 1
|
|
|
|
|
}) (uuid "${uuid(n++)}"))`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
// Footprints with text children (the field/text walk of the snapshot).
|
|
|
|
|
for (let i = 0; i < 200; i++) {
|
|
|
|
|
const x = 30 + (i % 20) * 8;
|
|
|
|
|
const y = 140 + Math.floor(i / 20) * 6;
|
|
|
|
|
lines.push(`\t(footprint "TestLib:R"
|
|
|
|
|
\t\t(layer "F.Cu")
|
|
|
|
|
\t\t(uuid "${uuid(n++)}")
|
|
|
|
|
\t\t(at ${x} ${y})
|
|
|
|
|
\t\t(attr smd)
|
|
|
|
|
\t\t(property "Reference" "R${i}"
|
|
|
|
|
\t\t\t(at 0 -2 0)
|
|
|
|
|
\t\t\t(layer "F.SilkS")
|
|
|
|
|
\t\t\t(uuid "${uuid(n++)}")
|
|
|
|
|
\t\t\t(effects (font (size 1 1) (thickness 0.15)))
|
|
|
|
|
\t\t)
|
|
|
|
|
\t\t(property "Value" "R"
|
|
|
|
|
\t\t\t(at 0 2 0)
|
|
|
|
|
\t\t\t(layer "F.Fab")
|
|
|
|
|
\t\t\t(uuid "${uuid(n++)}")
|
|
|
|
|
\t\t\t(effects (font (size 1 1) (thickness 0.15)))
|
|
|
|
|
\t\t)
|
|
|
|
|
\t)`);
|
|
|
|
|
}
|
|
|
|
|
lines.push(
|
|
|
|
|
`\t(segment (start 10 10) (end 15 10) (width 0.2) (layer "F.Cu") (net 0) (uuid "${SEG_TARGET}"))`,
|
|
|
|
|
);
|
|
|
|
|
lines.push(")");
|
|
|
|
|
return lines.join("\n");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type FS = { mkdirTree(p: string): void; writeFile(p: string, d: string): void };
|
|
|
|
|
type Mod = {
|
|
|
|
|
kicadOpenFile(p: string): unknown;
|
|
|
|
|
kicadOpenFileBusy(): boolean;
|
|
|
|
|
kicadTestSetOpenPark(ms: number): void;
|
|
|
|
|
kicadCollabSnapshot(): string;
|
|
|
|
|
kicadCollabSnapshotItems(): string;
|
|
|
|
|
kicadCollabApply(j: string): unknown;
|
|
|
|
|
kicadCollabApplyItems(j: string): unknown;
|
|
|
|
|
kicadCollabGetPos(id: string): string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
interface FuzzStats {
|
|
|
|
|
busySamples: number;
|
|
|
|
|
iterations: number;
|
|
|
|
|
errors: string[];
|
|
|
|
|
/** Largest `added` length any mid-load snapshot returned (guard ⇒ 0). */
|
|
|
|
|
maxBusySnapshotItems: number;
|
|
|
|
|
settled: boolean;
|
|
|
|
|
loadMs: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hasAbort(l: { consoleLogs: string[]; errors: string[] }): boolean {
|
|
|
|
|
return [...l.consoleLogs, ...l.errors].some((s) => s.includes("Aborted("));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function bootHarness(page: Page): Promise<void> {
|
|
|
|
|
await page.goto("/kicad/pcbnew-collab.html");
|
|
|
|
|
await expect(page.locator("#canvas")).toBeVisible({ timeout: 90000 });
|
|
|
|
|
await page.waitForFunction(() => !!window.wxElementRegistry, null, { timeout: 90000 });
|
|
|
|
|
await page.waitForFunction(
|
|
|
|
|
() => {
|
|
|
|
|
const m = (window as unknown as { Module?: Partial<Mod> }).Module;
|
|
|
|
|
return (
|
|
|
|
|
typeof m?.kicadOpenFile === "function" &&
|
|
|
|
|
typeof m?.kicadCollabSnapshotItems === "function" &&
|
|
|
|
|
typeof m?.kicadCollabApply === "function"
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
null,
|
|
|
|
|
{ timeout: 90000 },
|
|
|
|
|
);
|
|
|
|
|
await page.waitForFunction(
|
|
|
|
|
() =>
|
|
|
|
|
!!window.wxElementRegistry &&
|
|
|
|
|
window.wxElementRegistry
|
|
|
|
|
.findAll({ visible: true })
|
|
|
|
|
.some((e) => /Frame$/.test(e.typeName) || (e.name || "").endsWith("Frame")),
|
|
|
|
|
null,
|
|
|
|
|
{ timeout: 90000 },
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* In-page: write the board, open it, and hammer every collab entry for as long
|
|
|
|
|
* as `kicadOpenFileBusy()` reports the open in flight. Mid-load applies try to
|
|
|
|
|
* MOVE the probe segment — the guard must drop them (asserted by the caller via
|
|
|
|
|
* the probe's position). `starve` additionally saturates every core with
|
|
|
|
|
* spinning workers so natural thread-pool waits park too (stress mode).
|
|
|
|
|
*/
|
|
|
|
|
async function openAndHammer(
|
|
|
|
|
page: Page,
|
|
|
|
|
opts: { content: string; probeUuid: string; parkMs: number; starve: boolean },
|
|
|
|
|
): Promise<FuzzStats> {
|
|
|
|
|
return page.evaluate(async ({ content, probeUuid, parkMs, starve }) => {
|
|
|
|
|
const w = window as unknown as {
|
|
|
|
|
FS: FS;
|
|
|
|
|
Module: Mod & { kicadTestSetOpenPark?: (ms: number) => void };
|
|
|
|
|
};
|
|
|
|
|
const dir = "/home/kicad/documents";
|
|
|
|
|
try {
|
|
|
|
|
w.FS.mkdirTree(dir);
|
|
|
|
|
} catch {
|
|
|
|
|
/* exists */
|
|
|
|
|
}
|
|
|
|
|
const path = `${dir}/fuzz.kicad_pcb`;
|
|
|
|
|
w.FS.writeFile(path, content);
|
|
|
|
|
if (parkMs > 0) w.Module.kicadTestSetOpenPark!(parkMs);
|
|
|
|
|
|
|
|
|
|
// Mid-load applies try to move the probe AWAY from home; both wire families.
|
|
|
|
|
const wireDelta = JSON.stringify({
|
|
|
|
|
added: [],
|
|
|
|
|
changed: [
|
|
|
|
|
{
|
|
|
|
|
sexpr: `(segment (start 55 55) (end 60 55) (width 0.2) (layer "F.Cu") (net 0) (uuid "${probeUuid}"))`,
|
|
|
|
|
parent: null,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
removed: [],
|
|
|
|
|
});
|
|
|
|
|
const scalarDelta = JSON.stringify({
|
|
|
|
|
added: [],
|
|
|
|
|
changed: [
|
|
|
|
|
{
|
|
|
|
|
id: probeUuid,
|
|
|
|
|
type: "PCB_TRACK",
|
|
|
|
|
sx: 55_000_000,
|
|
|
|
|
sy: 55_000_000,
|
|
|
|
|
ex: 60_000_000,
|
|
|
|
|
ey: 55_000_000,
|
|
|
|
|
width: 200000,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
removed: [],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const burners: Worker[] = [];
|
|
|
|
|
let burnUrl = "";
|
|
|
|
|
if (starve) {
|
|
|
|
|
burnUrl = URL.createObjectURL(
|
|
|
|
|
new Blob(["for(;;){let x=0;for(let i=0;i<1e7;i++)x+=i;}"], {
|
|
|
|
|
type: "text/javascript",
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
const cores = navigator.hardwareConcurrency || 8;
|
|
|
|
|
for (let i = 0; i < cores * 2; i++) burners.push(new Worker(burnUrl));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
w.Module.kicadOpenFile(path);
|
|
|
|
|
|
|
|
|
|
const t0 = performance.now();
|
|
|
|
|
let busySamples = 0;
|
|
|
|
|
let iterations = 0;
|
|
|
|
|
let maxBusySnapshotItems = 0;
|
|
|
|
|
const errors: string[] = [];
|
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
|
|
|
// The scheduler QUEUES busy-window entries for post-settle delivery
|
2026-08-05 15:01:52 +02:00
|
|
|
// (doc 17 §3b) — an unbounded hammer would replay hundreds of heavy
|
|
|
|
|
// applies/snapshots afterwards (each Push walks connectivity across the
|
|
|
|
|
// fixture's 800 vias; on a debug build every via prints an assert — a
|
|
|
|
|
// 170k-line console flood that drowns the drain). The deterministic
|
|
|
|
|
// contract needs delivery + order, not volume: cap the queued calls and
|
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
|
|
|
// keep observing the busy window. Volume lives in the STRESS test.
|
|
|
|
|
const maxEntryIters = 6;
|
|
|
|
|
// Every suspension of the open chain hands the event loop to this
|
2026-07-30 14:17:48 +02:00
|
|
|
// timer — exactly how the prod shell's collab attach interleaved.
|
|
|
|
|
while (performance.now() - t0 < 120000) {
|
|
|
|
|
if (!w.Module.kicadOpenFileBusy()) break;
|
|
|
|
|
busySamples++;
|
|
|
|
|
iterations++;
|
2026-08-05 15:01:52 +02:00
|
|
|
if (iterations > maxEntryIters) {
|
|
|
|
|
await new Promise((r) => setTimeout(r, 10));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-07-30 14:17:48 +02:00
|
|
|
for (const [name, fn] of [
|
|
|
|
|
["snapshotItems", () => w.Module.kicadCollabSnapshotItems()],
|
|
|
|
|
["snapshot", () => w.Module.kicadCollabSnapshot()],
|
|
|
|
|
["applyItems", () => w.Module.kicadCollabApplyItems(wireDelta)],
|
|
|
|
|
["apply", () => w.Module.kicadCollabApply(scalarDelta)],
|
|
|
|
|
] as const) {
|
|
|
|
|
try {
|
|
|
|
|
const out = fn();
|
|
|
|
|
if (typeof out === "string" && name.startsWith("snapshot")) {
|
|
|
|
|
const added = (JSON.parse(out) as { added: unknown[] }).added.length;
|
|
|
|
|
if (added > maxBusySnapshotItems) maxBusySnapshotItems = added;
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
errors.push(`${name} during load: ${String(e)}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
await new Promise((r) => setTimeout(r, 10));
|
|
|
|
|
}
|
|
|
|
|
for (const b of burners) b.terminate();
|
|
|
|
|
if (burnUrl) URL.revokeObjectURL(burnUrl);
|
|
|
|
|
if (parkMs > 0) w.Module.kicadTestSetOpenPark!(0);
|
|
|
|
|
return {
|
|
|
|
|
busySamples,
|
|
|
|
|
iterations,
|
|
|
|
|
errors,
|
|
|
|
|
maxBusySnapshotItems,
|
|
|
|
|
settled: !w.Module.kicadOpenFileBusy(),
|
|
|
|
|
loadMs: Math.round(performance.now() - t0),
|
|
|
|
|
};
|
|
|
|
|
}, opts);
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
/** Post-settle asserts shared by both tests: queued applies delivered + gate released. */
|
2026-07-30 14:17:48 +02:00
|
|
|
async function assertSettledContract(page: Page, stats: FuzzStats): Promise<void> {
|
|
|
|
|
expect(stats.settled, "kicadOpenFileBusy cleared after the load").toBe(true);
|
|
|
|
|
expect(stats.errors, "no traps while hammering entries mid-load").toEqual([]);
|
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
|
|
|
// Guard held: no mid-load snapshot ever saw the model. A busy-window
|
|
|
|
|
// snapshot returns a Promise (typeof !== "string" — the hammer skips it),
|
|
|
|
|
// so a nonzero count here means a synchronous walk leaked through the gate.
|
2026-07-30 14:17:48 +02:00
|
|
|
expect(stats.maxBusySnapshotItems, "mid-load snapshots returned the empty delta").toBe(0);
|
|
|
|
|
await expect.poll(() => page.title(), { timeout: 30000 }).toMatch(/fuzz/i);
|
|
|
|
|
|
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
|
|
|
// Delivery contract (docs/features/async/17 §3b): the embind lane QUEUED
|
|
|
|
|
// the mid-load applies and delivers them after settle, in order — the probe
|
|
|
|
|
// sits where the hammer's deltas moved it. (Legacy glue DROPPED them and
|
|
|
|
|
// the probe stayed home; that drop contract retired with the flip.)
|
|
|
|
|
//
|
|
|
|
|
// The hammer queued hundreds of calls (each mid-load snapshot delivers as
|
|
|
|
|
// a FULL board walk now, not the gate's empty delta) — wait for the
|
|
|
|
|
// time-boxed pump to drain the backlog before asserting final state.
|
|
|
|
|
await expect
|
|
|
|
|
.poll(
|
|
|
|
|
() =>
|
|
|
|
|
page.evaluate(
|
|
|
|
|
() =>
|
|
|
|
|
(globalThis as unknown as { __wxScheduler: { mutatorQueue: unknown[] } })
|
|
|
|
|
.__wxScheduler.mutatorQueue.length,
|
|
|
|
|
),
|
|
|
|
|
{ timeout: 240000, intervals: [1000] },
|
|
|
|
|
)
|
|
|
|
|
.toBe(0);
|
2026-08-05 15:01:52 +02:00
|
|
|
const HAMMER_TARGET = "55000000,55000000"; // both hammer deltas move the probe here
|
2026-07-30 14:17:48 +02:00
|
|
|
await expect
|
|
|
|
|
.poll(
|
|
|
|
|
() =>
|
|
|
|
|
page.evaluate((id) => (window.Module as unknown as Mod).kicadCollabGetPos(id), SEG_TARGET),
|
|
|
|
|
{ timeout: 10000, intervals: [200] },
|
|
|
|
|
)
|
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
|
|
|
.toBe(HAMMER_TARGET);
|
2026-07-30 14:17:48 +02:00
|
|
|
|
|
|
|
|
// Guard released: the snapshot now walks the real, fully-loaded board…
|
|
|
|
|
const itemCount = await page.evaluate(
|
|
|
|
|
() => JSON.parse((window.Module as unknown as Mod).kicadCollabSnapshotItems()).added.length,
|
|
|
|
|
);
|
|
|
|
|
expect(itemCount, "post-load snapshot sees the board").toBeGreaterThan(12000);
|
|
|
|
|
|
|
|
|
|
// …and a real apply lands.
|
|
|
|
|
await page.evaluate(
|
|
|
|
|
(id) =>
|
|
|
|
|
(window.Module as unknown as Mod).kicadCollabApply(
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
added: [],
|
|
|
|
|
changed: [
|
|
|
|
|
{
|
|
|
|
|
id,
|
|
|
|
|
type: "PCB_TRACK",
|
|
|
|
|
sx: 12_000_000,
|
|
|
|
|
sy: 34_000_000,
|
|
|
|
|
ex: 17_000_000,
|
|
|
|
|
ey: 34_000_000,
|
|
|
|
|
width: 200000,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
removed: [],
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
SEG_TARGET,
|
|
|
|
|
);
|
|
|
|
|
await expect
|
|
|
|
|
.poll(
|
|
|
|
|
() =>
|
|
|
|
|
page.evaluate((id) => (window.Module as unknown as Mod).kicadCollabGetPos(id), SEG_TARGET),
|
|
|
|
|
{ timeout: 10000, intervals: [200] },
|
|
|
|
|
)
|
|
|
|
|
.toBe("12000000,34000000");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test.describe("collab entries during a parked board load (open_gate)", () => {
|
|
|
|
|
test("deterministic park window: gate engages, entries no-op, gate releases", async ({
|
|
|
|
|
page,
|
|
|
|
|
testLogger,
|
|
|
|
|
}) => {
|
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
|
|
|
// The post-settle pump replays the whole hammer backlog (the drain-wait
|
|
|
|
|
// in assertSettledContract) — budget for it on top of the load.
|
2026-08-05 15:01:52 +02:00
|
|
|
test.setTimeout(420000);
|
2026-07-30 14:17:48 +02:00
|
|
|
await bootHarness(page);
|
|
|
|
|
|
|
|
|
|
const hooks = await page.evaluate(() => {
|
|
|
|
|
const m = window.Module as unknown as Partial<Mod>;
|
|
|
|
|
return {
|
|
|
|
|
busy: typeof m.kicadOpenFileBusy === "function",
|
|
|
|
|
park: typeof m.kicadTestSetOpenPark === "function",
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
expect(hooks.busy, "kicadOpenFileBusy export present (open_gate.h)").toBe(true);
|
|
|
|
|
expect(hooks.park, "kicadTestSetOpenPark export present (open_gate.h)").toBe(true);
|
|
|
|
|
|
|
|
|
|
const stats = await openAndHammer(page, {
|
|
|
|
|
content: bigBoard(),
|
|
|
|
|
probeUuid: SEG_TARGET,
|
|
|
|
|
parkMs: 1500, // entry + post-load parks — a guaranteed hammer window
|
|
|
|
|
starve: false,
|
|
|
|
|
});
|
|
|
|
|
console.log(
|
|
|
|
|
`[TEST] gate: ${stats.iterations} iterations over ${stats.loadMs}ms, ` +
|
|
|
|
|
`${stats.busySamples} busy samples, maxBusySnap=${stats.maxBusySnapshotItems}, ` +
|
|
|
|
|
`${stats.errors.length} errors`,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// The armed parks make the window unconditional on any machine.
|
|
|
|
|
expect(stats.busySamples, "the busy window was observed").toBeGreaterThan(0);
|
|
|
|
|
await assertSettledContract(page, stats);
|
|
|
|
|
expect(hasAbort(testLogger), "no WASM abort").toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Scheduler-dependent stress hunt — NOT a CI gate: window engagement is not
|
|
|
|
|
// guaranteed on a fast idle machine (see header). Loop it manually:
|
|
|
|
|
// PCBJAM_FUZZ_STRESS=1 npx playwright test --project=kicad-firefox kicad/collab-load-fuzz.spec.ts
|
|
|
|
|
test("stress: hammer through natural thread-wait parks under CPU starvation", async ({
|
|
|
|
|
page,
|
|
|
|
|
testLogger,
|
|
|
|
|
}) => {
|
|
|
|
|
test.skip(!process.env.PCBJAM_FUZZ_STRESS, "manual stress hunt (PCBJAM_FUZZ_STRESS=1)");
|
|
|
|
|
test.setTimeout(300000);
|
|
|
|
|
await bootHarness(page);
|
|
|
|
|
|
|
|
|
|
const stats = await openAndHammer(page, {
|
|
|
|
|
content: bigBoard(),
|
|
|
|
|
probeUuid: SEG_TARGET,
|
|
|
|
|
parkMs: 0, // natural parks only
|
|
|
|
|
starve: true,
|
|
|
|
|
});
|
|
|
|
|
console.log(
|
|
|
|
|
`[TEST] stress: ${stats.iterations} iterations over ${stats.loadMs}ms, ` +
|
|
|
|
|
`${stats.busySamples} busy samples, maxBusySnap=${stats.maxBusySnapshotItems}, ` +
|
|
|
|
|
`${stats.errors.length} errors`,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// No busySamples assert: with no natural park the window legitimately
|
|
|
|
|
// never opens. Everything that DID interleave must have been safe.
|
|
|
|
|
await assertSettledContract(page, stats);
|
|
|
|
|
expect(hasAbort(testLogger), "no WASM abort").toBe(false);
|
|
|
|
|
});
|
|
|
|
|
});
|