From 3a817347dbd46d0b7ff48abffeb4ad3a703a28df Mon Sep 17 00:00:00 2001 From: "Shota Kudo (chaploud)" Date: Thu, 16 Jul 2026 12:06:21 +0900 Subject: [PATCH 1/2] =?UTF-8?q?ADR-0204=20kickoff:=20binary-size=20campaig?= =?UTF-8?q?n=20=E2=80=94=20measured=20baseline=20+=20levers=20(D-521/D-522?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trigger: dogfooding mailbox from_cljw_05 (cljw measures zwasm at 44% of its shipped code, 4.0 MB budget line). zwasm-side symbol attribution (ReleaseSafe arm64 CLI 5,282,584 B @71cccba76) confirms and re-prioritizes: - api.jit_host_bridge = 1,311 KB / 5,453 syms (92% of api.*) — a comptime thunk cross-product (t2fp x2,880 + t1fp x960 + t0..4), NOT api surface. Lever #1 = D-522 (descriptor-driven trampolines; ABI/api unchanged). - engine.codegen.arm64.emit.compile = one 707 KB symbol (~161-arm ZirOp switch; handlers already extracted, dispatch_collector ADR-0074 substrate exists). Lever #2 = D-521 (finish registration, delete the switch; acceptance = emitted JIT bytes identical). - size_history.yaml baseline re-recorded: ReleaseFast base DOUBLED since 2026-06-12 (1,972,696 -> 3,884,856 B). Docs+ledger only; stage PRs follow separately. --- .dev/debt.yaml | 40 +++++++++ .dev/decisions/0204_binary_size_campaign.md | 89 +++++++++++++++++++++ .dev/handover.md | 21 ++++- bench/results/size_history.yaml | 12 +++ 4 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 .dev/decisions/0204_binary_size_campaign.md diff --git a/.dev/debt.yaml b/.dev/debt.yaml index ccbf25d61..2217b1cc5 100644 --- a/.dev/debt.yaml +++ b/.dev/debt.yaml @@ -3,6 +3,46 @@ # Refresh on every /continue resume — .claude/skills/continue/RESUME.md Step 0.5. entries: + - id: "D-521" + layer: "code" + status: "note" + description: |- + Binary-size campaign lever #2 (ADR-0204 D2): complete the table-driven + arm64 dispatch and DELETE the legacy switch. `engine.codegen.arm64.emit + .compile` is a single 707 KB __text symbol (~161-arm switch over ZirOp, + arms mostly delegate to already-extracted op_*.zig handlers); the + dispatch_collector substrate (ADR-0074) already table-dispatches + registered ops and falls through to the switch. Finish registration for + all remaining ops, delete the fall-through. Same treatment for the + x86_64 twin (costs Linux/Windows binaries; already target-gated out of + the arm64 binary). Acceptance: emitted JIT bytes IDENTICAL — + test-aot-diff 63/63 + fuzz-diff lanes + 3-OS test-all. + first_raised: "2026-07-16" + last_reviewed: "2026-07-16" + refs: |- + ADR-0204 D2; from_cljw_05 request 1; dispatch_collector ADR-0074 + front: binary-size + - id: "D-522" + layer: "api" + status: "note" + description: |- + Binary-size campaign lever #1 (ADR-0204 D1): collapse the + `api.jit_host_bridge` comptime thunk cross-product — 1,311 KB / 5,453 + syms, i.e. 92% of the whole api.* bucket (t2fp × 2,880 = 900 KB, + t1fp × 960 = 282 KB, t0..t4 × 320 each ≈ 124 KB; measured 2026-07-16, + ReleaseSafe arm64 CLI 5,282,584 B at 71cccba76). Stage 1 = + characterize the instantiation generator (which axes multiply to + 2,880); stage 2 = replace monomorphized per-type-combination + trampolines with runtime-generic marshalling over per-signature + descriptors for axes that don't change the emitted call sequence. + Constraints: JIT<->host ABI unchanged, no api break, no per-call + regression beyond noise (ADR-0181 bench series). + first_raised: "2026-07-16" + last_reviewed: "2026-07-16" + refs: |- + ADR-0204 D1; from_cljw_05 request 2 (re-prioritized: thunk + cross-product >> api-surface gating, which ADR-0073 already covers) + front: binary-size - id: "D-509" layer: "code" status: "note" diff --git a/.dev/decisions/0204_binary_size_campaign.md b/.dev/decisions/0204_binary_size_campaign.md new file mode 100644 index 000000000..d8bb24db1 --- /dev/null +++ b/.dev/decisions/0204_binary_size_campaign.md @@ -0,0 +1,89 @@ +# ADR-0204: Binary-size campaign — host-bridge thunk collapse + table-driven emitter completion + +- **Status**: Accepted (kickoff; stages land as separate PRs) +- **Date**: 2026-07-16 +- **Front**: binary-size (D-521 / D-522; dogfooding request `from_cljw_05`) +- **Findings base**: symbol-attribution measurement 2026-07-16 (below); + cljw-side measurement in `private/dogfooding_handover/from_cljw_05.md`. + +## Context + +cljw's binary-size campaign (cljw ADR-0172) measured zwasm as the largest +single component of its shipped binary: ~2.99 MB of `__text` (44% of all +code), with a 4.0 MB budget line, and sent three finished-form requests +(mailbox `from_cljw_05`, 2026-07-16): (1) table-driven arm64 emitter, +(2) comptime-gate unused api surfaces, (3) optional module split. Hard +constraints from the request: **no behaviour change, no JIT-output change, +no API break** — size-neutral refactors only, no urgency coupling. + +zwasm-side measurement (macOS arm64 ReleaseSafe CLI, 5,282,584 B at +71cccba76; `nm -n` next-addr − addr, bucketed by prefix) confirms and +*re-prioritizes* the cljw attribution: + +| Bucket | Size | Syms | Dominant content | +|---|---|---|---| +| `api.*` | 1,428 KB | 5,630 | `api.jit_host_bridge` alone = **1,311 KB / 5,453 syms** | +| `engine.*` | 1,248 KB | 516 | `engine.codegen.arm64.emit.compile` alone = **707 KB** (one symbol) | +| everything else | ≤101 KB per bucket | — | Io / debug / feature / instruction / … | + +The `size_history.yaml` series independently confirms the drift: ReleaseFast +`base` was 1,972,696 B at the last record (6979c838, 2026-06-12) and is +3,884,856 B at this kickoff (71cccba76) — the binary doubled in a month +(AOT campaign + host-bridge growth), unnoticed because the series' cadence +is phase-boundary. + +Inside `jit_host_bridge`, the cost is a comptime thunk cross-product, not +API surface: `t2fp__struct.f` × 2,880 instantiations = 900 KB, +`t1fp__struct.f` × 960 = 282 KB, `t0..t4__struct.f` × 320 each ≈ 124 KB. +The x86_64 backend is already target-comptime-gated out of the arm64 binary +(0 B), and `enable_component` already comptime-strips the CM subsystem at +`-Dwasi=p1` (ADR-0073 DCE substrate) — so cljw's request (2) as literally +stated ("gate unused api surfaces") is *already mostly satisfied*; what an +embedder actually pays for is the thunk cross-product, which every +JIT-enabled embedder pays regardless of which api slice it imports. + +## Decisions + +### D1 — Collapse the `jit_host_bridge` thunk cross-product (D-522, lever #1: ~1.2 MB potential) + +Reduce the comptime instantiation space of the host-call trampolines +(`t0..t4`, `t1fp`, `t2fp` families) toward a small set of runtime-generic +trampolines (argument marshalling driven by a per-signature descriptor +table instead of one monomorphized function per type combination). +Constraints: JIT↔host call ABI unchanged, no api break, no per-call +runtime regression beyond noise (bench-gated per ADR-0181 series). +Stage-gated: characterize the instantiation generator first (why 2,880 — +which axes multiply), then collapse axes that don't affect the emitted +call sequence. + +### D2 — Complete the table-driven arm64 dispatch; delete the legacy switch (D-521, lever #2: ~500 KB potential) + +`engine.codegen.arm64.emit.compile` (src/engine/codegen/arm64/emit.zig, +~2,100-line fn) is a ~161-arm switch over `ZirOp` whose arms mostly +delegate to already-extracted `op_*.zig` handlers. The table-driven +substrate already exists: `dispatch_collector` (ADR-0074) dispatches +registered ops first and falls through to the switch. Finish the +registration for all remaining ops and delete the fall-through switch. +Same treatment applies to the x86_64 twin (`x86_64/emit.zig` `compile()`) +— it costs the Linux/Windows binaries, not the Mac one. Acceptance gate: +**emitted JIT bytes identical** — `zig build test-aot-diff` (63/63), +`zig build fuzz-diff` lanes, full `test-all` on 3 OSes. + +### D3 — Module split (cljw request 3): NOT pursued as its own workstream + +Only adopted if it falls out of D1/D2 naturally (cljw's own framing). No +architecture contortion for a hybrid ReleaseSmall build. + +## Consequences + +- Debt rows D-521 (emitter table completion) + D-522 (thunk collapse) track + the stages; each stage = own `develop/` PR with the standard CI gate. +- Baseline recorded to `bench/results/size_history.yaml` at kickoff + (`record_binary_size.sh`, base + lean variants); re-run per stage-merge so + the size delta of each lever is OBSERVED (per-merge bench policy). +- Success bar (finished-form): arm64 ReleaseSafe CLI `__text` shrinks by + the two levers' measured share without any JIT-output or api change; + cljw re-measures on its normal pin-bump cadence — no coupling. +- Reply posted to the mailbox (`to_cljw_05.md`): confirms receipt, shares + the re-prioritized attribution (thunk cross-product ≫ api-surface gating; + x86_64 emitter already exists and is already target-gated). diff --git a/.dev/handover.md b/.dev/handover.md index 10033749e..829a310c4 100644 --- a/.dev/handover.md +++ b/.dev/handover.md @@ -11,7 +11,26 @@ PIC codegen D-516 fix; release.yml auto-built Release + 5 assets; release was USER-DIRECTED 2026-07-09 per ADR-0156). v1 frozen at `v1.11.1`. Dev model: cut a `develop/` branch from `main` → PR → CI `ci-required` 3-OS gate must be green to merge. **Release stays user-only (ADR-0156)** — never autonomously tag / -publish / cut over. No active campaign/bundle; no cron self-re-arm. +publish / cut over. Active campaign: binary-size (ADR-0204, below); no cron +self-re-arm. + +## Active front — binary-size campaign (ADR-0204, kickoff 2026-07-16) + +Trigger = dogfooding mailbox `from_cljw_05` (cljw measures zwasm at 44% of its +shipped code; user authorized an independent zwasm-side campaign). Measured +baseline (ReleaseSafe arm64 CLI 5,282,584 B @71cccba76; ReleaseFast base +DOUBLED since 2026-06-12: 1,972,696 → 3,884,856 B): +- **Lever #1 = D-522**: `api.jit_host_bridge` comptime thunk cross-product = + 1,311 KB / 5,453 syms (t2fp×2,880 + t1fp×960 + t0..4). Characterize the + instantiation axes first, then collapse to descriptor-driven trampolines. + ABI/api unchanged; bench-gated. +- **Lever #2 = D-521**: `engine.codegen.arm64.emit.compile` = one 707 KB + symbol (~161-arm ZirOp switch). Finish dispatch_collector (ADR-0074) + registration, delete the fall-through switch; x86_64 twin same treatment. + Acceptance: emitted JIT bytes IDENTICAL (test-aot-diff + fuzz-diff). +- Re-run `record_binary_size.sh` per stage-merge (rows into the same PR). +- cljw reply posted (`to_cljw_05.md`): thunk cross-product ≫ api-surface + gating (ADR-0073 already covers the latter); x86_64 emitter already exists. ## AOT-full-fidelity campaign — CLOSED 2026-07-09 (ADR-0203 Implemented) diff --git a/bench/results/size_history.yaml b/bench/results/size_history.yaml index 41a887802..dd92bc35e 100644 --- a/bench/results/size_history.yaml +++ b/bench/results/size_history.yaml @@ -37,3 +37,15 @@ variant: "lean" optimize: "ReleaseFast" zwasm_bytes: 1775096 + +- commit: "71cccba76" + timestamp: "2026-07-16T03:01:15Z" + variant: "base" + optimize: "ReleaseFast" + zwasm_bytes: 3884856 + +- commit: "71cccba76" + timestamp: "2026-07-16T03:01:15Z" + variant: "lean" + optimize: "ReleaseFast" + zwasm_bytes: 3646232 From e93c040811754fac2d49f2df17b7ef95ea84242b Mon Sep 17 00:00:00 2001 From: "Shota Kudo (chaploud)" Date: Thu, 16 Jul 2026 12:11:28 +0900 Subject: [PATCH 2/2] ADR-0204: record D-522 stage-1 characterization (thunk axes) jit_host_bridge instantiation axes = arg-kinds x RetKind(5) x MAX_HOST_SLOTS(64); the x64 slot axis is 94% of the product and exists only to comptime-hardcode the slot index. Stage 1 = shared FP-bridge delegation (~0.8 MB, no ABI change); stage 2 = slot-axis collapse via runtime slot conveyance (~0.4 MB, internal call-sequence change). --- .dev/debt.yaml | 18 ++++++++++------ .dev/decisions/0204_binary_size_campaign.md | 24 ++++++++++++++++----- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/.dev/debt.yaml b/.dev/debt.yaml index 2217b1cc5..a68df2247 100644 --- a/.dev/debt.yaml +++ b/.dev/debt.yaml @@ -30,13 +30,17 @@ entries: `api.jit_host_bridge` comptime thunk cross-product — 1,311 KB / 5,453 syms, i.e. 92% of the whole api.* bucket (t2fp × 2,880 = 900 KB, t1fp × 960 = 282 KB, t0..t4 × 320 each ≈ 124 KB; measured 2026-07-16, - ReleaseSafe arm64 CLI 5,282,584 B at 71cccba76). Stage 1 = - characterize the instantiation generator (which axes multiply to - 2,880); stage 2 = replace monomorphized per-type-combination - trampolines with runtime-generic marshalling over per-signature - descriptors for axes that don't change the emitted call sequence. - Constraints: JIT<->host ABI unchanged, no api break, no per-call - regression beyond noise (ADR-0181 bench series). + ReleaseSafe arm64 CLI 5,282,584 B at 71cccba76). Axes characterized + 2026-07-16: arg-kinds x RetKind(5) x MAX_HOST_SLOTS(64); the x64 + slot axis (94% of the product) exists only to comptime-hardcode the + slot index K. Stage 1 = shared FP-bridge delegation (t1fp/t2fp + currently inline ~300 B of payload-lookup+invokeCb per thunk vs the + GP thunks' 88 B bridge() delegation) — no ABI/JIT change, ~0.8 MB. + Stage 2 = collapse the slot axis by conveying K at runtime (call + site materializes slot index / payload ptr into a scratch reg + before BLR) — changes zwasm's own emitted call sequence (internal, + AOT-cache version-keyed), ~0.4 MB more. Constraints: no api break, + no per-call regression beyond noise (ADR-0181 bench series). first_raised: "2026-07-16" last_reviewed: "2026-07-16" refs: |- diff --git a/.dev/decisions/0204_binary_size_campaign.md b/.dev/decisions/0204_binary_size_campaign.md index d8bb24db1..360c5b719 100644 --- a/.dev/decisions/0204_binary_size_campaign.md +++ b/.dev/decisions/0204_binary_size_campaign.md @@ -50,11 +50,25 @@ Reduce the comptime instantiation space of the host-call trampolines (`t0..t4`, `t1fp`, `t2fp` families) toward a small set of runtime-generic trampolines (argument marshalling driven by a per-signature descriptor table instead of one monomorphized function per type combination). -Constraints: JIT↔host call ABI unchanged, no api break, no per-call -runtime regression beyond noise (bench-gated per ADR-0181 series). -Stage-gated: characterize the instantiation generator first (why 2,880 — -which axes multiply), then collapse axes that don't affect the emitted -call sequence. +Constraints: no api break, no per-call runtime regression beyond noise +(bench-gated per ADR-0181 series). + +Characterization (2026-07-16, `src/api/jit_host_bridge.zig` — 344 lines +generating 5,453 symbols): the instantiation axes are +`arg-kinds × RetKind(5) × MAX_HOST_SLOTS(64)` — t2fp = 3×3×5×64 = 2,880, +t1fp = 3×5×64 = 960, t0..t4 = 5×5×64 = 1,600. Two independent findings: +- **The ×64 slot axis is 94% of the product** and exists only so each + thunk comptime-hardcodes its slot index K (the JIT call passes only + `rt` + wasm args). Collapsing it means conveying K at runtime — e.g. + the call site materializes the slot index (or payload pointer) into a + scratch register before `BLR`. That changes zwasm's own emitted + call sequence (internal, version-keyed via the AOT cache — allowed), + NOT the embedder-facing api. +- **t1fp/t2fp inline the whole payload-lookup + invokeCb body per thunk** + (~300 B each) where the GP thunks delegate to a shared `bridge()` + (~88 B each). A shared FP-bridge delegation alone — no ABI/JIT change + at all — cuts ~0.8 MB and is the natural stage-1 increment; the slot- + axis collapse (further ~0.4 MB) is stage 2. ### D2 — Complete the table-driven arm64 dispatch; delete the legacy switch (D-521, lever #2: ~500 KB potential)