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
This commit is contained in:
Viktor Vaczi 2026-08-14 09:25:32 +02:00
commit 9c475a804e
120 changed files with 1522 additions and 2974 deletions

View file

@ -11,7 +11,7 @@ This document describes how to build KiCad for WebAssembly using the Docker-base
### Host Tools
Binaryen (wasm-opt) is downloaded automatically by the build script. No manual installation needed.
Node.js (for the seconds-long host postprocess step). Everything else runs inside the container.
## Quick Start
@ -36,47 +36,30 @@ Binaryen (wasm-opt) is downloaded automatically by the build script. No manual i
- `build-wasm/kicad-pcbnew/pcbnew/pcbnew.wasm` - WASM binary
- `build-wasm/kicad-pcbnew/pcbnew/pcbnew.wasm.map` - Source map (debug builds)
## Two-Phase Build
## Single-Phase Build
The build is split into two phases due to memory requirements:
The build is one pass: `docker/build.sh` compiles, links **and finalizes** the
wasm inside the container. The only host-side step is a postprocess on the
generated glue — `node scripts/common/patch-env-shim.mjs` merges `Module.ENV`
into the runtime's `ENV` (needed for `?trace=` and any future `Module.ENV`
use). It takes seconds and is idempotent.
### Phase 1: Docker Compilation
Compiles KiCad to WASM **without** asyncify transformation. This runs inside Docker with 32GB memory limit.
`--compile-only` / `--postprocess-only` split the two so CI can cache the
expensive compile and re-run just the host tail.
### Phase 2: Host Asyncify
Applies `wasm-opt --asyncify` on the host machine using Binaryen v121 (downloaded automatically to `tools/`). This transformation uses ~20-30GB RAM.
### Suspension: JSPI
**Note:** Binaryen v121 is used because v125 has a regression causing crashes in the asyncify liveness analysis.
Blocking calls — `wxDialog::ShowModal()`, `wxMessageBox()`, clipboard,
sleeps/waits, board loads — must yield to the browser event loop. This is
handled at **link time** by JSPI (JavaScript Promise Integration): every wasm
entry point that can suspend is a promising export
(`-sJSPI -sJSPI_EXPORTS=@scripts/common/jspi-exports.txt`), and the
`jspi-scheduler.js` pre-js supplies the spill-stack and resume-serialization
discipline around it. See
[docs/features/async/23-jspi-runtime.md](features/async/23-jspi-runtime.md).
### Why Asyncify?
Asyncify is an Emscripten transformation that allows WASM code to pause and resume execution. This is required for:
- **Modal dialogs** - `wxDialog::ShowModal()` blocks until user closes the dialog
- **Message boxes** - `wxMessageBox()` waits for user response
- **Clipboard operations** - Browser clipboard API is async
- **Sleep/wait operations** - Any blocking call that needs to yield to the browser
Without asyncify, modal dialogs would freeze the browser because WASM cannot yield control back to JavaScript's event loop.
### How It Works
1. `docker/build.sh` compiles KiCad in Docker (no asyncify flags)
2. Output is copied to `./output/` directory
3. `wasm-opt --asyncify` runs on host, transforming the WASM binary
4. Final output is ready for browser execution
### Technical Details
The asyncify transformation:
- Instruments every function that might be on the call stack during an async operation
- Adds stack save/restore logic to unwind and rewind the WASM stack
- Increases binary size by ~20% (141MB → 171MB for KiCad)
- Uses `asyncify-imports` pattern matching to identify async entry points
Import patterns used:
- `env.invoke_*` - Exception handling trampolines
- `env.__asyncjs__*` - EM_ASYNC_JS functions (like `startModal()`)
There is no post-link binary rewriting: the wasm the container links is the
wasm that ships.
## Docker Architecture
@ -155,17 +138,18 @@ The build system is optimized for fast development iteration:
- **ccache**: Caches compiled objects by hashing preprocessed source
- **wxWidgets**: `configure` runs once, `make` handles file-level dependencies
- **KiCad**: CMake tracks dependencies, only recompiles changed files
- **Asyncify**: Post-processing runs every build (~1 min, irreducible minimum)
- **Host postprocess**: the ENV-shim patch (`patch-env-shim.mjs`) re-runs every build — seconds
### Performance
| Scenario | Time |
|----------|------|
| No changes | ~1.5 min |
| Single file change (KiCad or wxWidgets) | ~1.5 min |
| No changes | seconds |
| Single file change (KiCad or wxWidgets) | dominated by the recompile + relink of that target |
| Full rebuild | ~10 min |
Most time is spent on asyncify post-processing which runs on every build.
There is no fixed per-build post-processing cost: an unchanged tree re-runs
only the host ENV-shim patch.
### Debug vs Release
@ -259,24 +243,30 @@ The WASM port requires compatibility layers for browser execution:
| Directory | Purpose |
|-----------|---------|
| `wasm/kiplatform/` | Platform abstraction (app, UI, printing, etc.) |
| `wasm/libcontext/` | Coroutine/fiber implementation for Asyncify |
| `kicad/thirdparty/libcontext/` | Coroutine backend (JSPI: one promising activation per coroutine) |
| `wasm/stubs/` | Stub implementations (libgit2, curl) |
| `wasm/config/` | Build configuration headers |
## Emscripten Flags
Key flags used in the build:
Key flags used in the build (browser apps; see
`scripts/kicad/build-kicad-target.sh` for the authoritative link surface):
```
-pthread -sUSE_PTHREADS=1 # Threading support
-sASYNCIFY=1 # Async coroutine support
-sALLOW_MEMORY_GROWTH=1 # Dynamic memory
-sINITIAL_MEMORY=256MB # Starting memory
-sMAXIMUM_MEMORY=4GB # Maximum memory
-sLEGACY_GL_EMULATION # OpenGL compatibility
-sMAX_WEBGL_VERSION=2 # WebGL 2.0
-pthread -sUSE_PTHREADS=1 # Threading support
-sJSPI # JSPI suspension
-sJSPI_EXPORTS=@scripts/common/jspi-exports.txt # promising-export census
--pre-js scripts/common/shims/jspi-scheduler.js # scheduler/turnstile shim
-sALLOW_MEMORY_GROWTH=1 # Dynamic memory
-sINITIAL_MEMORY=256MB # Starting memory
-sMAXIMUM_MEMORY=4GB # Maximum memory
-sMAX_WEBGL_VERSION=2 # WebGL 2.0
```
Headless targets (`kicad_tools`, `occ_service`) link **no suspension
backend**: nothing in them may suspend, so they carry none of the three
JSPI-related flags above.
## Testing
After building, run the test suite: