pcbjam/docs/features/wasm-exceptions/README.md
Viktor Vaczi 66ce367703 docs: asyncify currData + wasm-exceptions research dossiers
Two research dossiers under docs/features:
- async/: Asyncify currData contention — the clipboard crash and post-idle
  tool hang as one disease (single global suspension slot, three producers),
  asyncify internals walkthroughs, prior art, arbiter designs A/B.
- wasm-exceptions/: -fexceptions vs -fwasm-exceptions — measured invoke-driven
  share of the asyncify tax on pcbnew (59% raw / 64% gzip; download 64.5->36 MB
  if migrated), KiCad dialog-in-catch audit (85 direct / 93 review of 636,
  catch_audit.py included), binaryen toolchain status (partial EH support since
  v125; TryTable unsupported), and a catch-arm-hoisting pre-pass design that
  would obsolete the KiCad-side refactor. Cross-linked with the parked
  KICAD_WASM_EH end-to-end experiment (docs/wasm-exceptions-experiment.md).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 17:02:59 +02:00

97 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# `-fexceptions` vs `-fwasm-exceptions` in KiCad-WASM — research dossier
> **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.
## 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, ~400–800 lines, 1–2 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. |
| [`04-kicad-audit.md`](04-kicad-audit.md) | The brace-matching catch-block audit: 85 direct / 93 review / 458 trivial of 636; libpng/libjpeg setjmp story; refactor effort if done by hand. |
| [`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. |
| [`catch_audit.py`](catch_audit.py) | The audit tool (re-runnable; suitable as a CI gate on the kicad submodule). |
| [`audit-results.txt`](audit-results.txt) | Full audit output incl. all 85 direct-suspend sites. |
## 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 1–2) — 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.