oxedyne/fe2o3/fe2o3_austenite/tools/bench/edit_latency.mjs
3.9 KiB, 1 run, executable
created by r1870400018:60935, 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 | #!/usr/bin/env node |
| 2 | // S0 incremental edit-latency bench: a scripted one-character edit to a |
| 3 | // paragraph, timed to the recompiled page -- Austenite's compileProjectDelta |
| 4 | // (changed-only) against typst.ts's real Daimond wiring, which is a full |
| 5 | // recompile to the `vector` format on every edit (see lib/wasm_common.mjs and |
| 6 | // tools/bench/README.md; Daimond does not call typst.ts's incr_compile/ |
| 7 | // IncrServer API anywhere, so timing that API here would not be the number a |
| 8 | // Daimond user feels). |
| 9 | // |
| 10 | // The compiler instance and, for Austenite, the `known`-id cache are kept live |
| 11 | // across every edit in this one process, matching how the real watch loop |
| 12 | // holds them for the life of a view. |
| 13 | // |
| 14 | // Usage: edit_latency.mjs --engine=austenite|typstts --vendor=DIR --doc=FILE |
| 15 | // [--edits=N] [--positions=N] |
| 16 | // Prints one JSON summary to stdout: { doc, engine, samples: [s...], p50_s, p95_s, ok }. |
| 17 | import fs from 'node:fs'; |
| 18 | import { loadEngine, timeOnce } from './lib/wasm_common.mjs'; |
| 19 | |
| 20 | function parseArgs(argv) { |
| 21 | const out = { edits: 10, positions: 5 }; |
| 22 | for (const a of argv.slice(2)) { |
| 23 | const m = a.match(/^--([^=]+)=(.*)$/); |
| 24 | if (!m) continue; |
| 25 | const [, k, v] = m; |
| 26 | out[k] = /^\d+$/.test(v) ? Number(v) : v; |
| 27 | } |
| 28 | return out; |
| 29 | } |
| 30 | |
| 31 | function percentile(sorted, p) { |
| 32 | if (!sorted.length) return null; |
| 33 | const idx = Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length)); |
| 34 | return sorted[idx]; |
| 35 | } |
| 36 | |
| 37 | async function main() { |
| 38 | const args = parseArgs(process.argv); |
| 39 | for (const req of ['engine', 'vendor', 'doc']) { |
| 40 | if (!args[req]) { |
| 41 | process.stderr.write(`edit_latency: --${req} is required\n`); |
| 42 | process.exit(2); |
| 43 | } |
| 44 | } |
| 45 | let text = fs.readFileSync(args.doc, 'utf8'); |
| 46 | const tokens = [...text.matchAll(/EDITTOK(\d+)/g)].map((m) => m[0]); |
| 47 | const uniqueTokens = [...new Set(tokens)]; |
| 48 | if (!uniqueTokens.length) { |
| 49 | process.stdout.write(JSON.stringify({ ok: false, error: 'no EDITTOK markers found; doc not from gen_synthetic.py' }) + '\n'); |
| 50 | process.exit(1); |
| 51 | } |
| 52 | const positions = []; |
| 53 | for (let i = 0; i < args.positions; i++) { |
| 54 | const idx = Math.floor((i / args.positions) * uniqueTokens.length); |
| 55 | positions.push(uniqueTokens[Math.min(idx, uniqueTokens.length - 1)]); |
| 56 | } |
| 57 | |
| 58 | const engine = await loadEngine(args.engine, args.vendor); |
| 59 | let known = []; |
| 60 | let editCounter = 0; |
| 61 | const samples = []; |
| 62 | let ok = true; |
| 63 | |
| 64 | // One initial full compile, untimed (the doc's first load, not an edit). |
| 65 | { |
| 66 | const project = { main: '/main.typ', sources: [['/main.typ', text]], known: [] }; |
| 67 | const ret = engine.compileDelta(project); |
| 68 | if (ret && Array.isArray(ret.order)) known = ret.order; |
| 69 | if (ret && ret.error) ok = false; |
| 70 | } |
| 71 | |
| 72 | for (let i = 0; i < args.edits; i++) { |
| 73 | const tok = positions[i % positions.length]; |
| 74 | editCounter += 1; |
| 75 | // A one-character edit: append a digit to the sentinel token, distinct |
| 76 | // each time, so the paragraph's text genuinely differs run to run. |
| 77 | const marker = `${tok}e${editCounter}`; |
| 78 | text = text.split(tok).join(marker); |
| 79 | // The token at this position now reads `marker`; later edits at the same |
| 80 | // position must search for that, not the original `EDITTOKn`. |
| 81 | positions[i % positions.length] = marker; |
| 82 | |
| 83 | const project = { main: '/main.typ', sources: [['/main.typ', text]], known }; |
| 84 | const { wall_s, ret } = timeOnce(() => engine.compileDelta(project)); |
| 85 | if (!ret || ret.error) { |
| 86 | ok = false; |
| 87 | process.stderr.write(`edit_latency: edit ${i} error: ${ret && ret.error}\n`); |
| 88 | } else if (Array.isArray(ret.order)) { |
| 89 | known = ret.order; |
| 90 | } |
| 91 | samples.push(wall_s); |
| 92 | } |
| 93 | |
| 94 | const sorted = [...samples].sort((a, b) => a - b); |
| 95 | process.stdout.write( |
| 96 | JSON.stringify({ |
| 97 | doc: args.doc, |
| 98 | engine: args.engine, |
| 99 | edits: args.edits, |
| 100 | positions: args.positions, |
| 101 | samples, |
| 102 | p50_s: percentile(sorted, 50), |
| 103 | p95_s: percentile(sorted, 95), |
| 104 | ok, |
| 105 | }) + '\n' |
| 106 | ); |
| 107 | } |
| 108 | |
| 109 | main().catch((e) => { |
| 110 | process.stdout.write(JSON.stringify({ ok: false, error: String((e && e.stack) || e) }) + '\n'); |
| 111 | process.exit(1); |
| 112 | }); |