// Perf-baseline harness for the module-migration refactor (R0). // // Rerun this after every phase (R0 → R3c) to prove the split does not regress // screen-entry, frame-time, memory, or server latency. It writes a markdown // results block to stdout; paste it into docs/perf-baseline.md (or redirect). // // Usage: // node scripts/perf-baseline.mjs --base http://127.0.0.1:8000 [--n 60] [--soak 30] // // Maintainer/CI-only dev tooling (uses the committed @playwright/test browser); // never part of the serve or Docker path. Metrics that need a seeded library // with charts (playback frame-time, screen-entry into a live highway) are // clearly labelled — run those against an environment with real songs. import { createRequire } from 'module'; const require = createRequire(import.meta.url); const { chromium } = require('@playwright/test'); const args = new Map(); for (let i = 2; i < process.argv.length; i += 2) args.set(process.argv[i].replace(/^--/, ''), process.argv[i + 1]); const BASE = args.get('base') || 'http://127.0.0.1:8000'; const N = parseInt(args.get('n') || '60', 10); const SOAK_S = parseInt(args.get('soak') || '30', 10); const pct = (xs, p) => { if (!xs.length) return null; const s = [...xs].sort((a, b) => a - b); return s[Math.min(s.length - 1, Math.floor((p / 100) * s.length))]; }; const ms = (x) => (x == null ? '—' : `${x.toFixed(1)}`); // ── Server latency: p50/p95/p99 over N requests per endpoint ────────────────── async function serverLatency(paths) { const rows = []; for (const path of paths) { const t = []; let status = 0; for (let i = 0; i < N; i++) { const t0 = performance.now(); try { const r = await fetch(BASE + path); status = r.status; await r.arrayBuffer(); } catch { status = -1; } t.push(performance.now() - t0); } rows.push({ path, status, p50: pct(t, 50), p95: pct(t, 95), p99: pct(t, 99) }); } return rows; } // ── Client: cold boot-to-interactive + idle memory after a soak ─────────────── async function clientMetrics() { const browser = await chromium.launch(); const page = await browser.newPage(); const t0 = Date.now(); await page.goto(BASE, { waitUntil: 'networkidle', timeout: 60000 }); const bootMs = Date.now() - t0; // performance.memory is Chromium-only; JS heap after settle. const mem0 = await page.evaluate(() => (performance.memory ? performance.memory.usedJSHeapSize : null)); await page.waitForTimeout(SOAK_S * 1000); const mem1 = await page.evaluate(() => (performance.memory ? performance.memory.usedJSHeapSize : null)); const scripts = await page.evaluate(() => document.querySelectorAll('script[data-plugin-id]').length); await browser.close(); return { bootMs, memStartMB: mem0 && mem0 / 1048576, memSoakMB: mem1 && mem1 / 1048576, scripts }; } const server = await serverLatency([ '/api/version', '/api/plugins', '/api/library?limit=60', '/api/library/artists', ]); const client = await clientMetrics(); const now = new Date().toISOString(); let out = `\n\n\n`; out += `### Server latency (ms)\n\n| Endpoint | status | p50 | p95 | p99 |\n|---|---|---|---|---|\n`; for (const r of server) out += `| \`${r.path}\` | ${r.status} | ${ms(r.p50)} | ${ms(r.p95)} | ${ms(r.p99)} |\n`; out += `\n### Client\n\n| Metric | Value |\n|---|---|\n`; out += `| Cold boot → networkidle | ${client.bootMs} ms |\n`; out += `| JS heap after load | ${client.memStartMB ? client.memStartMB.toFixed(1) + ' MB' : '—'} |\n`; out += `| JS heap after ${SOAK_S}s idle soak | ${client.memSoakMB ? client.memSoakMB.toFixed(1) + ' MB' : '—'} |\n`; out += `| Plugin scripts injected | ${client.scripts} |\n`; out += `\n> **Requires a seeded library** (not captured by this run): playback frame-time p95\n`; out += `> on the 2D + 3D highway, and screen-entry (plugin inject → interactive) for\n`; out += `> editor / notedetect / highway_3d with a real chart loaded. Run this harness\n`; out += `> against an environment with charts in \`DLC_DIR\` to fill those in.\n`; console.log(out);