// How the app feels on a big vault. Not part of `bun run e2e` (it is slow): electron scripts/perf-large.cjs [notes=10000] const { app, BrowserWindow } = require('electron'); const path = require('path'); const os = require('os'); const fs = require('fs'); const N = Number(process.argv.find((a) => /^\d+$/.test(a))) || 10000; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ink-perf-')); const vault = path.join(tmp, 'Vault'); const words = 'alpha beta gamma delta project roadmap meeting notes idea review plan budget design draft summary weekly sync'.split(' '); const rnd = (n) => Math.floor(Math.random() * n); const sentence = () => Array.from({ length: 8 + rnd(10) }, () => words[rnd(words.length)]).join(' '); for (let i = 0; i < N; i++) { const folder = path.join(vault, `Area ${i % 40}`); fs.mkdirSync(folder, { recursive: true }); let body = `# Note ${i}\n\n`; for (let p = 0; p < 6; p++) body += sentence() + (rnd(3) === 0 ? ` [[Note ${rnd(N)}]]` : '') + '.\n\n'; fs.writeFileSync(path.join(folder, `Note ${i}.md`), `---\ntags: [t${i % 50}]\n---\n\n${body}`); } for (let a = 0; a < 40; a++) fs.writeFileSync(path.join(vault, `Area ${a}.md`), `Area ${a}\n`); // one very long page: headings, paragraphs, bullets, a table fs.writeFileSync(path.join(vault, 'Long.md'), Array.from({ length: 1500 }, (_, i) => `## Section ${i}\n\n${sentence()} **bold** and a [[Note ${i}]] link. ${sentence()}\n\n- ${sentence()}\n- ${sentence()}\n`).join('\n')); app.setPath('userData', tmp); fs.writeFileSync(path.join(tmp, 'settings.json'), JSON.stringify({ vault, lastPage: 'Area 1', theme: 'dark', bounds: { width: 1300, height: 850 } })); const started = Date.now(); require('../electron/main.cjs'); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); app.whenReady().then(async () => { let win; for (let i = 0; i < 200 && !(win = BrowserWindow.getAllWindows()[0]); i++) await sleep(50); const wc = win.webContents; if (wc.isLoading()) await new Promise((r) => wc.once('did-finish-load', r)); const js = (code) => wc.executeJavaScript(`(async () => { ${code} })()`); const report = (label, ms, extra = '') => console.log(`${label.padEnd(40)} ${String(Math.round(ms)).padStart(6)} ms ${extra}`); // frames the page could not draw: longest gap between animation frames over a period const jank = (ms) => js(`let last = performance.now(), worst = 0; const end = last + ${ms}; await new Promise((res) => { const f = (t) => { worst = Math.max(worst, t - last); last = t; t < end ? requestAnimationFrame(f) : res(); }; requestAnimationFrame(f); }); return worst`); const timeIn = (code) => js(`const t = performance.now(); ${code}; return performance.now() - t`); try { for (let i = 0; i < 300; i++) { if (await js(`return !!document.querySelector('.tree .row')`)) break; await sleep(100); } report('launch to sidebar shown', Date.now() - started); report('sidebar rows (folded)', 0, String(await js(`return document.querySelectorAll('.tree .row').length`))); // open every top-level folder const t0 = Date.now(); await js(`for (const r of document.querySelectorAll('.tree > .node > .row')) { r.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true })); }`); await sleep(200); report('open all 40 folders', Date.now() - t0, 'rows now: ' + (await js(`return document.querySelectorAll('.tree .row').length`))); report('worst frame gap after that (1s)', await jank(1000)); // open a page that has many backlinks / mentions const o = Date.now(); await js(`document.querySelector('.tree .row[data-id="Area 2/Note 2"]')?.click()`); for (let i = 0; i < 100; i++) { if ((await js(`return document.querySelector('.title')?.textContent`)) === 'Note 2') break; await sleep(20); } report('open a page', Date.now() - o); // a very long page const L = Date.now(); await js(`document.querySelector('.tree .row[data-id="Long"]')?.click()`); for (let i = 0; i < 300; i++) { if ((await js(`return document.querySelectorAll('.block').length`)) > 4000) break; await sleep(20); } report('open a 4500-block page', Date.now() - L); report('worst frame gap after that (1s)', await jank(1000)); report('scroll that page 20 steps', await js(`const sc = [...document.querySelectorAll('*')].find((e) => e.scrollHeight > e.clientHeight + 5000); if (!sc) return -1; const t = performance.now(); for (let i = 0; i < 20; i++) { sc.scrollTop += 1500; await new Promise((r) => requestAnimationFrame(r)); } return performance.now() - t`)); // search const s = Date.now(); await js(`document.querySelector('.sidebar button[aria-label="Search"]').click()`); await sleep(100); await js(`const i = document.querySelector('.pal-top input'); i.value = 'roadmap budget'; i.dispatchEvent(new Event('input', { bubbles: true }))`); for (let i = 0; i < 100; i++) { if (await js(`return document.querySelectorAll('.pal-list .pi').length > 3`)) break; await sleep(20); } report('search shows results', Date.now() - s); await js(`document.querySelector('.overlay')?.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))`); await sleep(400); // graph const g = Date.now(); await js(`document.querySelector('.sidebar button[aria-label="Graph view"]').click()`); for (let i = 0; i < 200; i++) { if (await js(`return !!document.querySelector('canvas, .graph svg, .gv')`)) break; await sleep(25); } report('graph view appears', Date.now() - g); report('worst frame gap while it settles (3s)', await jank(3000)); await js(`document.querySelector('.sidebar button[aria-label="Graph view"]').click()`); await sleep(500); // tasks list const k = Date.now(); await js(`document.querySelector('.sidebar button[aria-label="Tasks"]').click()`); for (let i = 0; i < 100; i++) { if (await js(`const p = document.querySelector('.tasks-panel'); return !!p && !/Loading/.test(p.textContent)`)) break; await sleep(20); } report('tasks dialog', Date.now() - k); } catch (err) { console.log('harness', err.stack); } fs.rmSync(tmp, { recursive: true, force: true }); app.exit(0); });