From bf523c16e33abd6e73187f7e0fed64ff1249930a Mon Sep 17 00:00:00 2001 From: Dietmar Rietsch Date: Sat, 17 Oct 2026 09:05:15 +0200 Subject: [PATCH 1/2] web-render: ship the engine wasm with fat LTO; 9 MiB size gate; 0.1.0-canary.18 The engine wasm was built with cargo's default release profile: no cross-crate LTO and 16 codegen units. A `wasm-release` profile (release, lto = "fat", codegen-units = 1) used by scripts/build-wasm.sh and scripts/build-wasm-perf.sh takes blitz_web_bg.wasm from 10 723 939 to 9 016 823 bytes (-1 707 116, -15.9%; brotli 2 612 273 -> 2 496 172), built with the CI pins (Rust 1.93.0, wasm-bindgen 0.2.125, binaryen 119, wasm-opt -Oz unchanged). The native tests and benches keep their profiles. No slowdown: the five perf workloads, timed in node against both wasm builds in alternating rounds, run 0-6% faster, and every workload's JSON output is byte-identical between the two builds. The size gate now reads its ceiling from the manifest's maxBytes for the blitz wasm, lowered from 64 MiB to 9 MiB (9 437 184), so growth past the current size plus ~4.7% fails the build. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/vitest.yml | 2 +- docs/README.md | 2 +- docs/architecture.md | 8 +++++--- packages/web-bundle/manifest.json | 2 +- packages/web-bundle/package.json | 2 +- packages/web-render/Cargo.toml | 11 +++++++++++ scripts/build-wasm-perf.sh | 6 ++++-- scripts/build-wasm.sh | 31 ++++++++++++++++++++++--------- 8 files changed, 46 insertions(+), 18 deletions(-) diff --git a/.github/workflows/vitest.yml b/.github/workflows/vitest.yml index 1c2b7c1..16c4ed9 100644 --- a/.github/workflows/vitest.yml +++ b/.github/workflows/vitest.yml @@ -52,7 +52,7 @@ jobs: run: | curl -sL https://github.com/WebAssembly/binaryen/releases/download/version_119/binaryen-version_119-x86_64-linux.tar.gz | tar xz echo "$PWD/binaryen-version_119/bin" >> "$GITHUB_PATH" - - name: build the Blitz ENGINE wasm artifact (64 MiB budget gate; stamps bin/SOURCE_HASH) + - name: build the Blitz ENGINE wasm artifact (manifest maxBytes budget gate; stamps bin/SOURCE_HASH) run: bash scripts/build-wasm.sh --engine # ------------------------------------------------------------------ - run: pnpm install --frozen-lockfile diff --git a/docs/README.md b/docs/README.md index 3daceba..3b63fea 100644 --- a/docs/README.md +++ b/docs/README.md @@ -25,7 +25,7 @@ them. The last column says what the decision means for this plugin. | [017](https://github.com/paged-media/plugin-sdk/blob/main/docs/adr/017-importer-exporter-door-shape.md) | plugin-sdk | Importer and exporter door shape | `.html` and `.htm` files are routed to this plugin's importer by extension or MIME type. It receives the file's bytes and inserts a web frame (`packages/web-bundle/src/activate.ts`). The exporter half is not used. | | [305](https://github.com/paged-media/plugin-sdk/blob/main/docs/adr/305-doors-always-present.md) | plugin-sdk | Every door is always present; `supports()` reports a missing backend | Each optional door is probed with `host.supports(...)` and has a fallback: a textarea for the code editor, a substitution note for font bytes, a paste box for the clipboard, the metadata copy alone for container parts, a render that is not submitted for the scene layer, no importer. | | [307](https://github.com/paged-media/plugin-sdk/blob/main/docs/adr/307-contract-as-peer-dependency.md) | plugin-sdk | Bundles take the contract packages as peer dependencies | `@paged-media/plugin-api` and `@paged-media/plugin-sdk` are peer dependencies of `@paged-media/web` (`packages/web-bundle/package.json`). | -| [308](https://github.com/paged-media/plugin-sdk/blob/main/docs/adr/308-plugin-wasm.md) | plugin-sdk | Plugin wasm is a declared capability, loaded by the bundle, under one size budget | The manifest declares one wasm module, `bin/blitz_web_bg.wasm`, with a 64 MiB ceiling. `packages/web-bundle/src/engine-loader.ts` loads it on first render, and `scripts/build-wasm.sh` fails above the same ceiling. | +| [308](https://github.com/paged-media/plugin-sdk/blob/main/docs/adr/308-plugin-wasm.md) | plugin-sdk | Plugin wasm is a declared capability, loaded by the bundle, under one size budget | The manifest declares one wasm module, `bin/blitz_web_bg.wasm`, with a 9 MiB ceiling (`maxBytes`). `packages/web-bundle/src/engine-loader.ts` loads it on first render, and `scripts/build-wasm.sh` reads the ceiling from the manifest and fails above it. | | [309](https://github.com/paged-media/plugin-sdk/blob/main/docs/adr/309-conformance-against-real-engine.md) | plugin-sdk | Conformance runs against the real published engine | `packages/web-bundle/test/conformance/` drives the bundle through `createHeadlessHost` from `@paged-media/plugin-sdk`. | | [310](https://github.com/paged-media/plugin-sdk/blob/main/docs/adr/310-one-write-door.md) | plugin-sdk | One write door: `document.mutate`, engine-owned history, failures as outcomes | Insert and flatten go through `host.document.mutate`, a save through `setMetadata`. The code tests `outcome.applied` and leaves undo to the host: an insert is one batch, so one undo step. | | [311](https://github.com/paged-media/plugin-sdk/blob/main/docs/adr/311-plugin-state-under-own-id.md) | plugin-sdk | Plugin state lives only under the plugin's own id | The source is kept under the metadata key `x-paged:media.paged.web` and in the plugin's own container-part namespace. | diff --git a/docs/architecture.md b/docs/architecture.md index 8c19866..316e46f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -204,13 +204,15 @@ back on every open to the families the document's text names and lacks (`documen The manifest declares the matching capabilities: `document` (read `broad`, write `scoped`), `rendering` (`sceneLayer`, `overlay`), `dataProviders` (consume `dataset`), `editContext` (`webFrame`), `assets` (`fonts`), `clipboard` -(`full`), `network: false`, and one wasm module (`blitz`, at most 64 MiB). +(`full`), `network: false`, and one wasm module (`blitz`, at most 9 MiB: `maxBytes` 9 437 184). ## Build and test - `bash scripts/build-wasm.sh --engine` builds the crate for `wasm32-unknown-unknown` with - the `blitz` feature, runs `wasm-bindgen --target web` and, if installed, `wasm-opt -Oz`, - and writes `packages/web-bundle/bin/` (gitignored). It fails if the wasm exceeds 64 MiB. + the `blitz` feature and the crate's `wasm-release` profile (release with fat LTO in one + codegen unit), runs `wasm-bindgen --target web` and, if installed, `wasm-opt -Oz`, and + writes `packages/web-bundle/bin/` (gitignored). It fails if the wasm exceeds the manifest's + `maxBytes` for it. Without `--engine` the crate is built without the engine and has neither export. `pnpm -r build` then runs tsup; `.github/workflows/publish.yml` does both in that order and publishes `@paged-media/web` under the `canary` tag. diff --git a/packages/web-bundle/manifest.json b/packages/web-bundle/manifest.json index f2b84f7..2a99540 100644 --- a/packages/web-bundle/manifest.json +++ b/packages/web-bundle/manifest.json @@ -25,7 +25,7 @@ "name": "blitz", "path": "bin/blitz_web_bg.wasm", "purpose": "engine", - "maxBytes": 67108864 + "maxBytes": 9437184 } ], "network": false, diff --git a/packages/web-bundle/package.json b/packages/web-bundle/package.json index 95d6224..62174cf 100644 --- a/packages/web-bundle/package.json +++ b/packages/web-bundle/package.json @@ -1,7 +1,7 @@ { "name": "@paged-media/web", "private": false, - "version": "0.1.0-canary.17", + "version": "0.1.0-canary.18", "license": "AGPL-3.0-only OR LicenseRef-PMEL", "type": "module", "main": "./src/index.ts", diff --git a/packages/web-render/Cargo.toml b/packages/web-render/Cargo.toml index 073a5ed..9d94360 100644 --- a/packages/web-render/Cargo.toml +++ b/packages/web-render/Cargo.toml @@ -123,3 +123,14 @@ harness = false required-features = ["blitz"] [workspace] + +# The profile the engine wasm ships with (scripts/build-wasm.sh, +# scripts/build-wasm-perf.sh). Whole-program LTO in one codegen unit lets the +# optimiser drop and merge code across the Blitz/Stylo/Parley crates that the +# default release profile (thin local LTO, 16 units) keeps. It only changes how +# the same code is compiled: the native tests and benches keep the default +# profiles. +[profile.wasm-release] +inherits = "release" +lto = "fat" +codegen-units = 1 diff --git a/scripts/build-wasm-perf.sh b/scripts/build-wasm-perf.sh index f64e0fc..91213a1 100755 --- a/scripts/build-wasm-perf.sh +++ b/scripts/build-wasm-perf.sh @@ -27,10 +27,12 @@ if [[ "$LOCKED" != "$CLI" ]]; then exit 1 fi -( cd "$CRATE" && cargo build --release --target "$TARGET" \ +# Same profile as the shipped engine (`wasm-release`, Cargo.toml), so the +# wall-clock the perf spec prints is the shipped build's. +( cd "$CRATE" && cargo build --profile wasm-release --target "$TARGET" \ --features blitz,perf-counters --target-dir "$CRATE/target/perf" ) mkdir -p "$OUT" -wasm-bindgen "$CRATE/target/perf/$TARGET/release/web_render.wasm" \ +wasm-bindgen "$CRATE/target/perf/$TARGET/wasm-release/web_render.wasm" \ --target web --out-dir "$OUT" --out-name blitz_web echo "build-wasm-perf: wrote the counting engine to $OUT" diff --git a/scripts/build-wasm.sh b/scripts/build-wasm.sh index 4d13fda..cb180a0 100755 --- a/scripts/build-wasm.sh +++ b/scripts/build-wasm.sh @@ -57,14 +57,17 @@ if [[ "$LOCKED" != "$CLI" ]]; then fi SOURCE_HASH=$(node "$HERE/scripts/source-hash.mjs") +# The crate's `wasm-release` profile (Cargo.toml): release + fat LTO in one +# codegen unit, which the shipped size depends on. +PROFILE="wasm-release" # shellcheck disable=SC2086 # FEATURES is intentionally word-split -( cd "$CRATE" && WEB_RENDER_SOURCE_HASH="$SOURCE_HASH" cargo build --release --target "$TARGET" $FEATURES ) +( cd "$CRATE" && WEB_RENDER_SOURCE_HASH="$SOURCE_HASH" cargo build --profile "$PROFILE" --target "$TARGET" $FEATURES ) # Cargo writes under CARGO_TARGET_DIR when it is set (relative to the crate, # where cargo ran), else the crate's own target/. TARGET_DIR="${CARGO_TARGET_DIR:-target}" [[ "$TARGET_DIR" = /* ]] || TARGET_DIR="$CRATE/$TARGET_DIR" -WASM_IN="$TARGET_DIR/$TARGET/release/web_render.wasm" +WASM_IN="$TARGET_DIR/$TARGET/$PROFILE/web_render.wasm" if [[ ! -f "$WASM_IN" ]]; then echo "build-wasm: expected $WASM_IN — build produced no cdylib" >&2 exit 1 @@ -83,21 +86,31 @@ echo "$SOURCE_HASH" > "$OUT/SOURCE_HASH" echo "build-wasm: wrote artifact(s) to $OUT (SOURCE_HASH ${SOURCE_HASH:0:12})" ls -la "$OUT" -# Size report + budget gate. The manifest's `capabilities.wasm[blitz]` -# declares a 64 MiB ceiling for this artifact; the app-wide sum is the -# editor's concern (its scripts/wasm-budget.mjs). +# Size report + budget gate. The ceiling is the manifest's +# `capabilities.wasm[blitz].maxBytes` (read here, so the two cannot drift): +# the shipped size plus a small margin, so growth fails the build and a raise +# is a reviewed change. The app-wide sum is the editor's concern (its +# scripts/wasm-budget.mjs). BG="$OUT/blitz_web_bg.wasm" -BUDGET_BYTES=$((64 * 1024 * 1024)) +BUDGET_BYTES=$(node -e ' + const m = require(process.argv[1]); + const w = (m.capabilities.wasm || []).find((x) => x.name === "blitz"); + if (!w || !(w.maxBytes > 0)) process.exit(1); + console.log(w.maxBytes); +' "$HERE/packages/web-bundle/manifest.json") || { + echo "build-wasm: error — manifest.json declares no maxBytes for the blitz wasm" >&2 + exit 1 +} RAW_BYTES=$(wc -c < "$BG" | tr -d ' ') -printf 'build-wasm: artifact %s = %d bytes (%.2f MiB), budget %d MiB\n' \ +printf 'build-wasm: artifact %s = %d bytes (%.2f MiB), budget %d bytes (%.2f MiB)\n' \ "$(basename "$BG")" "$RAW_BYTES" "$(echo "$RAW_BYTES" | awk '{print $1/1048576}')" \ - $((BUDGET_BYTES / 1024 / 1024)) + "$BUDGET_BYTES" "$(echo "$BUDGET_BYTES" | awk '{print $1/1048576}')" if command -v brotli >/dev/null 2>&1; then BR_BYTES=$(brotli -q 11 -c "$BG" 2>/dev/null | wc -c | tr -d ' ') printf 'build-wasm: brotli transfer size = %d bytes (%.2f MiB)\n' \ "$BR_BYTES" "$(echo "$BR_BYTES" | awk '{print $1/1048576}')" fi if (( RAW_BYTES > BUDGET_BYTES )); then - echo "build-wasm: ERROR — artifact exceeds the 64 MiB budget" >&2 + echo "build-wasm: ERROR — artifact exceeds its $BUDGET_BYTES-byte budget (manifest maxBytes)" >&2 exit 1 fi From ea43105731786115900cf827bdcdb0d335ff9009 Mon Sep 17 00:00:00 2001 From: Dietmar Rietsch Date: Sat, 17 Oct 2026 09:10:25 +0200 Subject: [PATCH 2/2] docs: engine wasm size and the build-profile perf record status.md gets an "Engine size" section: 9 015 481 bytes as CI builds it (2 497 009 brotli), against 10 723 633 for the published canary.17 wasm, and the 9 MiB manifest gate. The perf record gets the size and wall-clock comparison of the release and wasm-release builds. Co-Authored-By: Claude Opus 5.5 --- docs/design/perf-baseline-2026-10-05.md | 33 +++++++++++++++++++++++++ docs/status.md | 13 ++++++++++ 2 files changed, 46 insertions(+) diff --git a/docs/design/perf-baseline-2026-10-05.md b/docs/design/perf-baseline-2026-10-05.md index 6823124..37342f4 100644 --- a/docs/design/perf-baseline-2026-10-05.md +++ b/docs/design/perf-baseline-2026-10-05.md @@ -204,3 +204,36 @@ Recorded, not built: needs a different fragmentation model, not a cache. - **Parse once per flow group** (target 7) is host-side (the bundle calls the engine per group). + +## The wasm build profile (2026-10-17) + +The engine wasm moved from cargo's default release profile (no cross-crate LTO, 16 codegen +units) to the crate's `wasm-release` profile (release, `lto = "fat"`, `codegen-units = 1`). +`wasm-opt -Oz` was kept. Both builds used the CI pins: Rust 1.93.0, wasm-bindgen 0.2.125 +and binaryen 119. + +| | raw bytes | brotli | +|---|---:|---:| +| release profile (to 0.1.0-canary.17) | 10 723 939 | 2 612 273 | +| `wasm-release` (from 0.1.0-canary.18) | **9 016 823** | **2 496 172** | + +Those are local builds. CI's builds differ by a few hundred bytes from the embedded build +paths: the published canary.17 wasm is 10 723 633 bytes, and CI's `wasm-release` build is +9 015 481 bytes (2 497 009 brotli). + +The wall-clock comes from the five workloads above, run through the wasm's exported entries +in node on one machine. Each build ran 10 alternating rounds of 40 iterations per workload, +and the table gives the median of the round medians. It is for information only and is not +gated: + +| Workload | release | `wasm-release` | +|---|---:|---:| +| article, 1 frame (400 × 8192 px) | 5.16 ms | 4.84 ms | +| article flowed into 4 frames | 8.90 ms | 8.82 ms | +| article flowed into 12 frames | 21.40 ms | 21.12 ms | +| 300-row table, 1 frame | 12.18 ms | 12.16 ms | +| 200 styled runs, 1 frame | 2.10 ms | 2.03 ms | +| instantiation | 9.9 ms | 8.7 ms | + +For every workload, the two builds return byte-identical JSON. The host budgets in +`perf-budgets.spec.ts` give the same counts for both. diff --git a/docs/status.md b/docs/status.md index 6b99a23..037311a 100644 --- a/docs/status.md +++ b/docs/status.md @@ -210,6 +210,19 @@ band-only flow paint, one font context per engine) are in Chrome, with every open defect and its upstream cause, is in `packages/web-conformance/chrome/PARITY.md` and `UPSTREAM.md`. +## Engine size + +The engine wasm (`bin/blitz_web_bg.wasm`: Blitz with Stylo, Taffy and Parley, plus the +vendored font) is **9 015 481 bytes** (8.60 MiB; 2 497 009 bytes brotli) as CI builds it +from 0.1.0-canary.18. It is built with the crate's `wasm-release` profile, which is release +with fat LTO in one codegen unit, followed by `wasm-opt -Oz`. Up to canary.17 it used the +default release profile, and the published canary.17 wasm is 10 723 633 bytes. The perf +workloads run as fast as before or faster, and they produce byte-identical output +([`design/perf-baseline-2026-10-05.md`](design/perf-baseline-2026-10-05.md#the-wasm-build-profile-2026-10-17)). +`scripts/build-wasm.sh` fails when the wasm exceeds the manifest's `maxBytes` for it, +9 437 184 bytes (9 MiB). The app-wide budget, which counts the engine and every plugin, is +the editor's. + ## CI `vitest` (push, pull request) builds the engine wasm from the checkout and fails on a red