feedBack/scripts/perf-baseline.mjs
Byron Gamatos 950e348357
Some checks failed
ship-ci / ci (push) Has been cancelled
R0: module-migration rails (src/ serving, live-edit cache, scriptType loading, governance) (#812)
Host enablement for the plugin ES-module migration: sandboxed /api/plugins/{id}/src/ serving, no-cache+weak-ETag/304 live-edit caching on src/+screen.js+assets, scriptType:module loader injection + scriptType/minHost manifest passthrough; constitution v1.2.0 + module playbook + signed size-exemptions register + maintainer/CI-only ESLint gate; rerunnable perf-baseline harness. Reviewed by Codex (local), Copilot, and CodeRabbit.
2026-07-08 10:14:40 +02:00

95 lines
4.3 KiB
JavaScript

// 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<!-- generated by scripts/perf-baseline.mjs @ ${now} against ${BASE} (n=${N}, soak=${SOAK_S}s) -->\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);