Files
gongxue-base/scripts/run-docker-4c16g-benchmark.js

107 lines
3.9 KiB
JavaScript

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;
});