Files
bitvid/scripts/agent/load-test.mjs
T

304 lines
8.3 KiB
JavaScript

import { WebSocket } from "ws";
import { generateSecretKey, getPublicKey, finalizeEvent } from "nostr-tools";
import { startRelay } from "./simple-relay.mjs";
import fs from "fs";
import path from "path";
import { performance } from "perf_hooks";
// Parse args
const args = process.argv.slice(2);
const getArg = (name, def) => {
const idx = args.indexOf(name);
return idx !== -1 ? args[idx + 1] : def;
};
const DURATION_MS = parseInt(getArg("--duration", "60000")); // 1 minute
const N_CLIENTS = parseInt(getArg("--clients", "100")); // Default 100 for safety
const RELAY_URL = getArg("--relay", null);
const RATE_PER_CLIENT = parseFloat(getArg("--rate", "0.1")); // events/sec
console.log(`Load Test Config:
Duration: ${DURATION_MS}ms
Clients: ${N_CLIENTS}
Relay: ${RELAY_URL || "Internal (localhost:8888)"}
Rate: ${RATE_PER_CLIENT} ev/s/client
`);
async function main() {
let relayServer = null;
let targetUrl = RELAY_URL;
if (!targetUrl) {
relayServer = startRelay(8888);
targetUrl = "ws://localhost:8888";
}
// Setup metrics
const latencies = [];
let sentCount = 0;
let okCount = 0;
let errorCount = 0;
const errors = {};
// Hot functions profiling
const buildTimes = [];
const signTimes = [];
const clients = [];
// Start clients
console.log("Connecting clients...");
for (let i = 0; i < N_CLIENTS; i++) {
clients.push(createClient(targetUrl, i));
}
await Promise.all(clients.map((c) => c.connect()));
console.log("All clients connected.");
// Start load
console.log("Starting load...");
const startTime = Date.now();
const interval = setInterval(() => {
reportMetrics();
}, 5000);
clients.forEach((c) => c.startLoad());
// Wait for duration
await new Promise((resolve) => setTimeout(resolve, DURATION_MS));
// Stop load
console.log("Stopping load...");
clearInterval(interval);
clients.forEach((c) => c.stopLoad());
// Wait a bit for pending OKs
await new Promise((resolve) => setTimeout(resolve, 2000));
// Disconnect
clients.forEach((c) => c.close());
if (relayServer) {
await relayServer.close();
}
// Generate Report
const endTime = Date.now();
const actualDuration = (endTime - startTime) / 1000;
latencies.sort((a, b) => a - b);
const avgLatency =
latencies.reduce((a, b) => a + b, 0) / latencies.length || 0;
const p95Latency = latencies[Math.floor(latencies.length * 0.95)] || 0;
const p99Latency = latencies[Math.floor(latencies.length * 0.99)] || 0;
const avgBuildTime = buildTimes.reduce((a, b) => a + b, 0) / buildTimes.length || 0;
const avgSignTime = signTimes.reduce((a, b) => a + b, 0) / signTimes.length || 0;
const report = {
timestamp: new Date().toISOString(),
config: {
durationMs: DURATION_MS,
clients: N_CLIENTS,
relay: targetUrl,
rate: RATE_PER_CLIENT
},
metrics: {
totalSent: sentCount,
totalOk: okCount,
totalErrors: errorCount,
throughput: okCount / actualDuration,
latency: {
avg: avgLatency,
p95: p95Latency,
p99: p99Latency,
min: latencies[0] || 0,
max: latencies[latencies.length - 1] || 0
},
resources: {
cpu: process.cpuUsage(),
memory: process.memoryUsage()
}
},
hot_functions: [
{ name: 'buildEvent', avg_ms: avgBuildTime },
{ name: 'signEvent', avg_ms: avgSignTime }
],
errors: errors,
recommendations: []
};
if (p95Latency > 200) {
report.recommendations.push(
"High latency detected (>200ms p95). Consider scaling relay or reducing batch size."
);
}
if (errorCount > 0) {
report.recommendations.push(
"Errors detected. Check error logs and relay stability."
);
}
if (report.metrics.throughput < N_CLIENTS * RATE_PER_CLIENT * 0.8) {
report.recommendations.push(
"Throughput is significantly lower than target rate. Possible bottleneck."
);
}
if (avgSignTime > 5) { // 5ms per signature is quite high for main thread loop
report.recommendations.push(
"Cryptographic bottleneck detected: signEvent avg > 5ms. Consider offloading to workers."
);
}
const artifactsDir = path.resolve("artifacts");
if (!fs.existsSync(artifactsDir)) {
fs.mkdirSync(artifactsDir);
}
const dateStr = new Date().toISOString().split("T")[0].replace(/-/g, "");
const reportPath = path.join(artifactsDir, `load-report-${dateStr}.json`);
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log(`Report written to ${reportPath}`);
// Exit
process.exit(0);
function reportMetrics() {
const mem = process.memoryUsage();
console.log(
`[Status] Sent: ${sentCount}, OK: ${okCount}, Errs: ${errorCount}, Heap: ${(mem.heapUsed / 1024 / 1024).toFixed(2)}MB`
);
}
function createClient(url, index) {
let ws;
let timer;
let connected = false;
const sk = generateSecretKey();
const pk = getPublicKey(sk);
const pending = new Map(); // eventId -> timestamp
return {
connect: () =>
new Promise((resolve) => {
ws = new WebSocket(url);
ws.on("open", () => {
connected = true;
resolve();
});
ws.on("message", (data) => {
try {
const msg = JSON.parse(data);
if (msg[0] === "OK") {
const eventId = msg[1];
const ok = msg[2];
const reason = msg[3];
if (pending.has(eventId)) {
const start = pending.get(eventId);
const lat = Date.now() - start;
latencies.push(lat);
pending.delete(eventId);
if (ok) okCount++;
else {
errorCount++;
errors[reason] = (errors[reason] || 0) + 1;
}
}
} else if (msg[0] === "NOTICE") {
errorCount++;
errors["NOTICE: " + msg[1]] =
(errors["NOTICE: " + msg[1]] || 0) + 1;
}
} catch (e) {
// ignore
}
});
ws.on("error", (e) => {
errorCount++;
errors[e.message] = (errors[e.message] || 0) + 1;
resolve(); // resolve anyway to not block
});
ws.on("close", () => {
connected = false;
});
}),
startLoad: () => {
if (!connected) return;
const intervalMs = 1000 / RATE_PER_CLIENT;
timer = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
const isLarge = Math.random() > 0.8; // 20% large events
const event = isLarge
? createVideoPost(pk, sk)
: createViewEvent(pk, sk);
pending.set(event.id, Date.now());
ws.send(JSON.stringify(["EVENT", event]));
sentCount++;
}
}, intervalMs);
},
stopLoad: () => {
if (timer) clearInterval(timer);
},
close: () => {
if (ws) ws.close();
}
};
}
function createViewEvent(pk, sk) {
const startBuild = performance.now();
const event = {
kind: 30079,
created_at: Math.floor(Date.now() / 1000),
tags: [["d", "test-view"]],
content: "",
pubkey: pk
};
const endBuild = performance.now();
buildTimes.push(endBuild - startBuild);
const startSign = performance.now();
const signed = finalizeEvent(event, sk);
const endSign = performance.now();
signTimes.push(endSign - startSign);
return signed;
}
function createVideoPost(pk, sk) {
const startBuild = performance.now();
const event = {
kind: 30078,
created_at: Math.floor(Date.now() / 1000),
tags: [
["d", "video-1"],
["title", "Test Video"],
["t", "video"]
],
content: JSON.stringify({
title: "Test Video",
description: "A large description ".repeat(100),
url: "https://example.com/video.mp4"
}),
pubkey: pk
};
const endBuild = performance.now();
buildTimes.push(endBuild - startBuild);
const startSign = performance.now();
const signed = finalizeEvent(event, sk);
const endSign = performance.now();
signTimes.push(endSign - startSign);
return signed;
}
}
main().catch(console.error);