pcbjam/wasm/bindings/open_gate.h
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

80 lines
3 KiB
C++

/*
* Truthful "kicadOpenFile in flight" signal for the web shell.
*
* kicadOpenFile runs OpenProjectFiles as a suspending (promising) export: the
* embind call hands JS a Promise long before the load finishes, and the chain
* stays suspended (and resumes, and suspends again) across the whole
* multi-second load. Any bare embind entry that walks the model while that
* chain is suspended mid-mutation (collab snapshot, presence bind) reads a
* half-built item graph and can trap — same class as the wx dispatch
* interlock and the collab apply-busy probe, but through a JS entry neither
* of those covers.
*
* The guard is RAII on the open's C++ stack frame: a suspension keeps the
* whole C++ stack (and with it this frame) alive, so the count is held for
* the suspension's entire lifetime and drops exactly when OpenProjectFiles
* truly returns (the same primitive as wxWasmDispatchGuard). A trap escaping
* the open leaves the count stuck — the JS poll times out and degrades.
*/
#pragma once
#include <wx/wasm/private/dispatch.h>
namespace pcbjam_open
{
inline int& busyCount()
{
static int s_count = 0;
return s_count;
}
/**
* Held for the whole open. Two counters, same suspension-RAII trick:
*
* - `busyCount` is OURS: it answers kicadOpenFileBusy() for the web shell and
* gates the collab entries (JS → embind reentry).
* - `wxWasmDispatchGuard` enrolls the open in the WX DISPATCH INTERLOCK. This
* matters because `kicadOpenFile` enters through embind, not through a wx
* dispatch entry point, so without it `wxWasmDispatchParked()` reads FALSE
* for the entire load: every suspension (progress pump, thread-pool futex
* wait, lib bridge) lets the pump dispatch a QUEUED WX TIMER into the
* half-built board — src/wasm/timer.cpp fires it because nothing looks
* parked — and the handler walks half-mutated widget/board state ("index
* out of bounds", the same signature as the symbol-chooser crash the
* interlock was built for). Holding the guard makes those timers defer
* (retry 17 ms later) until the load truly completes. Paints keep running;
* the progress dialog's own pump is the designed exception (it zeroes the
* count).
*/
struct BusyGuard
{
BusyGuard() { ++busyCount(); }
~BusyGuard() { --busyCount(); }
private:
wxWasmDispatchGuard m_dispatch;
};
/** JS-pollable: is a kicadOpenFile chain still in flight (possibly parked)? */
inline bool busy()
{
return busyCount() > 0;
}
/**
* Test-only deterministic park (tests/kicad/collab-load-fuzz.spec.ts): with a
* nonzero value, kicadOpenFile suspends for this many ms on entry and again
* after OpenProjectFiles returns — busy guard held, model fully loaded.
* Natural in-load suspensions (thread-pool futex waits) are
* scheduler-dependent and never happen on a fast idle machine, so the guard
* would be untestable in CI without this window. 0 (the default) is a no-op
* in production.
*/
inline int& testParkMs()
{
static int s_ms = 0;
return s_ms;
}
} // namespace pcbjam_open