import { spawnSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; const repoRoot = process.cwd(); const databaseUrl = process.env.DATABASE_URL || 'postgresql://postgres:postgres@127.0.0.1:54322/postgres'; const apiBase = process.env.PERF_API_BASE || 'http://127.0.0.1:8787'; const matrix = [ { name: 'read-30', duration: 120, concurrency: 30, ramp: 15, writes: false, ratio: 0 }, { name: 'mixed-50', duration: 60, concurrency: 50, ramp: 10, writes: true, ratio: 0.1 }, { name: 'mixed-100', duration: 60, concurrency: 100, ramp: 15, writes: true, ratio: 0.08 }, { name: 'mixed-150', duration: 60, concurrency: 150, ramp: 20, writes: true, ratio: 0.06 }, ]; function run(command, args, options = {}) { const result = spawnSync(command, args, { cwd: repoRoot, stdio: 'inherit', shell: process.platform === 'win32', env: { ...process.env, DATABASE_URL: databaseUrl, AUTH_SESSION_SECRET: process.env.AUTH_SESSION_SECRET || 'development-session-secret-change-me', DB_POOL_MAX: process.env.DB_POOL_MAX || '10', BENCHMARK_API_CPUS: process.env.BENCHMARK_API_CPUS || '2.0', BENCHMARK_API_MEMORY: process.env.BENCHMARK_API_MEMORY || '4g', ...options.env, }, }); if (result.status !== 0) { throw new Error(`${command} ${args.join(' ')} failed with exit code ${result.status}`); } } function latestBenchmarkJson(before) { const dir = path.join(repoRoot, 'docs', 'refactor', 'performance-reports'); const files = fs.existsSync(dir) ? fs.readdirSync(dir) .filter(name => /^api-benchmark-\d{8}-\d{6}\.json$/.test(name)) .map(name => path.join(dir, name)) .filter(file => fs.statSync(file).mtimeMs >= before) .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs) : []; if (!files[0]) throw new Error('Benchmark report JSON was not generated.'); return path.relative(repoRoot, files[0]).replaceAll('\\', '/'); } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } async function waitForHealth(timeoutMs = 120_000) { const started = Date.now(); let lastError = null; while (Date.now() - started < timeoutMs) { try { const response = await fetch(`${apiBase}/health`); const payload = await response.json().catch(() => ({})); if (response.ok && payload.ok) return; lastError = new Error(`HTTP ${response.status}: ${JSON.stringify(payload)}`); } catch (error) { lastError = error; } await sleep(1000); } throw new Error(`API did not become healthy at ${apiBase}. ${lastError?.message || ''}`); } async function main() { const generated = []; try { run('docker', ['compose', '-f', 'docker-compose.api.yml', '-f', 'docker-compose.api.benchmark.yml', 'up', '-d', '--build', 'api']); await waitForHealth(); for (const item of matrix) { const before = Date.now(); run('node', ['scripts/api-performance-benchmark.js'], { env: { PERF_START_SERVER: 'false', PERF_API_BASE: apiBase, PERF_AUTH_MODE: 'app_session', PERF_DURATION_SECONDS: String(item.duration), PERF_CONCURRENCY: String(item.concurrency), PERF_RAMP_SECONDS: String(item.ramp), PERF_INCLUDE_WRITES: item.writes ? 'true' : 'false', PERF_PRACTICE_FLOW_RATIO: String(item.ratio), PERF_PRACTICE_FLOW_ANSWERS: '3', }, }); generated.push({ scenario: item.name, report: latestBenchmarkJson(before) }); } console.log(JSON.stringify({ status: 'pass', apiBase, reports: generated }, null, 2)); } finally { if (process.env.PERF_KEEP_DOCKER_API !== 'true') { spawnSync('docker', ['compose', '-f', 'docker-compose.api.yml', '-f', 'docker-compose.api.benchmark.yml', 'down'], { cwd: repoRoot, stdio: 'inherit', shell: process.platform === 'win32', }); } } } main().catch(error => { console.error(error); process.exitCode = 1; });