#!/usr/bin/env node 'use strict'; const fs = require('fs'); const path = require('path'); const { performance } = require('perf_hooks'); function globToRegExp(pattern) { let working = pattern .replace(/\*\*/g, ':::DOUBLE_WILDCARD:::') .replace(/\*/g, ':::SINGLE_WILDCARD:::'); working = working.replace(/([.+^${}()|[\]\\])/g, '\\$1'); working = working .replace(/:::DOUBLE_WILDCARD:::\//g, '(?:.*/)?') .replace(/:::DOUBLE_WILDCARD:::/g, '.*') .replace(/:::SINGLE_WILDCARD:::/g, '[^/]*'); return new RegExp(`^${working}$`); } function walkFiles(root, matcher) { const out = []; const stack = [root]; while (stack.length) { const current = stack.pop(); const stat = fs.statSync(current, { throwIfNoEntry: true }); if (stat.isDirectory()) { const entries = fs.readdirSync(current); for (const entry of entries) { stack.push(path.join(current, entry)); } } else if (stat.isFile()) { const relativePath = path.relative(root, current).replace(/\\/g, '/'); if (matcher.test(relativePath)) { out.push(current); } } } return out; } function parseArgs(argv) { const args = { config: path.join(__dirname, 'config.json'), iterations: undefined, thresholdMs: undefined, out: undefined, repoRoot: path.join(__dirname, '..', '..'), }; for (let i = 2; i < argv.length; i++) { const current = argv[i]; switch (current) { case '--config': args.config = argv[++i]; break; case '--iterations': args.iterations = Number(argv[++i]); break; case '--threshold-ms': args.thresholdMs = Number(argv[++i]); break; case '--out': args.out = argv[++i]; break; case '--repo-root': case '--samples': args.repoRoot = argv[++i]; break; default: throw new Error(`Unknown argument: ${current}`); } } return args; } function loadConfig(configPath) { const json = fs.readFileSync(configPath, 'utf8'); const cfg = JSON.parse(json); if (!Array.isArray(cfg.scenarios) || cfg.scenarios.length === 0) { throw new Error('config.scenarios must be a non-empty array'); } return cfg; } function ensureWithinRepo(repoRoot, target) { const relative = path.relative(repoRoot, target); if (relative === '' || relative === '.') { return true; } return !relative.startsWith('..') && !path.isAbsolute(relative); } function parseNodePackage(contents) { const parsed = JSON.parse(contents); if (!parsed.name || !parsed.version) { throw new Error('package.json missing name/version'); } return { name: parsed.name, version: parsed.version }; } function parsePythonMetadata(contents) { let name; let version; for (const line of contents.split(/\r?\n/)) { if (!name && line.startsWith('Name:')) { name = line.slice(5).trim(); } else if (!version && line.startsWith('Version:')) { version = line.slice(8).trim(); } if (name && version) { break; } } if (!name || !version) { throw new Error('METADATA missing Name/Version headers'); } return { name, version }; } function formatRow(row) { const cols = [ row.id.padEnd(28), row.sampleCount.toString().padStart(5), row.meanMs.toFixed(2).padStart(9), row.p95Ms.toFixed(2).padStart(9), row.maxMs.toFixed(2).padStart(9), ]; return cols.join(' | '); } function percentile(sortedDurations, percentile) { if (sortedDurations.length === 0) { return 0; } const rank = (percentile / 100) * (sortedDurations.length - 1); const lower = Math.floor(rank); const upper = Math.ceil(rank); const weight = rank - lower; if (upper >= sortedDurations.length) { return sortedDurations[lower]; } return sortedDurations[lower] + weight * (sortedDurations[upper] - sortedDurations[lower]); } function main() { const args = parseArgs(process.argv); const cfg = loadConfig(args.config); const iterations = args.iterations ?? cfg.iterations ?? 5; const thresholdMs = args.thresholdMs ?? cfg.thresholdMs ?? 5000; const results = []; const failures = []; for (const scenario of cfg.scenarios) { const scenarioRoot = path.resolve(args.repoRoot, scenario.root); if (!ensureWithinRepo(args.repoRoot, scenarioRoot)) { throw new Error(`Scenario root ${scenario.root} escapes repo root ${args.repoRoot}`); } if (!fs.existsSync(scenarioRoot)) { throw new Error(`Scenario root ${scenarioRoot} does not exist`); } const matcher = globToRegExp(scenario.matcher.replace(/\\/g, '/')); const durations = []; let sampleCount = 0; for (let attempt = 0; attempt < iterations; attempt++) { const start = performance.now(); const files = walkFiles(scenarioRoot, matcher); if (files.length === 0) { throw new Error(`Scenario ${scenario.id} matched no files`); } for (const filePath of files) { const contents = fs.readFileSync(filePath, 'utf8'); if (scenario.parser === 'node') { parseNodePackage(contents); } else if (scenario.parser === 'python') { parsePythonMetadata(contents); } else { throw new Error(`Unknown parser ${scenario.parser} for scenario ${scenario.id}`); } } const end = performance.now(); durations.push(end - start); sampleCount = files.length; } durations.sort((a, b) => a - b); const mean = durations.reduce((acc, value) => acc + value, 0) / durations.length; const p95 = percentile(durations, 95); const max = durations[durations.length - 1]; if (max > thresholdMs) { failures.push(`${scenario.id} exceeded threshold: ${(max).toFixed(2)} ms > ${thresholdMs} ms`); } results.push({ id: scenario.id, label: scenario.label, sampleCount, meanMs: mean, p95Ms: p95, maxMs: max, iterations, }); } console.log('Scenario | Count | Mean(ms) | P95(ms) | Max(ms)'); console.log('---------------------------- | ----- | --------- | --------- | ----------'); for (const row of results) { console.log(formatRow(row)); } if (args.out) { const header = 'scenario,iterations,sample_count,mean_ms,p95_ms,max_ms\n'; const csvRows = results .map((row) => [ row.id, row.iterations, row.sampleCount, row.meanMs.toFixed(4), row.p95Ms.toFixed(4), row.maxMs.toFixed(4), ].join(',') ) .join('\n'); fs.writeFileSync(args.out, header + csvRows + '\n', 'utf8'); } if (failures.length > 0) { console.error('\nPerformance threshold exceeded:'); for (const failure of failures) { console.error(` - ${failure}`); } process.exitCode = 1; } } if (require.main === module) { try { main(); } catch (err) { console.error(err instanceof Error ? err.message : err); process.exit(1); } }