import fs from 'node:fs'; import path from 'node:path'; import process from 'node:process'; import { fileURLToPath } from 'node:url'; const defaultThresholds = { errors: 0, errorRate: 0.001, p95Ms: 300, p99Ms: 800, concurrency: 30, durationSeconds: 120, }; function parseArgs(argv) { const options = { input: '', output: '', json: false, quiet: false, thresholds: { ...defaultThresholds }, }; for (let index = 2; index < argv.length; index += 1) { const arg = argv[index]; if (arg === '--json') options.json = true; else if (arg === '--quiet') options.quiet = true; else if (arg === '--input') { options.input = argv[index + 1] || ''; index += 1; } else if (arg.startsWith('--input=')) { options.input = arg.slice('--input='.length); } else if (arg === '--output') { options.output = argv[index + 1] || ''; index += 1; } else if (arg.startsWith('--output=')) { options.output = arg.slice('--output='.length); } else if (arg === '--max-p95-ms') { options.thresholds.p95Ms = numberArg(argv[index + 1], options.thresholds.p95Ms); index += 1; } else if (arg.startsWith('--max-p95-ms=')) { options.thresholds.p95Ms = numberArg(arg.slice('--max-p95-ms='.length), options.thresholds.p95Ms); } else if (arg === '--max-p99-ms') { options.thresholds.p99Ms = numberArg(argv[index + 1], options.thresholds.p99Ms); index += 1; } else if (arg.startsWith('--max-p99-ms=')) { options.thresholds.p99Ms = numberArg(arg.slice('--max-p99-ms='.length), options.thresholds.p99Ms); } else if (arg === '--max-error-rate') { options.thresholds.errorRate = numberArg(argv[index + 1], options.thresholds.errorRate); index += 1; } else if (arg.startsWith('--max-error-rate=')) { options.thresholds.errorRate = numberArg(arg.slice('--max-error-rate='.length), options.thresholds.errorRate); } else if (arg === '--min-concurrency') { options.thresholds.concurrency = numberArg(argv[index + 1], options.thresholds.concurrency); index += 1; } else if (arg.startsWith('--min-concurrency=')) { options.thresholds.concurrency = numberArg(arg.slice('--min-concurrency='.length), options.thresholds.concurrency); } else if (arg === '--min-duration-seconds') { options.thresholds.durationSeconds = numberArg(argv[index + 1], options.thresholds.durationSeconds); index += 1; } else if (arg.startsWith('--min-duration-seconds=')) { options.thresholds.durationSeconds = numberArg(arg.slice('--min-duration-seconds='.length), options.thresholds.durationSeconds); } else if (!arg.startsWith('-') && !options.input) { options.input = arg; } } return options; } function numberArg(value, fallback) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : fallback; } function readJson(filePath) { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } function numberValue(value, fallback = 0) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : fallback; } function booleanValue(value) { return value === true || value === 'true'; } function summarizeBenchmark(report) { const summary = report.summary || {}; const latencyOk = summary.latencyOk || {}; const config = report.config || {}; return { errors: numberValue(summary.errors), errorRate: numberValue(summary.errorRate), p95Ms: numberValue(latencyOk.p95Ms), p99Ms: numberValue(latencyOk.p99Ms), concurrency: numberValue(config.concurrency), durationSeconds: numberValue(config.durationSeconds), includeWrites: booleanValue(config.includeWrites), requests: numberValue(summary.requests), ok: numberValue(summary.ok), throughputRps: numberValue(summary.throughputRps), apiBase: report.apiBase || '', startedAt: report.startedAt || '', finishedAt: report.finishedAt || '', }; } function evaluateSummary(summary, thresholds = defaultThresholds) { const failures = []; if (summary.errors !== thresholds.errors) failures.push(`errors expected ${thresholds.errors} but got ${summary.errors}`); if (summary.errorRate > thresholds.errorRate) failures.push(`errorRate expected <= ${thresholds.errorRate} but got ${summary.errorRate}`); if (summary.p95Ms > thresholds.p95Ms) failures.push(`p95Ms expected <= ${thresholds.p95Ms} but got ${summary.p95Ms}`); if (summary.p99Ms > thresholds.p99Ms) failures.push(`p99Ms expected <= ${thresholds.p99Ms} but got ${summary.p99Ms}`); if (summary.concurrency < thresholds.concurrency) failures.push(`concurrency expected >= ${thresholds.concurrency} but got ${summary.concurrency}`); if (summary.durationSeconds < thresholds.durationSeconds) failures.push(`durationSeconds expected >= ${thresholds.durationSeconds} but got ${summary.durationSeconds}`); if (summary.includeWrites !== false) failures.push('includeWrites expected false'); return { status: failures.length ? 'fail' : 'pass', failures, summary, thresholds, }; } function launchGateCheck(evaluation) { return { id: 'performance.api-real-data-read', status: evaluation.status, command: 'npm run perf:api:local', completedAt: new Date().toISOString(), artifact: '', summary: { errors: evaluation.summary.errors, errorRate: evaluation.summary.errorRate, p95Ms: evaluation.summary.p95Ms, p99Ms: evaluation.summary.p99Ms, concurrency: evaluation.summary.concurrency, durationSeconds: evaluation.summary.durationSeconds, includeWrites: evaluation.summary.includeWrites, }, }; } async function main() { const options = parseArgs(process.argv); if (!options.input) { console.error('Usage: node scripts/performance-summary.js --input docs/refactor/performance-reports/api-benchmark-xxx.json [--json]'); process.exitCode = 1; return; } const inputPath = path.resolve(process.cwd(), options.input); const report = readJson(inputPath); const summary = summarizeBenchmark(report); const evaluation = evaluateSummary(summary, options.thresholds); const payload = { input: inputPath, evaluation, launchGateCheck: { ...launchGateCheck(evaluation), artifact: options.input, }, }; if (options.output) { const outputPath = path.resolve(process.cwd(), options.output); fs.mkdirSync(path.dirname(outputPath), { recursive: true }); fs.writeFileSync(outputPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); } if (options.json) { console.log(JSON.stringify(payload, null, 2)); } else if (!options.quiet) { console.log(`[${evaluation.status.toUpperCase()}] ${path.basename(inputPath)} p95=${summary.p95Ms}ms p99=${summary.p99Ms}ms errors=${summary.errors} errorRate=${summary.errorRate} concurrency=${summary.concurrency} duration=${summary.durationSeconds}s`); for (const failure of evaluation.failures) console.log(`- ${failure}`); } if (evaluation.status !== 'pass') process.exitCode = 1; } if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { main(); } export { defaultThresholds, evaluateSummary, launchGateCheck, summarizeBenchmark };