Skip to content
This repository was archived by the owner on Oct 20, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/vitest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
8 changes: 5 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
33 changes: 33 additions & 0 deletions docs/design/perf-baseline-2026-10-05.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
13 changes: 13 additions & 0 deletions docs/status.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/web-bundle/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"name": "blitz",
"path": "bin/blitz_web_bg.wasm",
"purpose": "engine",
"maxBytes": 67108864
"maxBytes": 9437184
}
],
"network": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/web-bundle/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
11 changes: 11 additions & 0 deletions packages/web-render/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 4 additions & 2 deletions scripts/build-wasm-perf.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
31 changes: 22 additions & 9 deletions scripts/build-wasm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Loading