feat: add docker capacity benchmark mode

This commit is contained in:
Codex
2026-07-01 03:31:17 +08:00
parent eb3dca5fb8
commit 3d33c783cd
11 changed files with 242 additions and 6 deletions

View File

@@ -1,4 +1,5 @@
import { spawn } from 'node:child_process';
import crypto from 'node:crypto';
import fs from 'node:fs/promises';
import net from 'node:net';
import path from 'node:path';
@@ -23,6 +24,9 @@ const TENANT_CODE = process.env.PERF_TENANT_CODE || 'master';
const QUESTION_LIMIT = envNumber('PERF_QUESTION_LIMIT', 20);
const MAX_ERRORS_TO_KEEP = envNumber('PERF_MAX_ERRORS', 20);
const DEFAULT_TIMEOUT_MS = envNumber('PERF_REQUEST_TIMEOUT_MS', 15_000);
const AUTH_MODE = normalizeAuthMode(process.env.PERF_AUTH_MODE || 'legacy');
const STATIC_BEARER_TOKEN = process.env.PERF_BEARER_TOKEN || '';
const AUTH_SESSION_SECRET = process.env.AUTH_SESSION_SECRET || 'development-session-secret-change-me';
let apiBase = API_BASE_ENV || '';
let serverProcess = null;
@@ -44,6 +48,14 @@ function envNumber(key, fallback, options = {}) {
return fallback;
}
function normalizeAuthMode(value) {
const normalized = String(value || '').toLowerCase().replace(/[-_]/g, '');
if (normalized === 'appsession' || normalized === 'session' || normalized === 'tk') return 'app_session';
if (normalized === 'bearer' || normalized === 'jwt' || normalized === 'token') return 'bearer';
if (normalized === 'none' || normalized === 'anonymous') return 'none';
return 'legacy';
}
function getFreePort() {
return new Promise((resolve, reject) => {
const server = net.createServer();
@@ -91,6 +103,7 @@ async function requestJson(endpoint, context, timeoutMs = DEFAULT_TIMEOUT_MS) {
const started = performance.now();
const tenantId = context.tenantId || context.tenant?.id;
const userId = context.userId || context.user?.id;
const authToken = context.authToken || '';
try {
const response = await fetch(buildUrl(apiBase, endpoint), {
method: endpoint.method || 'GET',
@@ -98,7 +111,8 @@ async function requestJson(endpoint, context, timeoutMs = DEFAULT_TIMEOUT_MS) {
headers: {
'content-type': 'application/json',
...(endpoint.tenantHeader === false || !tenantId ? {} : { 'x-tenant-id': tenantId }),
...(endpoint.userHeader === false || !userId ? {} : { 'x-user-id': userId }),
...(AUTH_MODE === 'legacy' && endpoint.userHeader !== false && userId ? { 'x-user-id': userId } : {}),
...(endpoint.authHeader === false || !authToken ? {} : { authorization: `Bearer ${authToken}` }),
...(endpoint.headers || {}),
},
body: endpoint.body ? JSON.stringify(endpoint.body(context)) : undefined,
@@ -311,6 +325,7 @@ async function startServerIfNeeded() {
...process.env,
PORT: String(port),
DATABASE_URL,
AUTH_SESSION_SECRET,
MAX_JSON_BODY_BYTES: process.env.MAX_JSON_BODY_BYTES || '1048576',
MAX_IMPORT_JSON_BODY_BYTES: process.env.MAX_IMPORT_JSON_BODY_BYTES || '10485760',
},
@@ -342,6 +357,46 @@ async function many(pool, sql, params = []) {
return result.rows;
}
function createSessionToken() {
return `tk_${crypto.randomBytes(32).toString('base64url')}`;
}
function hashSessionToken(token) {
return crypto.createHmac('sha256', AUTH_SESSION_SECRET).update(token).digest('hex');
}
async function createBenchmarkAuthToken(context) {
if (AUTH_MODE === 'none' || AUTH_MODE === 'legacy') return '';
if (AUTH_MODE === 'bearer') {
if (!STATIC_BEARER_TOKEN) throw new Error('PERF_BEARER_TOKEN is required when PERF_AUTH_MODE=bearer.');
return STATIC_BEARER_TOKEN;
}
if (AUTH_MODE !== 'app_session') return '';
if (!context.tenant?.id || !context.user?.id) {
throw new Error('PERF_AUTH_MODE=app_session requires discovered tenant and user context.');
}
const token = createSessionToken();
const pool = new pg.Pool({ connectionString: DATABASE_URL, max: 2 });
try {
await pool.query(
`
insert into app_private.auth_sessions (
tenant_id, user_id, token_hash, provider, expires_at, metadata
)
values (
$1, $2, $3, 'performance_benchmark',
now() + interval '2 hours',
'{"createdBy":"api-performance-benchmark","authMode":"app_session"}'::jsonb
)
`,
[context.tenant.id, context.user.id, hashSessionToken(token)],
);
return token;
} finally {
await pool.end();
}
}
async function discoverBenchmarkContext() {
const pool = new pg.Pool({ connectionString: DATABASE_URL, max: 4 });
try {
@@ -790,6 +845,7 @@ async function runBenchmark(context) {
concurrency: CONCURRENCY,
rampSeconds: RAMP_SECONDS,
includeWrites: INCLUDE_WRITES,
authMode: AUTH_MODE,
practiceFlowRatio: INCLUDE_WRITES ? PRACTICE_FLOW_RATIO : 0,
practiceFlowAnswers: INCLUDE_WRITES ? PRACTICE_FLOW_ANSWERS : 0,
questionLimit: QUESTION_LIMIT,
@@ -906,10 +962,11 @@ async function main() {
throw new Error('PERF_API_BASE is required when PERF_START_SERVER=false.');
}
const context = await discoverBenchmarkContext();
context.authToken = await createBenchmarkAuthToken(context);
await startServerIfNeeded();
if (!apiBase) throw new Error('API base URL was not resolved.');
console.log(`[perf] target api=${apiBase} tenant=${context.tenant.slug} user=${context.user.id}`);
console.log(`[perf] duration=${DURATION_SECONDS}s concurrency=${CONCURRENCY} includeWrites=${INCLUDE_WRITES}`);
console.log(`[perf] duration=${DURATION_SECONDS}s concurrency=${CONCURRENCY} includeWrites=${INCLUDE_WRITES} authMode=${AUTH_MODE}`);
const report = await runBenchmark(context);
const files = await writeReport(report);
console.log(`[perf] requests=${report.summary.requests} ok=${report.summary.ok} errors=${report.summary.errors} rps=${report.summary.throughputRps} p95=${report.summary.latencyOk.p95Ms}ms`);