oxedyne/daimond/dev/probe_typstloop.mjs
10.4 KiB, 1 run
created by r2519314175:99, 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_typstloop.mjs — why the live view rebuilds with nothing edited, and what |
| 2 | // the vendored packages cost the heap. |
| 3 | // |
| 4 | // Two reports to settle, on the author's own book: |
| 5 | // |
| 6 | // 1. the status bar cycles `Live` / `Rebuilding` every 1-2 s with nothing being |
| 7 | // written, so something the loop reads is changing when nothing has; |
| 8 | // 2. the compiler reached 2427 MB and the budget stopped the loop, against the |
| 9 | // 191 MB dev/TYPST_WATCH.md measured for the same book. |
| 10 | // |
| 11 | // So: seed the book, arm the watch through the shipped path, then SIT STILL and |
| 12 | // count rebuilds; and compile the same project over and over reading the heap after |
| 13 | // each, with the cetz packages live and with them stubbed out, which is the |
| 14 | // difference `7194f43` made. |
| 15 | // |
| 16 | // It asserts nothing and is not a verifier. |
| 17 | // |
| 18 | // eval "$(bash dev/world.sh 5 --env)" |
| 19 | // node dev/probe_typstloop.mjs # packages live |
| 20 | // node dev/probe_typstloop.mjs --stub # the two cetz imports removed |
| 21 | // node dev/probe_typstloop.mjs --idle # the idle-rebuild count only |
| 22 | import fs from 'node:fs'; |
| 23 | import path from 'node:path'; |
| 24 | import { open, scratch } from './harness.mjs'; |
| 25 | |
| 26 | const BOOK = process.env.HOME + '/usr/books/elearnity'; |
| 27 | if (!fs.existsSync(BOOK)) { console.log('no book at ' + BOOK); process.exit(0); } |
| 28 | const STUB = process.argv.includes('--stub'); |
| 29 | const IDLE = process.argv.includes('--idle'); |
| 30 | const REPS = (() => { const i = process.argv.indexOf('--reps'); |
| 31 | return i > 0 ? parseInt(process.argv[i + 1], 10) : 5; })(); |
| 32 | const CEIL = 1800; // MB of compiler heap at which this stops, to spare the box |
| 33 | |
| 34 | function collect(dir, prefix, keep, depth) { |
| 35 | const out = []; |
| 36 | for (const name of fs.readdirSync(path.join(BOOK, dir))) { |
| 37 | if (/^(archive|revision|audit|evaluations|plan|dev|cover|ref|__pycache__)/i.test(name)) continue; |
| 38 | const abs = path.join(BOOK, dir, name); |
| 39 | const rel = prefix + '/' + name; |
| 40 | let st; |
| 41 | try { st = fs.statSync(abs); } catch (e) { continue; } |
| 42 | if (st.isDirectory()) { |
| 43 | if (depth > 0) out.push(...collect(dir + '/' + name, rel, keep, depth - 1)); |
| 44 | continue; |
| 45 | } |
| 46 | if (!keep(name)) continue; |
| 47 | const buf = fs.readFileSync(abs); |
| 48 | out.push([rel, /\.(typ|svg|bib|csv|json|yaml|yml|toml|xml|txt)$/i.test(name) |
| 49 | ? buf.toString('utf8') : Array.from(buf)]); |
| 50 | } |
| 51 | return out; |
| 52 | } |
| 53 | const typOnly = (n) => /\.typ$/i.test(n); |
| 54 | const seed = [ |
| 55 | ...collect('CheapThinking', 'elearnity/CheapThinking', typOnly, 0), |
| 56 | ...collect('style', 'elearnity/style', typOnly, 2), |
| 57 | ...collect('assets/svg', 'elearnity/assets/svg', (n) => /\.svg$/i.test(n), 1), |
| 58 | ...collect('assets/thinking', 'elearnity/assets/thinking', (n) => /\.svg$/i.test(n), 1), |
| 59 | ...collect('assets/fonts', 'elearnity/assets/fonts', (n) => /\.(ttf|otf|ttc|otc)$/i.test(n), 2), |
| 60 | ['elearnity/terms.typ', fs.readFileSync(path.join(BOOK, 'terms.typ'), 'utf8')], |
| 61 | ]; |
| 62 | console.log(`seeding ${seed.length} files; packages ${STUB ? 'STUBBED OUT' : 'LIVE'}`); |
| 63 | |
| 64 | const MAIN = 'elearnity/CheapThinking/thinking.typ'; |
| 65 | const s = await open({ name: 'typstloop' }); |
| 66 | const p = s.page; |
| 67 | await p.setViewportSize({ width: 1700, height: 1050 }); |
| 68 | await p.waitForTimeout(1500); |
| 69 | await p.evaluate(async () => { |
| 70 | const m = await import('/pkg/oxedyne_daimond.js'); |
| 71 | m.set_locked_packs(''); |
| 72 | m.set_account_ns('d~typstloop'); |
| 73 | }); |
| 74 | for (let i = 0; i < seed.length; i += 20) { |
| 75 | await p.evaluate(async (files) => { |
| 76 | const m = await import('/pkg/oxedyne_daimond.js'); |
| 77 | const app = new m.DaimondApp('http://127.0.0.1/v1/chat/completions', '', 'none', 256, '', true); |
| 78 | for (const f of files) { |
| 79 | if (typeof f[1] === 'string') await m.write_file(f[0], f[1]); |
| 80 | else await app.write_bytes(f[0], new Uint8Array(f[1])); |
| 81 | } |
| 82 | }, seed.slice(i, i + 20)); |
| 83 | } |
| 84 | if (STUB) { |
| 85 | await p.evaluate(async () => { |
| 86 | const m = await import('/pkg/oxedyne_daimond.js'); |
| 87 | let t = await m.read_file('elearnity/style/template/book_template.typ'); |
| 88 | t = t.split('#import "@preview/cetz:0.3.4"').join('// cetz removed') |
| 89 | .split('#import "@preview/cetz-plot:0.1.1": plot, chart').join('// cetz-plot removed'); |
| 90 | await m.write_file('elearnity/style/template/book_template.typ', t); |
| 91 | await m.write_file('elearnity/CheapThinking/thinking_chap_02_figures.typ', |
| 92 | '#let fig-ceilings = rect(width: 100%, height: 6cm)\n' |
| 93 | + '#let fig-thinking-time = rect(width: 100%, height: 6cm)\n'); |
| 94 | }); |
| 95 | } |
| 96 | const sleep = (ms) => new Promise(r => setTimeout(r, ms)); |
| 97 | |
| 98 | // ── What one compile hands back, over and over ────────────────────── |
| 99 | console.log('\n=== the same project, compiled repeatedly, nothing edited ==='); |
| 100 | let prevWatch = null; |
| 101 | for (let i = 0; i < REPS; i++) { |
| 102 | const r = await p.evaluate(async (main) => { |
| 103 | const m = await import('/pkg/oxedyne_daimond.js'); |
| 104 | const before = window.DaimondTypst.heapMB(); |
| 105 | const t = performance.now(); |
| 106 | const out = await m.typst_compile_project_vector(main); |
| 107 | const ms = performance.now() - t; |
| 108 | const stamps = out.watch ? Array.from(await m.typst_watch_stamps(Array.from(out.watch))) : []; |
| 109 | return { ms, before, after: window.DaimondTypst.heapMB(), |
| 110 | watch: out.watch ? Array.from(out.watch) : [], |
| 111 | stamps, |
| 112 | bytes: out.vector ? out.vector.length : 0, |
| 113 | error: out.error ? String(out.error).slice(0, 300) : '' }; |
| 114 | }, MAIN); |
| 115 | const w = r.watch; |
| 116 | let drift = ''; |
| 117 | if (prevWatch) { |
| 118 | if (prevWatch.length !== w.length) drift = ` WATCH LENGTH ${prevWatch.length} → ${w.length}`; |
| 119 | else { |
| 120 | const n = w.filter((x, k) => x !== prevWatch[k]).length; |
| 121 | if (n) drift = ` WATCH ORDER MOVED at ${n} of ${w.length} positions`; |
| 122 | } |
| 123 | } |
| 124 | prevWatch = w; |
| 125 | console.log(` ${i + 1}: ${r.ms.toFixed(0)} ms, heap ${r.before.toFixed(0)} → ${r.after.toFixed(0)} MB` |
| 126 | + ` (+${(r.after - r.before).toFixed(0)}), ${(r.bytes / 1048576).toFixed(1)} MB vector,` |
| 127 | + ` watch ${w.length}${drift}${r.error ? ' REFUSED: ' + r.error : ''}`); |
| 128 | if (r.error) break; |
| 129 | if (r.after > CEIL) { console.log(` stopping: past the ${CEIL} MB self-imposed ceiling`); break; } |
| 130 | } |
| 131 | |
| 132 | // ── The stamps, asked twice with nothing between ──────────────────── |
| 133 | console.log('\n=== the watch stamps, asked twice, nothing written ==='); |
| 134 | const twice = await p.evaluate(async (main) => { |
| 135 | const m = await import('/pkg/oxedyne_daimond.js'); |
| 136 | const out = await m.typst_compile_project_vector(main); |
| 137 | const files = Array.from(out.watch || []); |
| 138 | const a = Array.from(await m.typst_watch_stamps(files)); |
| 139 | await new Promise(r => setTimeout(r, 1200)); |
| 140 | const b = Array.from(await m.typst_watch_stamps(files)); |
| 141 | const moved = []; |
| 142 | for (let i = 0; i < files.length; i++) if (a[i] !== b[i]) moved.push([files[i], a[i], b[i]]); |
| 143 | return { n: files.length, moved: moved.slice(0, 10), total: moved.length }; |
| 144 | }, MAIN); |
| 145 | console.log(` ${twice.n} files; ${twice.total} changed with nothing written`); |
| 146 | for (const [f, a, b] of twice.moved) console.log(` ${f}: ${a} → ${b}`); |
| 147 | |
| 148 | // ── What the heap costs per REAL edit, which is the wall's own slope ─ |
| 149 | // |
| 150 | // A rebuild on unchanged bytes is free: comemo hashes the text and hands back the |
| 151 | // layout it already has. An edit is not, and the heap NEVER SHRINKS, so the figure |
| 152 | // below is the one that decides how many edits a session gets before the budget |
| 153 | // stops it. 2427 MB divided by it is how long the author had. |
| 154 | if (process.argv.includes('--edits')) { |
| 155 | console.log('\n=== the heap, one real edit at a time ==='); |
| 156 | const CHAP = 'elearnity/CheapThinking/thinking_chap_07.typ'; |
| 157 | for (let i = 0; i < 15; i++) { |
| 158 | const r = await p.evaluate(async ({ main, chap, word }) => { |
| 159 | const m = await import('/pkg/oxedyne_daimond.js'); |
| 160 | const t = await m.read_file(chap); |
| 161 | await m.write_file(chap, t.replace(/\n([A-Z][a-z]+ )/, '\n$1' + word + ' ')); |
| 162 | const before = window.DaimondTypst.heapMB(); |
| 163 | const t0 = performance.now(); |
| 164 | const out = await m.typst_compile_project_vector(main); |
| 165 | return { ms: performance.now() - t0, before, after: window.DaimondTypst.heapMB(), |
| 166 | error: out.error ? String(out.error).slice(0, 200) : '' }; |
| 167 | }, { main: MAIN, chap: CHAP, word: 'probe' + i }); |
| 168 | console.log(` edit ${i + 1}: ${r.ms.toFixed(0)} ms, heap ${r.before.toFixed(0)}` |
| 169 | + ` → ${r.after.toFixed(0)} MB (+${(r.after - r.before).toFixed(0)})` |
| 170 | + (r.error ? ' REFUSED: ' + r.error : '')); |
| 171 | if (r.after > CEIL) { console.log(' stopping: past the ceiling'); break; } |
| 172 | } |
| 173 | } |
| 174 | |
| 175 | // ── The shipped loop, armed, and then left alone ──────────────────── |
| 176 | if (IDLE || true) { |
| 177 | console.log('\n=== the loop, armed through the button, then left alone ==='); |
| 178 | await p.evaluate(() => window.DaimondDoc.show('elearnity/CheapThinking/thinking.typ')); |
| 179 | await p.waitForTimeout(1200); |
| 180 | const st = () => p.evaluate(() => window.DaimondTypstWatch.state()); |
| 181 | await p.click('[data-act="compile"]', { force: true }); |
| 182 | for (let i = 0; i < 240 && !(await st()).drawn; i++) await sleep(500); |
| 183 | const a = await st(); |
| 184 | console.log(` armed: ${a.pages} pages, ${a.files} watched, heap ${a.heap.toFixed(0)} MB,` |
| 185 | + ` builds ${a.builds}, drawn ${a.drawn}, failed ${a.failed}`); |
| 186 | for (let i = 0; i < 8; i++) { |
| 187 | await sleep(2500); |
| 188 | const n = await st(); |
| 189 | console.log(` +${((i + 1) * 2.5).toFixed(1)}s mode ${n.mode} building ${n.building}` |
| 190 | + ` builds ${n.builds} drawn ${n.drawn} failed ${n.failed}` |
| 191 | + ` heap ${n.heap.toFixed(0)} MB rheap ${n.rheap.toFixed(0)} MB` |
| 192 | + (n.error ? ' err: ' + n.error.slice(0, 90) : '')); |
| 193 | if (n.heap > CEIL) { console.log(' stopping: past the ceiling'); break; } |
| 194 | } |
| 195 | } |
| 196 | |
| 197 | // ── And what the reader actually sees, on the pages that were wrong ── |
| 198 | // |
| 199 | // The three rendering faults were reported on the CONTENTS page and the copyright |
| 200 | // page of this book, which is where a `#link` per line and three inline ones are. |
| 201 | // So those are the pages photographed, rather than a fixture that has neither. |
| 202 | { |
| 203 | const shots = process.argv.includes('--shot') |
| 204 | ? process.argv[process.argv.indexOf('--shot') + 1].split(',').map(Number) |
| 205 | : [2, 5]; |
| 206 | const WORK = scratch('typstloop'); |
| 207 | fs.mkdirSync(WORK, { recursive: true }); |
| 208 | for (const n of shots) { |
| 209 | const r = await p.evaluate((n) => { |
| 210 | window.DaimondTypstWatch.goToPage(n); |
| 211 | const sc = document.querySelector('#typst-live .tl-scroll').getBoundingClientRect(); |
| 212 | return { x: sc.x, y: sc.y, w: sc.width, h: sc.height }; |
| 213 | }, n); |
| 214 | await sleep(900); |
| 215 | const f = `${WORK}/page-${n}.png`; |
| 216 | await p.screenshot({ path: f, clip: { x: Math.floor(r.x), y: Math.floor(r.y), |
| 217 | width: Math.floor(r.w), height: Math.floor(Math.min(r.h, 900)) } }); |
| 218 | console.log(' wrote ' + f); |
| 219 | } |
| 220 | } |
| 221 | |
| 222 | await p.evaluate(async () => { |
| 223 | const m = await import('/pkg/oxedyne_daimond.js'); |
| 224 | m.set_account_ns(''); |
| 225 | }); |
| 226 | await s.close(); |