feat: implement load and stress test script with relay monitoring

Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
google-labs-jules[bot]andPR0M3TH3AN committed 2026-01-27 19:10:29 +00:00
1 parent c8264c300b
commit d3a3097d1c
2 files changed
+40 -107

No files matched your search

-97
View File
@@ -1,97 +0,0 @@
{
"timestamp": "2026-01-26T19:39:32.887Z",
"config": {
"duration": 60,
"clients": 1000,
"rate": 0.1,
"relay": "ws://localhost:8889",
"force": false
},
"metrics": {
"total_sent": 6467,
"total_received": 6467,
"errors": 0,
"throughput_sent": 107.78333333333333,
"throughput_recv": 107.78333333333333,
"latency_ms": {
"p50": 0.9700979999979609,
"p95": 14.598196999999345,
"p99": 27.9113440000001
},
"operation_times_ms": {
"build_avg": 0.10325683753031617,
"sign_avg": 6.474848927014066
},
"resource_usage": [
{
"timestamp": 1769456317885,
"cpuPercent": 74.54,
"rss": 129236992,
"heapUsed": 26275344
},
{
"timestamp": 1769456322898,
"cpuPercent": 71.77,
"rss": 130256896,
"heapUsed": 30151424
},
{
"timestamp": 1769456327916,
"cpuPercent": 70.52,
"rss": 131235840,
"heapUsed": 30829208
},
{
"timestamp": 1769456332917,
"cpuPercent": 72.98,
"rss": 131637248,
"heapUsed": 38053280
},
{
"timestamp": 1769456337917,
"cpuPercent": 78.13,
"rss": 132292608,
"heapUsed": 25903752
},
{
"timestamp": 1769456342918,
"cpuPercent": 92.71,
"rss": 133566464,
"heapUsed": 28111320
},
{
"timestamp": 1769456347922,
"cpuPercent": 94.4,
"rss": 133959680,
"heapUsed": 33235048
},
{
"timestamp": 1769456352924,
"cpuPercent": 77.78,
"rss": 134717440,
"heapUsed": 27477160
},
{
"timestamp": 1769456357925,
"cpuPercent": 86.22,
"rss": 135241728,
"heapUsed": 36644992
},
{
"timestamp": 1769456362926,
"cpuPercent": 79.21,
"rss": 136028160,
"heapUsed": 28313856
},
{
"timestamp": 1769456367927,
"cpuPercent": 71.57,
"rss": 136814592,
"heapUsed": 39710304
}
],
"error_breakdown": {}
},
"bottlenecks": [],
"remediation": []
}
+40 -10
View File
@@ -1,12 +1,15 @@
import './setup-test-env.js';
import { parseArgs } from 'node:util';
import { spawn } from 'node:child_process';
import { spawn, exec } from 'node:child_process';
import { fileURLToPath } from 'url';
import NodeWebSocket from 'ws';
import * as NostrTools from 'nostr-tools';
import fs from 'node:fs';
import path from 'node:path';
import { performance } from 'node:perf_hooks';
import util from 'node:util';
const execAsync = util.promisify(exec);
// Polyfill global NostrTools for app modules
global.NostrTools = NostrTools;
@@ -76,6 +79,7 @@ const metrics = {
sign: [],
},
resourceUsage: [],
relayResourceUsage: [], // { timestamp, cpu, mem }
};
// --- Helpers ---
@@ -96,7 +100,7 @@ async function startRelay() {
// Wait for port
await waitForPort(parseInt(port));
console.log('[Setup] Relay started.');
console.log('[Setup] Relay started (PID: ' + relayProcess.pid + ').');
}
async function waitForPort(port) {
@@ -121,6 +125,20 @@ async function waitForPort(port) {
throw new Error(`Relay did not start on port ${port}`);
}
async function getProcessStats(pid) {
try {
// Output: %CPU %MEM
const { stdout } = await execAsync(`ps -p ${pid} -o %cpu,%mem --no-headers`);
const [cpu, mem] = stdout.trim().split(/\s+/);
return {
cpu: parseFloat(cpu) || 0,
mem: parseFloat(mem) || 0
};
} catch (e) {
return null;
}
}
function createClient() {
const sk = NostrTools.generateSecretKey();
const pk = NostrTools.getPublicKey(sk);
@@ -177,7 +195,6 @@ async function run() {
// 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);
});
@@ -195,6 +212,7 @@ async function run() {
// 3. Load Loop
console.log('[Load] Starting load generation...');
console.log('[Load] Requirements: 10% Video Post (multipart), 90% View Event');
const startTime = performance.now();
const endTime = startTime + (config.duration * 1000);
@@ -209,7 +227,7 @@ async function run() {
let lastTime = performance.now();
// Monitoring Loop
const monitorInterval = setInterval(() => {
const monitorInterval = setInterval(async () => {
const now = performance.now();
const cpu = process.cpuUsage();
const elapsedMs = now - lastTime;
@@ -228,11 +246,22 @@ async function run() {
heapUsed: mem.heapUsed,
});
if (relayProcess) {
const stats = await getProcessStats(relayProcess.pid);
if (stats) {
metrics.relayResourceUsage.push({
timestamp: Date.now(),
cpuPercent: stats.cpu,
memPercent: stats.mem
});
}
}
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)}%`);
console.log(`[Monitor] Sent: ${metrics.sent}, Recv: ${metrics.received}, Errors: ${metrics.errors}, Latency p50: ${p50.toFixed(2)}ms, Runner CPU: ${cpuPercent.toFixed(1)}%`);
}
}, 5000);
@@ -249,7 +278,6 @@ async function run() {
const isHeavy = Math.random() < 0.1;
try {
let event;
const buildStart = performance.now();
const now = Math.floor(Date.now() / 1000);
const hexPk = client.pk;
@@ -315,7 +343,7 @@ async function run() {
} else {
// View Event
event = buildViewEvent({
const event = buildViewEvent({
pubkey: hexPk,
created_at: now,
pointerValue: `load-video-${Math.random()}`,
@@ -388,6 +416,8 @@ function calculatePercentile(data, percentile) {
}
function generateReport() {
const signAvg = metrics.operationTimes.sign.reduce((a, b) => a + b, 0) / metrics.operationTimes.sign.length || 0;
const report = {
timestamp: new Date().toISOString(),
config,
@@ -405,9 +435,10 @@ function generateReport() {
},
operation_times_ms: {
build_avg: metrics.operationTimes.build.reduce((a, b) => a + b, 0) / metrics.operationTimes.build.length || 0,
sign_avg: metrics.operationTimes.sign.reduce((a, b) => a + b, 0) / metrics.operationTimes.sign.length || 0,
sign_avg: signAvg,
},
resource_usage: metrics.resourceUsage,
relay_resource_usage: metrics.relayResourceUsage,
error_breakdown: metrics.errorCounts,
},
bottlenecks: [],
@@ -425,7 +456,6 @@ function generateReport() {
}
// 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.');
@@ -442,7 +472,7 @@ function generateReport() {
const reportPath = path.join(ARTIFACTS_DIR, filename);
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
// Also save as load-report.json
// Also save as load-report.json for convenience
const latestPath = path.join(ARTIFACTS_DIR, 'load-report.json');
fs.copyFileSync(reportPath, latestPath);