pcbjam/docs/features/wasm-exceptions/README.md

112 lines
8.5 KiB
Markdown
Raw Normal View History

# `-fexceptions` vs `-fwasm-exceptions` in KiCad-WASM — research dossier
feat(wasm-eh): migrate the WASM build to native wasm exceptions (+ 3D viewer default-on) Replace the legacy Emscripten JS-exceptions model with native wasm-EH (legacy encoding) across the whole build, keeping Asyncify coroutines working via a from-source Binaryen --hoist-cpp-catches pre-pass. Net result: native-EH is the only build mode, the 3D viewer is on by default, and pcbnew shrinks substantially. Highlights: - Binaryen submodule everywhere + --hoist-cpp-catches integration in apply-asyncify; post-link Asyncify covers every app wasm (not just standalone test wasm). - Build deps (incl. OpenCASCADE without OCC_CONVERT_SIGNALS) and all KiCad apps with -fwasm-exceptions; emscripten_sleep added to the post-link asyncify-imports. - libcontext fiber entry wired under native exceptions; while-loop main loop + currData shim injected into all wx apps. - Native-EH collab apply fixed: DEBUG-define the embind TU + match all out-of-CMake C++ TUs' ABI flags to the core, fixing the vtable-layout skew / mis-dispatch. - 3D viewer enabled by default (real raytracer linked, not the stub). - Retire the EH-spike scaffolding; flip the asyncify-races ablation pins to shim-redundancy pins (native-EH stays clean with the legacy shims ablated). - Fix the asyncify-races quiescence check to not require Asyncify.currData==0: under the native-EH per-frame-yield top loop the main stack is asyncify-suspended every frame, so currData legitimately churns (a freed-but-not-yet-nulled buffer, not a leak). Refresh the pcbnew toolbar screenshot baseline for the new kicad. - CI: drop the obsolete binaryen_version input/env (the build uses the binaryen submodule fork's wasm-opt, not a version download); key the wasm-output cache on the binaryen submodule SHA instead. Bumps the wxwidgets + binaryen submodules to their squashed feature commits. Validated green: all 7 apps native-EH (real 3D in pcbnew); KiCad e2e 63/63 Firefox + Chromium (3D viewer renders); wx 336; coroutine 34/34 both engines; asyncify 7/7 both engines. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 19:50:18 +02:00
> **✅ FINALIZED:** native wasm-EH is now the **only** build mode — there is no `-fexceptions` /
> `WX_LEGACY_EH` path, and the 3D viewer builds by default. The migration plan, audit, and spike
> notes below are retained as the historical research/decision record.
> **Status:** research / decision record. A parallel session attempted the migration
> end-to-end and **parked it** on an emscripten-4.0.2 LLVM codegen bug — see
> [`docs/wasm-exceptions-experiment.md`](../../wasm-exceptions-experiment.md) (full
> build-plumbing patch preserved in its appendix; flags
> `-fwasm-exceptions -sSUPPORT_LONGJMP=wasm -sWASM_LEGACY_EXCEPTIONS=0`, raw linked
> pcbnew 92 MB and zero `invoke_*`/`dynCall`). This dossier is the research companion:
> mechanism, measurements, audit, and the asyncify catch-block design.
> Authored 2026-06-11/12. Companion to [`docs/features/async/`](../async/) (the Asyncify
> `currData` contention dossier) — this dossier covers the *exception-handling* axis of
> the same machine.
feat(wasm-eh): migrate the WASM build to native wasm exceptions (+ 3D viewer default-on) Replace the legacy Emscripten JS-exceptions model with native wasm-EH (legacy encoding) across the whole build, keeping Asyncify coroutines working via a from-source Binaryen --hoist-cpp-catches pre-pass. Net result: native-EH is the only build mode, the 3D viewer is on by default, and pcbnew shrinks substantially. Highlights: - Binaryen submodule everywhere + --hoist-cpp-catches integration in apply-asyncify; post-link Asyncify covers every app wasm (not just standalone test wasm). - Build deps (incl. OpenCASCADE without OCC_CONVERT_SIGNALS) and all KiCad apps with -fwasm-exceptions; emscripten_sleep added to the post-link asyncify-imports. - libcontext fiber entry wired under native exceptions; while-loop main loop + currData shim injected into all wx apps. - Native-EH collab apply fixed: DEBUG-define the embind TU + match all out-of-CMake C++ TUs' ABI flags to the core, fixing the vtable-layout skew / mis-dispatch. - 3D viewer enabled by default (real raytracer linked, not the stub). - Retire the EH-spike scaffolding; flip the asyncify-races ablation pins to shim-redundancy pins (native-EH stays clean with the legacy shims ablated). - Fix the asyncify-races quiescence check to not require Asyncify.currData==0: under the native-EH per-frame-yield top loop the main stack is asyncify-suspended every frame, so currData legitimately churns (a freed-but-not-yet-nulled buffer, not a leak). Refresh the pcbnew toolbar screenshot baseline for the new kicad. - CI: drop the obsolete binaryen_version input/env (the build uses the binaryen submodule fork's wasm-opt, not a version download); key the wasm-output cache on the binaryen submodule SHA instead. Bumps the wxwidgets + binaryen submodules to their squashed feature commits. Validated green: all 7 apps native-EH (real 3D in pcbnew); KiCad e2e 63/63 Firefox + Chromium (3D viewer renders); wx 336; coroutine 34/34 both engines; asyncify 7/7 both engines. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 19:50:18 +02:00
> **UPDATE 2026-06-22 (see [`06-spike-plan.md`](06-spike-plan.md)).** A 5-agent spike refreshed
> this dossier and corrected three things below: (1) **the encoding is resolved to LEGACY**
> (`WASM_LEGACY_EXCEPTIONS=1`) — Asyncify can't consume exnref in any released Binaryen, so the
> "exnref → TryTable variant" fork is closed; the experiment's `=0` was a dead end. (2) **Binaryen
> is not a blocker** — CI/publish already pin `BINARYEN_VERSION=130` (the "v121 locally" note below
> is only the finalize/in-link copy). (3) The long pole is the **emsdk/LLVM compiler bump** for
> parseable legacy wasm-EH + the OCC `br_table` fix, *not* a newer wasm-opt. The phased red-green
> plan lives in 06.
## Why this exists
The whole build is on **`-fexceptions`** (Emscripten's JavaScript-based exception
handling). That choice is not cosmetic — it is the single largest driver of our Asyncify
cost: it forces `env.invoke_*` into `ASYNCIFY_IMPORTS`, which makes nearly the whole call
graph "suspension-capable" and therefore instrumented. We **measured** the consequence on
our own binary: 59% of the raw asyncify tax (64% of the gzipped tax) on pcbnew exists only
because of the invoke machinery. Migrating to native **`-fwasm-exceptions`** would cut the
shipped pcbnew download from **64.5 MB to ~36 MB (44%)** and the module from
**187 MB to ~122 MB (35%)**, plus an unmeasured-but-real runtime win on every try-region
hot path.
The migration is currently blocked by one upstream limitation — Binaryen's Asyncify pass
cannot handle a suspension *inside a catch handler* — and KiCad triggers exactly that
pattern (modal error dialogs from catch blocks) in **85 audited places**. This dossier
records the mechanism, the measurements, the toolchain status, the audit, and a concrete
**fork design (catch-arm hoisting)** that would remove the blocker without refactoring
KiCad at all.
## TL;DR / decision
- **Stay on `-fexceptions` for now.** Asyncify stays under any design (fibers + EM_ASYNC_JS
have no wasm-EH replacement); this is purely about how much it must instrument.
- The prize is measured, not estimated: **44% download, 35% module size** (see 02).
- Binaryen merged *partial* asyncify+wasm-EH support in **v125** (2025-11-19). Our emsdk
bundles **v121** — we don't even have the partial support locally.
- The remaining hole — unwind-from-catch — is fixable with a **bounded new Binaryen pass**
(catch-arm hoisting, ~400800 lines, 12 weeks, genuinely upstreamable; see 05). It is a
*pre-pass* before stock `--asyncify``Asyncify.cpp` itself needs zero changes, so
"fork" overstates it (one added file; `get-wasm-opt.sh` already has the
build-from-source deployment path). It **obsoletes the 85-site KiCad refactor** entirely.
- **Blocker ordering (learned from the parked experiment):** the catch-block limitation
is blocker #2. Blocker #1 is an **LLVM codegen bug in emscripten 4.0.2** (invalid
`br_table` arity in OpenCASCADE code under wasm-EH) — needs an emsdk bump first. And the
experiment had to force the **exnref encoding** (`WASM_LEGACY_EXCEPTIONS=0`, because
4.0.2's legacy encoding output doesn't even parse in Binaryen), which collides with the
fact that Binaryen's asyncify has **zero `TryTable` support**: the encoding choice after
the emsdk bump decides which variant of the catch fix applies (see 03 §experiment, 05).
- Trigger to act: when we are ready to invest ~2 weeks of toolchain work, or if upstream
lands full support on binaryen #4470 first. Until then the Asyncify arbiter work
(docs/features/async/) is the priority — it fixes shipping bugs and is needed either way.
## Document index
| File | Contents |
|---|---|
| [`01-background-two-eh-models.md`](01-background-two-eh-models.md) | How JS-EH (`invoke_*`) and wasm-EH actually work, and the three concrete couplings into our Asyncify machine. |
| [`02-measurements.md`](02-measurements.md) | Our controlled size experiment on pcbnew (methodology + numbers) and the published third-party benchmarks. |
| [`03-toolchain-status.md`](03-toolchain-status.md) | Compatibility matrix: emcc checks, binaryen history (what merged in v125, what didn't), JSPI/fibers, setjmp/longjmp, mixing modes. |
| [`05-asyncify-fork-design.md`](05-asyncify-fork-design.md) | Asyncify.cpp internals, why catch arms are structurally hard, and the catch-arm-hoisting fork design with limits and effort. |
feat(wasm-eh): migrate the WASM build to native wasm exceptions (+ 3D viewer default-on) Replace the legacy Emscripten JS-exceptions model with native wasm-EH (legacy encoding) across the whole build, keeping Asyncify coroutines working via a from-source Binaryen --hoist-cpp-catches pre-pass. Net result: native-EH is the only build mode, the 3D viewer is on by default, and pcbnew shrinks substantially. Highlights: - Binaryen submodule everywhere + --hoist-cpp-catches integration in apply-asyncify; post-link Asyncify covers every app wasm (not just standalone test wasm). - Build deps (incl. OpenCASCADE without OCC_CONVERT_SIGNALS) and all KiCad apps with -fwasm-exceptions; emscripten_sleep added to the post-link asyncify-imports. - libcontext fiber entry wired under native exceptions; while-loop main loop + currData shim injected into all wx apps. - Native-EH collab apply fixed: DEBUG-define the embind TU + match all out-of-CMake C++ TUs' ABI flags to the core, fixing the vtable-layout skew / mis-dispatch. - 3D viewer enabled by default (real raytracer linked, not the stub). - Retire the EH-spike scaffolding; flip the asyncify-races ablation pins to shim-redundancy pins (native-EH stays clean with the legacy shims ablated). - Fix the asyncify-races quiescence check to not require Asyncify.currData==0: under the native-EH per-frame-yield top loop the main stack is asyncify-suspended every frame, so currData legitimately churns (a freed-but-not-yet-nulled buffer, not a leak). Refresh the pcbnew toolbar screenshot baseline for the new kicad. - CI: drop the obsolete binaryen_version input/env (the build uses the binaryen submodule fork's wasm-opt, not a version download); key the wasm-output cache on the binaryen submodule SHA instead. Bumps the wxwidgets + binaryen submodules to their squashed feature commits. Validated green: all 7 apps native-EH (real 3D in pcbnew); KiCad e2e 63/63 Firefox + Chromium (3D viewer renders); wx 336; coroutine 34/34 both engines; asyncify 7/7 both engines. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 19:50:18 +02:00
| [`06-spike-plan.md`](06-spike-plan.md) | **(2026-06-22)** Refreshed findings + the phased red-green spike plan; supersedes the encoding/Binaryen-version framing above. |
| [`07-spike-results-and-opinion.md`](07-spike-results-and-opinion.md) | **(2026-06-22)** Toy-spike results: asyncify + legacy-wasm-EH works; the `HoistCppCatches` Binaryen pass flips suspend-in-catch green on all 3 engines; go/no-go opinion. |
| [`08-wx-app-render-rootcause.md`](08-wx-app-render-rootcause.md) | Why a native-EH wx app rendered blank: the `set_main_loop` `"unwind"` throw caught by native-EH `catch_all` cleanup pads tore down the main frame. |
| [`09-event-loop-deparking-plan.md`](09-event-loop-deparking-plan.md) | The EH-agnostic main-loop rework (de-park → per-frame-yield `while`-loop) fixing the blank render + the coroutine/menu regressions. |
| [`10-pthreads-native-eh.md`](10-pthreads-native-eh.md) | **(2026-06-24)** Native-EH × pthreads: the main-thread thread-spawn regression (`invalid state: 1` / re-entrant `main()`); the pool pattern survives; the KiCad raw→pool refactor plan + the test gap. |
## Relationship to docs/features/async/
Independent axes of the same machine. The async dossier is about *correctness* (one global
`Asyncify.currData` shared by overlapping suspensions → crash/hang); this dossier is about
*cost* (how much code Asyncify instruments). Fixing one does not fix the other. Sequencing:
async arbiter first (shipping bugs), wasm-EH migration second (size/speed), and the
migration plan below assumes the arbiter exists.
## Migration plan (when triggered)
0. Resume the parked experiment (`docs/wasm-exceptions-experiment.md`): bump emsdk past
the 4.0.2 LLVM `br_table` bug, `git apply` its appendix patch (`KICAD_WASM_EH=1`
gated), full clean deps rebuild (stamp-skip gotcha: stale sjlj objects in cairo etc.).
Then decide the EH encoding: if newer LLVM emits parseable **legacy** encoding, the
catch-arm-hoisting pre-pass (05) applies; if **exnref** stays forced, asyncify needs
`TryTable` support + exnref spilling instead (05 §new-EH variant).
1. Newer Binaryen for the post-link step only: `scripts/common/get-wasm-opt.sh` already
abstracts the binary — point it at a ≥ v125 build carrying the hoisting patch (05).
2. Validate partial support first: rebuild one app `-fwasm-exceptions` + asyncify-asserts,
run the e2e suites; the asserts tripwire makes any missed unwind-from-catch a
deterministic trap.
3. Uniform flag flip: `-fexceptions``-fwasm-exceptions` in
`scripts/build-wxuniversal-wasm.sh:141-142`, `scripts/kicad/build-kicad-target.sh`
(lines ~203/207/211/214), `tests/apps/Makefile.wasm` (all occurrences) — plus
`-sSUPPORT_LONGJMP=wasm` (default with wasm-EH; the `emscripten` flavor is a hard error).
4. Drop `env.invoke_*` from `ASYNCIFY_IMPORTS` in `scripts/common/apply-asyncify.sh:33`.
5. Expect to delete/shrink shim machinery that exists only for the invoke world
(`inject-dyncall-shims.sh` phases 12) — verify, don't assume.
6. Keep `catch_audit.py` as a CI gate only if shipping *without* the fork (i.e., the
hand-refactor path); with the fork it is informational.