mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 22:26:38 +03:00
Compare production-equivalent direct and app-server paths, include annotated UI work, expose first-usable latency, and keep semantic quality gates honest.\n\nAI-assisted: OpenAI Codex.
115 lines
4.1 KiB
JavaScript
115 lines
4.1 KiB
JavaScript
import { spawn } from 'node:child_process';
|
|
import { performance } from 'node:perf_hooks';
|
|
|
|
export function runCodexExecBenchmark({
|
|
command = 'codex',
|
|
args,
|
|
cwd = process.cwd(),
|
|
env = process.env,
|
|
timeoutMs = 300_000,
|
|
spawnFactory = spawn,
|
|
} = {}) {
|
|
if (!Array.isArray(args) || args.length === 0) throw new TypeError('args are required');
|
|
const startedAt = performance.now();
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawnFactory(command, args, {
|
|
cwd,
|
|
env,
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
let stdout = '';
|
|
let stderr = '';
|
|
let buffer = '';
|
|
let threadStartedMs = null;
|
|
let turnStartedMs = null;
|
|
let firstAgentMessageMs = null;
|
|
let usage = null;
|
|
const events = [];
|
|
const timer = setTimeout(() => {
|
|
child.kill('SIGTERM');
|
|
reject(new Error(`codex exec timed out after ${timeoutMs}ms`));
|
|
}, timeoutMs);
|
|
timer.unref?.();
|
|
|
|
const consumeLine = (line) => {
|
|
if (!line.trim()) return;
|
|
let event;
|
|
try { event = JSON.parse(line); } catch { return; }
|
|
events.push(event);
|
|
const elapsed = performance.now() - startedAt;
|
|
if (event.type === 'thread.started' && threadStartedMs == null) threadStartedMs = elapsed;
|
|
if (event.type === 'turn.started' && turnStartedMs == null) turnStartedMs = elapsed;
|
|
if (event.type === 'item.completed' && event.item?.type === 'agent_message' && firstAgentMessageMs == null) {
|
|
firstAgentMessageMs = elapsed;
|
|
}
|
|
if (event.type === 'turn.completed') usage = event.usage || null;
|
|
};
|
|
|
|
child.stdout.on('data', (chunk) => {
|
|
const text = String(chunk);
|
|
stdout += text;
|
|
buffer += text;
|
|
let newline;
|
|
while ((newline = buffer.indexOf('\n')) !== -1) {
|
|
consumeLine(buffer.slice(0, newline));
|
|
buffer = buffer.slice(newline + 1);
|
|
}
|
|
});
|
|
child.stderr.on('data', (chunk) => { stderr += String(chunk); });
|
|
child.once('error', (error) => {
|
|
clearTimeout(timer);
|
|
reject(error);
|
|
});
|
|
child.once('exit', (code, signal) => {
|
|
clearTimeout(timer);
|
|
if (buffer) consumeLine(buffer);
|
|
const durationMs = performance.now() - startedAt;
|
|
if (code !== 0) {
|
|
const error = new Error(`codex exec failed (${code ?? signal}): ${stderr || stdout}`);
|
|
error.code = code;
|
|
reject(error);
|
|
return;
|
|
}
|
|
resolve({
|
|
durationMs,
|
|
threadStartedMs,
|
|
turnStartedMs,
|
|
firstAgentMessageMs,
|
|
usage,
|
|
events,
|
|
stdout,
|
|
stderr,
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
export function summarizeArchitectureRuns(runs) {
|
|
const completed = runs.filter((run) => !run.error);
|
|
return {
|
|
runs: runs.length,
|
|
passed: completed.filter((run) => run.passed).length,
|
|
medianFirstUsableMs: percentile(completed.map((run) => run.firstUsableMs).filter(Number.isFinite), 0.5),
|
|
p95FirstUsableMs: percentile(completed.map((run) => run.firstUsableMs).filter(Number.isFinite), 0.95),
|
|
medianTotalMs: percentile(completed.map((run) => run.totalMs), 0.5),
|
|
p95TotalMs: percentile(completed.map((run) => run.totalMs), 0.95),
|
|
medianStartupMs: percentile(completed.map((run) => run.startupMs).filter(Number.isFinite), 0.5),
|
|
medianGenerationMs: percentile(completed.map((run) => run.generationMs).filter(Number.isFinite), 0.5),
|
|
medianInputTokens: percentile(completed.map((run) => run.usage?.input_tokens).filter(Number.isFinite), 0.5),
|
|
medianCachedInputTokens: percentile(completed.map((run) => run.usage?.cached_input_tokens).filter(Number.isFinite), 0.5),
|
|
medianOutputTokens: percentile(completed.map((run) => run.usage?.output_tokens).filter(Number.isFinite), 0.5),
|
|
};
|
|
}
|
|
|
|
function percentile(values, quantile) {
|
|
if (values.length === 0) return null;
|
|
const sorted = [...values].sort((a, b) => a - b);
|
|
const index = (sorted.length - 1) * quantile;
|
|
const lower = Math.floor(index);
|
|
const upper = Math.ceil(index);
|
|
const value = lower === upper
|
|
? sorted[lower]
|
|
: sorted[lower] + (sorted[upper] - sorted[lower]) * (index - lower);
|
|
return Math.round(value * 100) / 100;
|
|
}
|