Files
openmaic/OpenMAIC/scripts/load-test-reads.mjs
2026-08-16 14:58:47 +08:00

98 lines
3.3 KiB
JavaScript

#!/usr/bin/env node
/**
* Read-path load test — P4 verification.
*
* Exercises the learner-end read surface (registry list/detail + bundle
* download — the static, zero-LLM hot path) with bounded concurrency and
* reports p50/p95/max latency + throughput. Run against a production-mode
* server:
*
* COURSEWARE_PUBLISH_TOKEN=... npx next start -p 3100 &
* node scripts/load-test-reads.mjs --base http://localhost:3100 \
* --courseware e2e-cw-1 --concurrency 20 --requests 400
*
* Expected: the read path is pure static file/JSON serving (no LLM, no
* generation), so latency should be flat under load; if it is not, the
* deployment has hot-path bottlenecks to address (object store, CDN, DB).
*/
import { performance } from 'node:perf_hooks';
const args = {};
for (let i = 2; i < process.argv.length; i++) {
const arg = process.argv[i];
if (arg.startsWith('--')) {
const key = arg.slice(2);
args[key] = process.argv[i + 1] && !process.argv[i + 1].startsWith('--')
? process.argv[i + 1]
: 'true';
if (args[key] !== 'true') i++;
}
}
const BASE = (args.base || 'http://localhost:3000').replace(/\/$/, '');
const COURSEWARE = args.courseware || 'e2e-cw-1';
const CONCURRENCY = Number(args.concurrency || 20);
const REQUESTS = Number(args.requests || 400);
const RAMP = Number(args.ramp || 1); // 1 = include download, 0 = metadata only
const latency = [];
let failures = 0;
let bytes = 0;
const pathOf = (i) => {
if (RAMP && i % 5 === 0) return `/api/coursewares/${COURSEWARE}/bundles/1/download`;
if (i % 3 === 0) return `/api/coursewares/${COURSEWARE}`;
return '/api/coursewares';
};
async function oneRequest(i) {
const url = `${BASE}${pathOf(i)}`;
const start = performance.now();
try {
const response = await fetch(url);
const body = await response.arrayBuffer();
const elapsed = performance.now() - start;
latency.push(elapsed);
bytes += body.byteLength;
if (!response.ok) failures++;
} catch (error) {
failures++;
latency.push(performance.now() - start);
console.error(`request ${i} failed: ${error.message}`);
}
}
async function main() {
console.log(`load test: base=${BASE} courseware=${COURSEWARE} concurrency=${CONCURRENCY} requests=${REQUESTS} ramp=${RAMP}`);
const started = performance.now();
let next = 0;
const workers = Array.from({ length: CONCURRENCY }, async () => {
while (true) {
const i = next++;
if (i >= REQUESTS) return;
await oneRequest(i);
}
});
await Promise.all(workers);
const totalMs = performance.now() - started;
const sorted = [...latency].sort((a, b) => a - b);
const p = (q) => sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * q))];
const avg = sorted.reduce((s, v) => s + v, 0) / sorted.length;
const rps = (REQUESTS / totalMs) * 1000;
console.log('\n=== read-path load test ===');
console.log(`requests: ${REQUESTS} failures: ${failures}`);
console.log(`throughput: ${rps.toFixed(1)} req/s (total ${(totalMs / 1000).toFixed(2)}s)`);
console.log(`bytes served: ${(bytes / 1024 / 1024).toFixed(2)} MB`);
console.log(`latency p50: ${p(0.5).toFixed(1)}ms p95: ${p(0.95).toFixed(1)}ms p99: ${p(0.99).toFixed(1)}ms max: ${sorted.at(-1).toFixed(1)}ms avg: ${avg.toFixed(1)}ms`);
process.exitCode = failures > 0 ? 1 : 0;
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});