Files
pbakaus_impeccable/scripts/benchmark-live-init.mjs
T
Paul BakausandClaude 917d3afcf2 Route the last two benchmark scripts through the shared argv parser
Follow-up on review feedback. The previous commit consolidated four of the six
Live benchmark parsers and left these two on their own hand-rolled `arg()`,
which was the inconsistency the first pass was meant to remove.

- benchmark-live-control.mjs and benchmark-live-init.mjs parsed --iterations
  with Number(), so a non-numeric value became NaN and `index < NaN` ran the
  benchmark zero times before failing on the metrics file. They also accepted
  only the space-separated form, so --iterations=20 silently measured the
  default. Both now use parseArgs + positiveIntFlag, which throws on a value
  that was clearly meant as a number.
- benchmark-live-control.mjs read the metrics file with no handling for the case
  where the run produced nothing: a missing file surfaced as a raw ENOENT stack
  and a malformed line as a bare SyntaxError. Report both with a diagnostic
  naming the file and the env var that populates it.
- summarize() now reports a `samples` count and nulls instead of letting
  percentile() read past an empty array, where the NaN serialized to null and a
  report of nothing measured looked like a real measurement.

Prepared with AI assistance under maintainer direction.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-17 14:13:32 -07:00

101 lines
3.0 KiB
JavaScript

#!/usr/bin/env node
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseArgs, positiveIntFlag } from './lib/cli-args.mjs';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const liveScript = path.join(root, 'skill/scripts/live.mjs');
const serverScript = path.join(root, 'skill/scripts/live-server.mjs');
const args = parseArgs(process.argv.slice(2));
const iterations = positiveIntFlag(args.iterations, 10);
const fixture = args.fixture ? String(args.fixture) : 'vite8-react-plain';
const fixtureDir = path.join(root, 'tests/framework-fixtures', fixture, 'files');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-live-init-'));
try {
fs.cpSync(fixtureDir, tmp, { recursive: true });
fs.writeFileSync(path.join(tmp, 'PRODUCT.md'), '# Product\n\nA realistic Live initialization benchmark fixture.\n');
fs.writeFileSync(path.join(tmp, 'DESIGN.md'), '# Design\n\nUse the fixture\'s existing type, color, and component system.\n');
fs.mkdirSync(path.join(tmp, '.impeccable/live'), { recursive: true });
fs.writeFileSync(path.join(tmp, '.impeccable/live/config.json'), JSON.stringify({
files: ['index.html'],
insertBefore: '</body>',
commentSyntax: 'html',
cspChecked: true,
}, null, 2) + '\n');
const cold = [];
for (let i = 0; i < iterations; i += 1) {
stop();
cold.push(runLive());
}
stop();
runLive();
const warm = [];
for (let i = 0; i < iterations; i += 1) warm.push(runLive());
console.log(JSON.stringify({
fixture,
iterations,
measuredAt: new Date().toISOString(),
cold: summarize(cold),
warm: summarize(warm),
samples: { cold, warm },
}, null, 2));
} finally {
stop();
fs.rmSync(tmp, { recursive: true, force: true });
}
function runLive() {
const start = performance.now();
const stdout = execFileSync(process.execPath, [liveScript], {
cwd: tmp,
encoding: 'utf-8',
timeout: 15_000,
});
const elapsed = performance.now() - start;
const result = JSON.parse(stdout);
if (!result.ok) throw new Error('live init failed: ' + stdout);
return round(elapsed);
}
function stop() {
try {
execFileSync(process.execPath, [serverScript, 'stop'], {
cwd: tmp,
stdio: 'ignore',
timeout: 5_000,
});
} catch {}
}
function summarize(samples) {
const sorted = [...samples].sort((a, b) => a - b);
return {
medianMs: percentile(sorted, 0.5),
p95Ms: percentile(sorted, 0.95),
minMs: sorted[0],
maxMs: sorted.at(-1),
};
}
function percentile(sorted, value) {
if (sorted.length === 1) return sorted[0];
const index = (sorted.length - 1) * value;
const lower = Math.floor(index);
const upper = Math.ceil(index);
const weight = index - lower;
return round(sorted[lower] * (1 - weight) + sorted[upper] * weight);
}
function round(value) {
return Math.round(value * 100) / 100;
}