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>
93 lines
5.1 KiB
Markdown
93 lines
5.1 KiB
Markdown
# 02 — Measurements: our controlled experiment + published benchmarks
|
||
|
||
## §1. Our experiment (pcbnew, 2026-06-10)
|
||
|
||
**Question:** how much of the asyncify size tax is invoke-driven (i.e., would vanish under
|
||
wasm-EH)?
|
||
|
||
**Method:** take the saved pre-asyncify artifact `output/pcbnew.finalized.wasm` (111 MB,
|
||
post-link, post-finalize, pre-asyncify) and run the production post-link pipeline
|
||
(`apply-asyncify.sh`: `wasm-opt --asyncify` + `wasm-opt -O2`, Binaryen v121) three ways,
|
||
into `output/measure/` (originals untouched):
|
||
|
||
1. **O2-only** (no asyncify) — the floor; *not a shippable config* (the app needs asyncify).
|
||
2. **Asyncify, `env.invoke_*` dropped** from the imports list — the wasm-EH proxy.
|
||
3. **Asyncify, full imports** — the shipped configuration (control).
|
||
|
||
Control sanity check: variant 3 reproduced the production `pcbnew.wasm` size exactly
|
||
(187 MB), so the pipeline replication is faithful.
|
||
|
||
**Results:**
|
||
|
||
| Variant | Raw | gzip -9 |
|
||
|---|---|---|
|
||
| O2-only floor | 77.6 MB | 19.8 MB |
|
||
| Asyncify w/o invokes (wasm-EH proxy) | 122.1 MB | 36.0 MB |
|
||
| Asyncify full (shipped) | 187.3 MB | 64.5 MB |
|
||
|
||
**Decomposition:**
|
||
|
||
- Total asyncify tax today: **+109.7 MB raw = +141% over floor** (+44.7 MB gzip). That is
|
||
~2× the worst-case figures in the published literature — KiCad is an extreme asyncify
|
||
case, and the invoke graph is why.
|
||
- **Invoke-driven share of the tax: 59% raw (65.2 MB), 64% gzip (28.5 MB).** This is the
|
||
part wasm-EH eliminates.
|
||
- The remaining no-invoke tax (+44.5 MB, +57% over floor) is the legitimate cost of the
|
||
real suspend chains (fibers + modal/clipboard sleeps) — in line with the published ~50% —
|
||
and is kept under any design.
|
||
|
||
**Headline:** wasm-EH ≈ **download 64.5 → 36.0 MB (−44%), module 187 → 122 MB (−35%)**,
|
||
before counting runtime. The real wasm-EH build differs from the proxy in both directions:
|
||
it also deletes the invoke wrappers + JS-EH glue outright (smaller still) while adding
|
||
wasm-EH's own try/catch instructions (~+4% per published prototype data).
|
||
|
||
**Caveats:** (a) the no-invoke proxy is **semantically unsound under `-fexceptions`** —
|
||
with invokes present in JS but uninstrumented in wasm, the first suspension crossing a try
|
||
frame corrupts state; measurement artifact only, never ship. (b) The finalized input is a
|
||
May 26 snapshot while today's apps are newer — irrelevant for the *relative* comparison
|
||
(same input, same tool, only the imports list varies). (c) Runtime gain is not directly
|
||
measurable from these artifacts; the size-tracks-speed heuristic (Zakai) plus the published
|
||
reports below are the basis for the runtime estimate.
|
||
|
||
## §2. Published benchmarks and field reports (researched 2026-06-10)
|
||
|
||
No clean head-to-head JS-EH vs wasm-EH benchmark of a large C++ app exists; triangulate:
|
||
|
||
**Asyncify instrumentation tax (the thing our invoke graph inflates):**
|
||
|
||
| Source | Workload | Numbers |
|
||
|---|---|---|
|
||
| Alon Zakai, "Pause and Resume… Asyncify" (2019, canonical) | multiple benchmarks | size ~**+50%** typical, ≤2× worst; speed tracks size; **22%** Fannkuch; **5×** pathological when one huge function dominates (SQLite 150→300 KB) |
|
||
| wa-sqlite (production, measured) | SQLite wasm | default asyncify **+70.6%** → **+35.3%** after narrowing instrumentation to the ~6 actually-async entry points (auto-detection had instrumented 700+ functions) |
|
||
| Wasmer PHP/WordPress (production) | PHP setjmp/longjmp via asyncify | **1.5× size, 2× slower**; replacing with wasm-EH SjLj: **2×** faster cold start (120→60 ms), separately **4×** (100→25 ms) |
|
||
| WordPress Playground | auto-detected asyncify list | ~70,000 functions instrumented, startup +4.5 s, fixed by manual list |
|
||
|
||
**JS-EH (`invoke_*`) tax:**
|
||
|
||
| Source | Workload | Numbers |
|
||
|---|---|---|
|
||
| OpenCascade.js (CAD kernel — closest cousin to KiCad's code character) | STEP file load | global JS-EH **~10 min → ~2 min (~5×)** with narrowed EH; "performance degradation and file size impact is currently massive" |
|
||
| emscripten discussion #17526 | JS-EH → wasm-EH | "not just 2× faster, but an order of magnitude" (anecdote) |
|
||
| Emscripten docs (Box2D) | `-fno-rtti -fno-exceptions` | −15% size (RTTI+EH combined; only weak isolation) |
|
||
|
||
**wasm-EH's own cost (what we'd pay instead):**
|
||
|
||
| Source | Numbers |
|
||
|---|---|
|
||
| Heejin Ahn, TPAC slides (prototype) | **+4%** whole-module instructions, **+11%** in exception-using functions |
|
||
| V8 / Emscripten docs | non-throwing path **~zero cost** by design |
|
||
|
||
**Most transferable to us:** wa-sqlite's 70.6→35.3 (the narrowing effect — ours measured
|
||
59–64%) and OpenCascade's ~5× (exception-dense C++ file loading — our `LoadBoard` path).
|
||
Triangulated runtime expectation for KiCad: **1.5–3× on load/parse/geometry paths**, more
|
||
on exception-dense I/O paths.
|
||
|
||
## §3. Reproducing
|
||
|
||
```sh
|
||
# control (uses the project script unmodified):
|
||
./scripts/common/apply-asyncify.sh output/pcbnew.finalized.wasm output/measure/pcbnew.asyncify-full.wasm
|
||
# proxy: same script with ASYNCIFY_IMPORTS="env.__asyncjs__*,env.emscripten_fiber_swap"
|
||
# floor: $(scripts/common/get-wasm-opt.sh | tail -1) -O2 in.wasm -o out.wasm
|
||
# then: ls -l + gzip -9c | wc -c
|
||
```
|