Oregami
Repositories/oxedyne/fe2o3

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 }.
17import fs from 'node:fs';
18import { loadEngine, timeOnce } from './lib/wasm_common.mjs';
19
20function 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
31function 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
37async 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
109main().catch((e) => {
110 process.stdout.write(JSON.stringify({ ok: false, error: String((e && e.stack) || e) }) + '\n');
111 process.exit(1);
112});