Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/probe_typstspin.mjs

4.4 KiB, 1 run

created by r2519314175:109, 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// probe_typstspin.mjs — the guard against a rebuild loop, proved on code that spins.
2//
3// The author reported the status bar cycling `Live` / `Rebuilding` every 1-2 s with
4// nothing being edited, and the compiler at 2427 MB by the end of it. A rebuild on
5// unchanged bytes is free, so a spin costs nothing until the bytes really do differ
6// — and then about 10 MB a time (`dev/probe_typstloop.mjs`), which is why an
7// unattended spin walks into the heap ceiling and why the loop has to notice.
8//
9// This serves a DELIBERATELY BROKEN `typstwatch.js` in which the poll always reports
10// a change, which is the fault whatever its cause, and asks what the loop does. Two
11// runs, and the pair is the point:
12//
13// node dev/probe_typstspin.mjs # the guard in place: it stops and says why
14// node dev/probe_typstspin.mjs --noguard # the guard defeated: it spins forever
15//
16// It asserts nothing and is not a verifier.
17//
18// eval "$(bash dev/world.sh 5 --env)"
19import fs from 'node:fs';
20import path from 'node:path';
21import { fileURLToPath } from 'node:url';
22import { open } from './harness.mjs';
23
24const HERE = path.dirname(fileURLToPath(import.meta.url));
25const WWW = path.join(HERE, '..', 'www');
26const NOGUARD = process.argv.includes('--noguard');
27
28const SRC = path.join(WWW, 'js', 'typstwatch.js');
29let body = fs.readFileSync(SRC, 'utf8');
30
31// The fault: every poll finds one watched file changed, whatever it really says.
32//
33// `--big` makes it claim a size past `DIGEST_MAX` as well, which is the one case
34// the contents cannot settle — the file is too large to read back — and therefore
35// the case the count-and-hold backstop is for. Without it the file is small, and
36// what stops the loop is the digest rather than the backstop.
37const BIG = process.argv.includes('--big');
38const anchor = '\t\tfor (let i = 0; i < S.files.length && i < got.length; i++) out[S.files[i]] = got[i];';
39if (body.split(anchor).length - 1 !== 1) {
40 console.error('the anchor for the fault is not there exactly once — nothing would be proved');
41 process.exit(2);
42}
43// The TIME moves and the size is left alone, which is what a file whose stamp
44// lies looks like from here — a fake length would be caught by the byte read
45// rather than by the digest, and would prove the wrong thing. `--big` fakes the
46// length on purpose, because that IS the case the digest cannot settle.
47body = body.replace(anchor, anchor + (BIG
48 ? '\n\t\tif (S.files.length) out[S.files[0]] = String(Date.now()) + \':99999999\';'
49 : '\n\t\tif (S.files.length) out[S.files[0]] = String(Date.now()) + \':\''
50 + ' + String(got[0]).split(\':\')[1];'));
51
52if (NOGUARD) {
53 for (const g of ['\tif (S.mode === \'live\' && S.same >= SPIN_MAX) {',
54 '\t\tif (await confirmed(p, now[p])) continue;']) {
55 if (body.split(g).length - 1 !== 1) { console.error('nothing to defeat: ' + g); process.exit(2); }
56 }
57 body = body
58 .replace('\tif (S.mode === \'live\' && S.same >= SPIN_MAX) {', '\tif (false) {')
59 .replace('\t\tif (await confirmed(p, now[p])) continue;', '\t\tif (false) continue;');
60}
61
62// BEFORE `goto`, or the page has already fetched the real file and the run proves
63// nothing — which is what the harness's `route` option exists for.
64const s = await open({ name: 'typstspin', route: async (page) => {
65 await page.route('**/js/typstwatch.js', r => r.fulfill({
66 status: 200, contentType: 'application/javascript', body }));
67} });
68const p = s.page;
69await p.setViewportSize({ width: 1500, height: 950 });
70await p.waitForTimeout(1200);
71await p.evaluate(async () => {
72 const m = await import('/pkg/oxedyne_daimond.js');
73 m.set_locked_packs('');
74 await m.write_file('spinprobe/main.typ',
75 '#set page(width: 300pt, height: 220pt)\n= A page\nSome text on it.\n');
76});
77await p.evaluate(() => window.DaimondDoc.show('spinprobe/main.typ'));
78await p.waitForTimeout(1000);
79await p.click('[data-act="compile"]', { force: true });
80
81const st = () => p.evaluate(() => window.DaimondTypstWatch.state());
82const sleep = (ms) => new Promise(r => setTimeout(r, ms));
83console.log(NOGUARD ? '=== the guard defeated ===' : '=== the guard in place ===');
84for (let i = 0; i < 12; i++) {
85 await sleep(2000);
86 const n = await st();
87 console.log(` +${(i + 1) * 2}s mode ${n.mode} builds ${n.builds} drawn ${n.drawn}`
88 + ` same ${n.same} heap ${n.heap.toFixed(0)} MB why "${n.why}"`);
89 if (n.mode === 'held') {
90 console.log(' it stopped, and what it says is:\n '
91 + n.reason.replace(/\s+/g, ' '));
92 break;
93 }
94}
95await s.close();