mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
304 lines
8.3 KiB
JavaScript
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);
|