Add load testing script and initial report

This commit is contained in:
google-labs-jules[bot] committed 2026-01-25 19:15:37 +00:00
1 parent b6260c1352
commit f7aef64103
4 files changed
+301 -30

No files matched your search

@@ -0,0 +1,37 @@
{
"timestamp": "2026-01-25T19:08:25.628Z",
"config": {
"duration": 5,
"clients": 10,
"rate": 1,
"relay": "ws://localhost:8889"
},
"metrics": {
"total_sent": 51,
"total_received": 51,
"errors": 0,
"throughput_sent": 10.2,
"throughput_recv": 10.2,
"latency_ms": {
"p50": 0.8110580000000027,
"p95": 1.366897999999992,
"p99": 3.3182230000002164
},
"operation_times_ms": {
"build_avg": 0.160458607843158,
"sign_avg": 6.053091784313705
},
"resource_usage": [
{
"timestamp": 1769368105533,
"cpuUser": 778796,
"cpuSystem": 118523,
"rss": 88248320,
"heapUsed": 12950504
}
],
"error_breakdown": {}
},
"bottlenecks": [],
"remediation": []
}
+97
View File
@@ -0,0 +1,97 @@
{
"timestamp": "2026-01-25T19:12:13.640Z",
"config": {
"duration": 60,
"clients": 100,
"rate": 1,
"relay": "ws://localhost:8889",
"force": false
},
"metrics": {
"total_sent": 6219,
"total_received": 6219,
"errors": 0,
"throughput_sent": 103.65,
"throughput_recv": 103.65,
"latency_ms": {
"p50": 0.4525159999998323,
"p95": 0.9470979999969131,
"p99": 7.444698999999673
},
"operation_times_ms": {
"build_avg": 0.05414333365494229,
"sign_avg": 5.486003140054635
},
"resource_usage": [
{
"timestamp": 1769368278643,
"cpuPercent": 63.94,
"rss": 97239040,
"heapUsed": 16337816
},
{
"timestamp": 1769368283643,
"cpuPercent": 64.36,
"rss": 98402304,
"heapUsed": 20259576
},
{
"timestamp": 1769368288642,
"cpuPercent": 63.86,
"rss": 98762752,
"heapUsed": 17659632
},
{
"timestamp": 1769368293643,
"cpuPercent": 61.32,
"rss": 99790848,
"heapUsed": 18155016
},
{
"timestamp": 1769368298645,
"cpuPercent": 61.19,
"rss": 100999168,
"heapUsed": 15496016
},
{
"timestamp": 1769368303646,
"cpuPercent": 63.12,
"rss": 102313984,
"heapUsed": 16290368
},
{
"timestamp": 1769368308649,
"cpuPercent": 61.61,
"rss": 105914368,
"heapUsed": 22963352
},
{
"timestamp": 1769368313653,
"cpuPercent": 60.4,
"rss": 106831872,
"heapUsed": 21787144
},
{
"timestamp": 1769368318653,
"cpuPercent": 60.86,
"rss": 120463360,
"heapUsed": 17511208
},
{
"timestamp": 1769368323657,
"cpuPercent": 60.52,
"rss": 120987648,
"heapUsed": 19454880
},
{
"timestamp": 1769368328657,
"cpuPercent": 59.65,
"rss": 121643008,
"heapUsed": 21375032
}
],
"error_breakdown": {}
},
"bottlenecks": [],
"remediation": []
}
+97
View File
@@ -0,0 +1,97 @@
{
"timestamp": "2026-01-25T19:12:13.640Z",
"config": {
"duration": 60,
"clients": 100,
"rate": 1,
"relay": "ws://localhost:8889",
"force": false
},
"metrics": {
"total_sent": 6219,
"total_received": 6219,
"errors": 0,
"throughput_sent": 103.65,
"throughput_recv": 103.65,
"latency_ms": {
"p50": 0.4525159999998323,
"p95": 0.9470979999969131,
"p99": 7.444698999999673
},
"operation_times_ms": {
"build_avg": 0.05414333365494229,
"sign_avg": 5.486003140054635
},
"resource_usage": [
{
"timestamp": 1769368278643,
"cpuPercent": 63.94,
"rss": 97239040,
"heapUsed": 16337816
},
{
"timestamp": 1769368283643,
"cpuPercent": 64.36,
"rss": 98402304,
"heapUsed": 20259576
},
{
"timestamp": 1769368288642,
"cpuPercent": 63.86,
"rss": 98762752,
"heapUsed": 17659632
},
{
"timestamp": 1769368293643,
"cpuPercent": 61.32,
"rss": 99790848,
"heapUsed": 18155016
},
{
"timestamp": 1769368298645,
"cpuPercent": 61.19,
"rss": 100999168,
"heapUsed": 15496016
},
{
"timestamp": 1769368303646,
"cpuPercent": 63.12,
"rss": 102313984,
"heapUsed": 16290368
},
{
"timestamp": 1769368308649,
"cpuPercent": 61.61,
"rss": 105914368,
"heapUsed": 22963352
},
{
"timestamp": 1769368313653,
"cpuPercent": 60.4,
"rss": 106831872,
"heapUsed": 21787144
},
{
"timestamp": 1769368318653,
"cpuPercent": 60.86,
"rss": 120463360,
"heapUsed": 17511208
},
{
"timestamp": 1769368323657,
"cpuPercent": 60.52,
"rss": 120987648,
"heapUsed": 19454880
},
{
"timestamp": 1769368328657,
"cpuPercent": 59.65,
"rss": 121643008,
"heapUsed": 21375032
}
],
"error_breakdown": {}
},
"bottlenecks": [],
"remediation": []
}
+70 -30
View File
@@ -24,10 +24,7 @@ if (!fs.existsSync(ARTIFACTS_DIR)) {
const DEFAULT_CONFIG = {
duration: 600, // seconds
clients: 1000,
rate: 0.1, // events per second per client? No, that would be 100 events/sec.
// Let's interpret 'rate' as TOTAL events per second to target,
// OR rate per client.
// If I have 1000 clients and rate 0.1, that's 100 events/sec. That's reasonable.
rate: 0.1, // events per second per client
relay: 'ws://localhost:8889',
};
@@ -38,6 +35,7 @@ const { values: args } = parseArgs({
clients: { type: 'string', short: 'c' },
rate: { type: 'string', short: 'r' },
relay: { type: 'string', short: 'u' }, // u for url
force: { type: 'boolean', short: 'f' },
},
});
@@ -46,11 +44,23 @@ const config = {
clients: args.clients ? parseInt(args.clients) : DEFAULT_CONFIG.clients,
rate: args.rate ? parseFloat(args.rate) : DEFAULT_CONFIG.rate,
relay: args.relay || DEFAULT_CONFIG.relay,
force: args.force || false,
};
console.log('--- Load Test Configuration ---');
console.log(JSON.stringify(config, null, 2));
// Guardrail: Public Relay
if (!config.force) {
const u = new URL(config.relay);
const isLocal = ['localhost', '127.0.0.1', '0.0.0.0'].includes(u.hostname) || u.hostname.endsWith('.local');
if (!isLocal) {
console.error(`[Guardrail] ERROR: Target relay ${config.relay} does not appear to be local.`);
console.error('Use --force to override (NOT RECOMMENDED for public relays).');
process.exit(1);
}
}
// --- State ---
const activeClients = []; // { ws, sk, pk, pendingEvents: Map<id, startTime> }
let relayProcess = null;
@@ -104,7 +114,6 @@ async function waitForPort(port) {
});
return;
} catch (e) {
// console.log('Retrying connection...', e.message);
await new Promise(r => setTimeout(r, retryInterval));
}
}
@@ -163,7 +172,14 @@ function createClient() {
async function run() {
try {
// 1. Start Relay
await startRelay();
// Only start if we are targeting the default local port, assuming we manage it.
// If user provided a custom URL, we assume they manage the relay unless it matches our default logic.
// For simplicity, we always try to start the relay if it's on localhost and port matches default or we can detect it's not running.
// But per instructions "Use a local relay", let's always try to start it if it's the default port, or fail if port is taken.
// Actually, let's just try to start it.
await startRelay().catch(e => {
console.log('[Setup] Could not start relay (maybe already running?):', e.message);
});
// 2. Init Clients
console.log(`[Setup] Connecting ${config.clients} clients...`);
@@ -188,19 +204,35 @@ async function run() {
console.log(`[Load] Target rate: ${totalRate.toFixed(2)} events/sec`);
let running = true;
let lastCpu = process.cpuUsage();
let lastTime = performance.now();
// Monitoring Loop
const monitorInterval = setInterval(() => {
const now = performance.now();
const cpu = process.cpuUsage();
const mem = process.memoryUsage();
metrics.resourceUsage.push({
timestamp: Date.now(),
cpuUser: cpu.user,
cpuSystem: cpu.system,
rss: mem.rss,
heapUsed: mem.heapUsed,
});
console.log(`[Monitor] Sent: ${metrics.sent}, Recv: ${metrics.received}, Errors: ${metrics.errors}, Latency p50: ${calculatePercentile(metrics.latencies, 50).toFixed(2)}ms`);
const elapsedMs = now - lastTime;
if (elapsedMs > 0) {
const userDeltaUs = cpu.user - lastCpu.user;
const systemDeltaUs = cpu.system - lastCpu.system;
const totalDeltaUs = userDeltaUs + systemDeltaUs;
const cpuPercent = (totalDeltaUs / (elapsedMs * 1000)) * 100; // us / (ms * 1000) = fraction
const mem = process.memoryUsage();
metrics.resourceUsage.push({
timestamp: Date.now(),
cpuPercent: parseFloat(cpuPercent.toFixed(2)),
rss: mem.rss,
heapUsed: mem.heapUsed,
});
lastCpu = cpu;
lastTime = now;
const p50 = calculatePercentile(metrics.latencies, 50);
console.log(`[Monitor] Sent: ${metrics.sent}, Recv: ${metrics.received}, Errors: ${metrics.errors}, Latency p50: ${p50.toFixed(2)}ms, CPU: ${cpuPercent.toFixed(1)}%`);
}
}, 5000);
// Event Loop
@@ -219,14 +251,14 @@ async function run() {
let event;
const buildStart = performance.now();
const now = Math.floor(Date.now() / 1000);
const hexPk = client.pk; // getPublicKey returns hex in v2
const hexPk = client.pk;
if (isHeavy) {
// Video Post
const content = {
version: 3,
title: `Load Test Video ${Date.now()}`,
description: 'A description for load testing purposes. '.repeat(50), // make it larger
description: 'A description for load testing purposes. '.repeat(50),
magnet: `magnet:?xt=urn:btih:${Math.random().toString(16).slice(2).repeat(10)}`,
mode: 'live',
videoRootId: `load-${Date.now()}-${Math.random()}`,
@@ -252,9 +284,6 @@ async function run() {
// Sign
const signStart = performance.now();
// We need to pass event to finalizeEvent.
// Note: build*Event returns a plain object.
// nostr-tools v2 finalizeEvent takes (t, secretKey)
const signedEvent = NostrTools.finalizeEvent(event, client.sk);
metrics.operationTimes.sign.push(performance.now() - signStart);
@@ -272,10 +301,7 @@ async function run() {
const elapsed = performance.now() - iterationStart;
const delay = Math.max(0, intervalMs - elapsed);
if (delay > 0) await new Promise(r => setTimeout(r, delay));
else {
// Yield to event loop if we are falling behind
await new Promise(r => setImmediate(r));
}
else await new Promise(r => setImmediate(r));
}
running = false;
};
@@ -300,6 +326,7 @@ async function run() {
console.log('[Teardown] Killing relay...');
relayProcess.kill();
}
process.exit(0);
}
}
@@ -337,7 +364,7 @@ function generateReport() {
remediation: []
};
// Simple heuristics for bottlenecks
// Heuristics
if (report.metrics.latency_ms.p99 > 1000) {
report.bottlenecks.push('High P99 Latency (>1s)');
report.remediation.push('Relay might be overloaded or network saturated. Investigate relay event loop lag.');
@@ -347,16 +374,29 @@ function generateReport() {
report.remediation.push('Check error breakdown.');
}
// Hot functions proxy
if (report.metrics.operation_times_ms.sign_avg > 10) {
report.bottlenecks.push('Signing is slow');
// Hot functions proxy / Crypto bottleneck
const signAvg = report.metrics.operation_times_ms.sign_avg;
if (signAvg > 10) {
report.bottlenecks.push(`Signing is slow (${signAvg.toFixed(2)}ms)`);
report.remediation.push('Optimize signing or offload to worker.');
}
const filename = `load-report-${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
if (signAvg > 50) {
report.bottlenecks.push('Cryptographic bottleneck');
report.remediation.push('Mark as requires-security-review. Check if running in a constrained environment or if crypto implementation is suboptimal.');
}
// Report File
const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, ''); // YYYYMMDD
const filename = `load-report-${dateStr}.json`;
const reportPath = path.join(ARTIFACTS_DIR, filename);
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log(`[Report] Saved to ${reportPath}`);
// Also save as load-report.json
const latestPath = path.join(ARTIFACTS_DIR, 'load-report.json');
fs.copyFileSync(reportPath, latestPath);
console.log(`[Report] Saved to ${reportPath} and ${latestPath}`);
}
run();