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)" |
| 19 | import fs from 'node:fs'; |
| 20 | import path from 'node:path'; |
| 21 | import { fileURLToPath } from 'node:url'; |
| 22 | import { open } from './harness.mjs'; |
| 23 | |
| 24 | const HERE = path.dirname(fileURLToPath(import.meta.url)); |
| 25 | const WWW = path.join(HERE, '..', 'www'); |
| 26 | const NOGUARD = process.argv.includes('--noguard'); |
| 27 | |
| 28 | const SRC = path.join(WWW, 'js', 'typstwatch.js'); |
| 29 | let 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. |
| 37 | const BIG = process.argv.includes('--big'); |
| 38 | const anchor = '\t\tfor (let i = 0; i < S.files.length && i < got.length; i++) out[S.files[i]] = got[i];'; |
| 39 | if (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. |
| 47 | body = 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 | |
| 52 | if (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. |
| 64 | const 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 | } }); |
| 68 | const p = s.page; |
| 69 | await p.setViewportSize({ width: 1500, height: 950 }); |
| 70 | await p.waitForTimeout(1200); |
| 71 | await 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 | }); |
| 77 | await p.evaluate(() => window.DaimondDoc.show('spinprobe/main.typ')); |
| 78 | await p.waitForTimeout(1000); |
| 79 | await p.click('[data-act="compile"]', { force: true }); |
| 80 | |
| 81 | const st = () => p.evaluate(() => window.DaimondTypstWatch.state()); |
| 82 | const sleep = (ms) => new Promise(r => setTimeout(r, ms)); |
| 83 | console.log(NOGUARD ? '=== the guard defeated ===' : '=== the guard in place ==='); |
| 84 | for (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 | } |
| 95 | await s.close(); |