Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_austenite/tools/bench/README.md

7.4 KiB, 1 run

created by r1870400018:60930, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1# S0: Austenite vs Typst speed bench
2
3The harness behind unit S0 of
4`~/usr/code/ai/claude/notes/austenite_typesetter_tail_plan_20260923.md`. It compares
5native Austenite against `typst compile` (`-j1` and `-j16`), and -- where Daimond's
6vendored wasm copies exist -- Austenite wasm against typst.ts wasm in node, including
7incremental edit latency. It writes a JSON report plus a markdown summary and refuses
8to present numbers taken while the host was loaded unless told to anyway.
9
10## Quick start
11
12```bash
13# 1. Build the austenite binary (only through this command -- see the top-level plan):
14flock ~/.cache/cargo-targets/claude-rc-3/evslot1.lock ~/usr/code/bash/rc-build 4G -- bash -c \
15 'cd ~/.cache/austenite-bench-wt && CARGO_BUILD_JOBS=4 \
16 CARGO_TARGET_DIR=~/.cache/cargo-targets/claude-rc-3/bench \
17 cargo build --release -p oxedyne_fe2o3_austenite --bin austenite'
18
19# 2. Run the harness (smoke mode by default -- small, fast, proves the plumbing):
20tools/bench/run_bench.sh \
21 --austenite-bin ~/.cache/cargo-targets/claude-rc-3/bench/release/austenite
22
23# 3. Read the report:
24cat tools/bench/out/<timestamp>/bench_report.md
25```
26
27For the real baseline, on an idle host:
28
29```bash
30tools/bench/run_bench.sh --austenite-bin <path> --full
31```
32
33`--full` runs the S0 protocol as specified: 2 warm-ups, 7 measured runs, the 300-page
34synthetic document, 50 edits at 5 positions. Smoke mode (the default) uses 1 warm-up, 3
35runs, a 20-page synthetic document and 6 edits at 2 positions -- enough to prove every
36leg of the harness runs and produces sane numbers, not enough to mean anything as a
37baseline.
38
39## What it measures, and how
40
41- **Engines**: `typst compile -j 16` (default), `typst compile -j 1`, native `austenite`,
42 Austenite wasm in node, and typst.ts wasm in node -- Daimond's real compile path.
43- **Corpora**: `fe2o3_austenite/samples/*.typ` plus one generated synthetic document
44 (`gen_synthetic.py`; seeded, no book content, no cetz). This is a narrower corpus than
45 the full plan's four-kind/three-size U12 set, which S0 does not depend on and did not
46 wait for -- see the brief this harness was built from.
47- **Protocol**: for each (doc, engine), `WARMUPS` untimed passes then `RUNS` measured
48 passes, run round-robin across every engine for that doc (engine A, B, C, A, B, C,
49 ...) rather than all of one engine then all of another, so host load hits every engine
50 equally over the course of the run. Each run is capped with `systemd-run --user
51 --scope --quiet -p MemoryMax=3G --slice=claude-rc.slice` and timed with
52 `/usr/bin/time -v`, which gives wall clock and peak RSS in one pass.
53- **Load flagging**: before and after every run, the harness reads `/proc/loadavg` (the
54 1-minute average) and `some avg10` from `/proc/pressure/cpu` and `/proc/pressure/
55 memory`. A run is flagged (`flagged_under_load: true`) when the load average was
56 above `BENCH_LOAD_THRESHOLD` (default 2) on either side. `aggregate.py` refuses to
57 print a report's numbers -- it writes a "REFUSED" json/md pair instead -- when any run
58 in the batch was flagged, unless `--force` is passed, in which case the report is
59 produced but headed "TAKEN UNDER LOAD" in bold, in the markdown and as `under_load:
60 true` in the JSON.
61- **Incremental edit latency**: `edit_latency.mjs` keeps one compiler instance alive
62 across a scripted run of one-character edits to five positions in the document (the
63 synthetic generator tags every paragraph with a stable `EDITTOKn` marker so a
64 position can be found and mutated by string search), timing each edit's recompile.
65 Austenite's leg calls `compileProjectDelta` with the `known`-id cache carried forward
66 edit to edit, exactly as the real watch loop does. **typst.ts's leg is a full
67 recompile to the `vector` format on every edit, not a call to typst.ts's
68 `incr_compile`/`IncrServer` API** -- checked against `www/js/typstwatch.js` and
69 `www/js/typst.js` in the Daimond app, neither of which calls that API anywhere; the
70 live-view path Daimond actually ships is `compileProjectVector`, a full recompile to
71 typst.ts's intermediate format, rendered to SVG by a second wasm module
72 (`typst_ts_renderer_bg.wasm`) through `render_svg(session, domElement)`, which needs a
73 live DOM. Timing the API Daimond does not call would not be the number a Daimond user
74 feels, so this harness times what Daimond actually does and excludes the DOM-render
75 step, which needs a browser; see "What is not measured" below.
76- **Phase attribution**: `austenite --timings` is a U9 addendum that has not landed
77 (checked by `grep -q -- '--timings' src/bin/austenite.rs` in `speed_bench.sh`, not by
78 probing the binary -- `austenite`'s argument parser treats an unrecognised flag as a
79 positional path, so guessing wrong would silently corrupt the invocation rather than
80 fail loudly). Until it lands, `speed_bench.sh` runs without it and says so on stderr;
81 once it lands, the same check turns it on with no harness change needed. Typst's own
82 `--timings` and a `perf record -g` cross-check are follow-on work this unit does not
83 attempt.
84- **Peak RSS**: `/usr/bin/time -v`'s "Maximum resident set size", per run for the native
85 and wasm-compile legs; for edit latency, once per whole edit session (a per-edit
86 figure inside one warm process is not a meaningful peak).
87
88## What is not measured
89
90- The final DOM-diff/SVG-string step of typst.ts's live view (`render_svg`), because it
91 needs an `HTMLElement`, i.e. a browser, not node. It runs on typst.ts's SIR output
92 after the compile this harness times, and by the numbers in
93 `www/js/typstwatch.js`'s own comments is the cheaper of the two steps, but it is not
94 zero and this harness does not claim to have measured it. A follow-on that drives a
95 real (headless) browser closes this gap.
96- Typst's own `--timings` trace and the `perf record -g` cross-check the plan's §S0
97 names for phase attribution.
98- Anything beyond `fe2o3_austenite/samples/*.typ` plus the one generated document --
99 no oxeweb TechSpec/Overview, no cetz examples, no 50/1000-page variants. Point
100 `--docs` at a wider glob once U12's corpora exist.
101
102## Files
103
104| File | Role |
105|---|---|
106| `run_bench.sh` | top-level driver: generates the synthetic doc, runs every leg, aggregates |
107| `speed_bench.sh` | native leg: typst -j16/-j1 vs austenite, interleaved, capped, timed |
108| `wasm_bench.mjs` / `wasm_bench_runner.sh` | wasm full-compile leg (node process per engine per doc, capped by the runner) |
109| `edit_latency.mjs` / `edit_latency_runner.sh` | incremental edit-latency leg |
110| `gen_synthetic.py` | deterministic synthetic `.typ` generator, any page count |
111| `aggregate.py` | merges the three legs' JSONL into `bench_report.json` + `bench_report.md`, and the load gate |
112| `lib/host.sh` | bash: `/proc/loadavg` + PSI reading, the capped-and-timed single-run helper |
113| `lib/wasm_common.mjs` | node: loads both vendored wasm compilers the way `www/js/typst.js` does, minus its use of `fetch` (Node does not resolve `file://` through `fetch`; confirmed on this host, Node v20.20.2 -- everything here reads with `fs` instead) |
114
115## Wasm legs are best-effort
116
117The wasm legs run only when Daimond's vendored copies are present:
118`~/usr/code/web/apps/oxedyne/daimond/www/vendor/austenite/` and `.../vendor/typst/`.
119Both are hand-shipped (not built by any `cargo`/`npm` step in this repo -- see the
120Daimond integration plan's decision on `vendor/austenite`), so their absence on a
121fresh checkout is normal, not an error: `run_bench.sh` says so on stderr and reports
122the native leg alone. Pass `--skip-wasm` to skip them even when present.