diff --git a/artifacts/load-report-2026-01-25T19-08-25-628Z.json b/artifacts/load-report-2026-01-25T19-08-25-628Z.json new file mode 100644 index 00000000..f1e37dbf --- /dev/null +++ b/artifacts/load-report-2026-01-25T19-08-25-628Z.json @@ -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": [] +} \ No newline at end of file diff --git a/artifacts/load-report-20260125.json b/artifacts/load-report-20260125.json new file mode 100644 index 00000000..50c7814c --- /dev/null +++ b/artifacts/load-report-20260125.json @@ -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": [] +} \ No newline at end of file diff --git a/artifacts/load-report.json b/artifacts/load-report.json new file mode 100644 index 00000000..50c7814c --- /dev/null +++ b/artifacts/load-report.json @@ -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": [] +} \ No newline at end of file diff --git a/scripts/agent/load-test.mjs b/scripts/agent/load-test.mjs index 5b0c9f4c..8b5aa863 100644 --- a/scripts/agent/load-test.mjs +++ b/scripts/agent/load-test.mjs @@ -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 } 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();