mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
319 lines
9.5 KiB
JavaScript
319 lines
9.5 KiB
JavaScript
import WebSocket from 'ws';
|
|
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools';
|
|
import { spawn } from 'child_process';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
const PROJECT_ROOT = path.resolve(__dirname, '../..');
|
|
|
|
// Configuration
|
|
const CONFIG = {
|
|
relayUrl: 'ws://localhost:8008',
|
|
numClients: 1000,
|
|
durationSeconds: 60,
|
|
eventsPerSecond: 100,
|
|
mix: {
|
|
viewEvent: 0.8,
|
|
videoPost: 0.2
|
|
},
|
|
unsafe: false
|
|
};
|
|
|
|
// Parse args
|
|
const args = process.argv.slice(2);
|
|
for (let i = 0; i < args.length; i++) {
|
|
const arg = args[i];
|
|
if (arg === '--clients') CONFIG.numClients = parseInt(args[++i], 10);
|
|
else if (arg === '--duration') CONFIG.durationSeconds = parseInt(args[++i], 10);
|
|
else if (arg === '--rate') CONFIG.eventsPerSecond = parseInt(args[++i], 10);
|
|
else if (arg === '--relay') CONFIG.relayUrl = args[++i];
|
|
else if (arg === '--unsafe') CONFIG.unsafe = true;
|
|
}
|
|
|
|
console.log('Load Test Configuration:', CONFIG);
|
|
|
|
// Guardrail
|
|
function isSafeRelay(url) {
|
|
if (CONFIG.unsafe) return true;
|
|
try {
|
|
const u = new URL(url);
|
|
return u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname === '[::1]';
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!isSafeRelay(CONFIG.relayUrl)) {
|
|
console.error('ERROR: Guardrail active. Cannot run against public relays without --unsafe flag.');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Artifacts setup
|
|
const ARTIFACTS_DIR = path.join(PROJECT_ROOT, 'artifacts');
|
|
if (!fs.existsSync(ARTIFACTS_DIR)) {
|
|
fs.mkdirSync(ARTIFACTS_DIR, { recursive: true });
|
|
}
|
|
|
|
// Helpers
|
|
function buildViewEvent(pubkey) {
|
|
return {
|
|
kind: 30079,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [
|
|
['d', 'watch-history'],
|
|
['t', 'view'],
|
|
['session', 'true']
|
|
],
|
|
content: JSON.stringify({ timestamp: Date.now(), random: Math.random() }),
|
|
pubkey
|
|
};
|
|
}
|
|
|
|
function buildVideoPostEvent(pubkey) {
|
|
return {
|
|
kind: 30078,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [
|
|
['d', Math.random().toString(36).substring(7)],
|
|
['t', 'video']
|
|
],
|
|
content: JSON.stringify({
|
|
version: 1,
|
|
title: `Load Test Video ${Math.random().toString(36)}`,
|
|
videoRootId: Math.random().toString(36),
|
|
mode: 's',
|
|
duration: 120
|
|
}),
|
|
pubkey
|
|
};
|
|
}
|
|
|
|
// State
|
|
const metrics = {
|
|
sent: 0,
|
|
ok: 0,
|
|
failed: 0,
|
|
latencies: [],
|
|
signingTimes: [],
|
|
startTime: Date.now(),
|
|
startCpu: process.cpuUsage(),
|
|
errors: []
|
|
};
|
|
|
|
// Main execution
|
|
async function main() {
|
|
let relayProcess = null;
|
|
const isLocalRelay = CONFIG.relayUrl.includes('localhost') || CONFIG.relayUrl.includes('127.0.0.1');
|
|
|
|
if (isLocalRelay) {
|
|
try {
|
|
await checkRelay(CONFIG.relayUrl);
|
|
console.log('Relay is already running.');
|
|
} catch (e) {
|
|
console.log('Relay not running, spawning simple-relay.mjs...');
|
|
const relayScript = path.join(PROJECT_ROOT, 'scripts/agent/simple-relay.mjs');
|
|
if (!fs.existsSync(relayScript)) {
|
|
console.error(`Simple relay script not found at ${relayScript}`);
|
|
process.exit(1);
|
|
}
|
|
relayProcess = spawn('node', [relayScript], {
|
|
stdio: 'inherit',
|
|
detached: false
|
|
});
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
}
|
|
}
|
|
|
|
console.log(`Simulating ${CONFIG.numClients} clients...`);
|
|
|
|
const MAX_CONNS = Math.min(CONFIG.numClients, 100);
|
|
const connections = [];
|
|
|
|
for (let i = 0; i < MAX_CONNS; i++) {
|
|
const ws = new WebSocket(CONFIG.relayUrl);
|
|
ws.on('error', (err) => {
|
|
metrics.errors.push(err.message);
|
|
});
|
|
connections.push(ws);
|
|
}
|
|
|
|
await Promise.all(connections.map(ws => new Promise(resolve => {
|
|
if (ws.readyState === WebSocket.OPEN) resolve();
|
|
else ws.on('open', resolve);
|
|
setTimeout(resolve, 1000);
|
|
})));
|
|
|
|
console.log(`${connections.length} connections established.`);
|
|
|
|
const keypairs = [];
|
|
for (let i = 0; i < CONFIG.numClients; i++) {
|
|
const sk = generateSecretKey();
|
|
keypairs.push({
|
|
sk,
|
|
pk: getPublicKey(sk)
|
|
});
|
|
}
|
|
|
|
console.log('Starting load generation...');
|
|
const intervalMs = 1000 / CONFIG.eventsPerSecond;
|
|
const endAt = Date.now() + (CONFIG.durationSeconds * 1000);
|
|
|
|
const pendingLatencies = new Map();
|
|
|
|
connections.forEach(ws => {
|
|
ws.removeAllListeners('message');
|
|
ws.on('message', (data) => {
|
|
try {
|
|
const msg = JSON.parse(data);
|
|
if (msg[0] === 'OK') {
|
|
const eventId = msg[1];
|
|
if (pendingLatencies.has(eventId)) {
|
|
const start = pendingLatencies.get(eventId);
|
|
const latency = Date.now() - start;
|
|
metrics.latencies.push(latency);
|
|
pendingLatencies.delete(eventId);
|
|
}
|
|
metrics.ok++;
|
|
} else if (msg[0] === 'NOTICE') {
|
|
metrics.errors.push(msg[1]);
|
|
}
|
|
} catch (e) {}
|
|
});
|
|
});
|
|
|
|
const loop = async () => {
|
|
while (Date.now() < endAt) {
|
|
const loopStart = Date.now();
|
|
|
|
const client = keypairs[Math.floor(Math.random() * keypairs.length)];
|
|
const ws = connections[Math.floor(Math.random() * connections.length)];
|
|
|
|
if (ws.readyState === WebSocket.OPEN) {
|
|
let eventTemplate;
|
|
if (Math.random() < CONFIG.mix.viewEvent) {
|
|
eventTemplate = buildViewEvent(client.pk);
|
|
} else {
|
|
eventTemplate = buildVideoPostEvent(client.pk);
|
|
}
|
|
|
|
try {
|
|
const signStart = process.hrtime();
|
|
const event = finalizeEvent(eventTemplate, client.sk);
|
|
const signEnd = process.hrtime(signStart);
|
|
const signMs = (signEnd[0] * 1000) + (signEnd[1] / 1e6);
|
|
metrics.signingTimes.push(signMs);
|
|
|
|
const msg = JSON.stringify(['EVENT', event]);
|
|
|
|
if (Math.random() < 0.05) {
|
|
pendingLatencies.set(event.id, Date.now());
|
|
}
|
|
|
|
ws.send(msg);
|
|
metrics.sent++;
|
|
} catch (err) {
|
|
metrics.failed++;
|
|
metrics.errors.push(err.message);
|
|
}
|
|
}
|
|
|
|
const elapsed = Date.now() - loopStart;
|
|
const targetDelay = 1000 / CONFIG.eventsPerSecond;
|
|
if (elapsed < targetDelay) {
|
|
await new Promise(r => setTimeout(r, targetDelay - elapsed));
|
|
}
|
|
}
|
|
finish(relayProcess);
|
|
};
|
|
|
|
loop();
|
|
}
|
|
|
|
function checkRelay(url) {
|
|
return new Promise((resolve, reject) => {
|
|
const ws = new WebSocket(url);
|
|
ws.on('open', () => {
|
|
ws.close();
|
|
resolve();
|
|
});
|
|
ws.on('error', (err) => {
|
|
reject(err);
|
|
});
|
|
});
|
|
}
|
|
|
|
function finish(relayProcess) {
|
|
console.log('Test finished. Generating report...');
|
|
|
|
const cpuUsage = process.cpuUsage(metrics.startCpu);
|
|
const memUsage = process.memoryUsage();
|
|
|
|
const avgLatency = metrics.latencies.reduce((a, b) => a + b, 0) / (metrics.latencies.length || 1);
|
|
const sortedLatencies = metrics.latencies.sort((a, b) => a - b);
|
|
const p95 = sortedLatencies[Math.floor(sortedLatencies.length * 0.95)] || 0;
|
|
const maxLatency = sortedLatencies[sortedLatencies.length - 1] || 0;
|
|
|
|
const avgSignTime = metrics.signingTimes.reduce((a, b) => a + b, 0) / (metrics.signingTimes.length || 1);
|
|
const sortedSignTimes = metrics.signingTimes.sort((a, b) => a - b);
|
|
const p95SignTime = sortedSignTimes[Math.floor(sortedSignTimes.length * 0.95)] || 0;
|
|
|
|
const durationSec = (Date.now() - metrics.startTime) / 1000;
|
|
const throughput = metrics.ok / durationSec;
|
|
|
|
const report = {
|
|
timestamp: new Date().toISOString(),
|
|
config: CONFIG,
|
|
metrics: {
|
|
durationSeconds: durationSec,
|
|
totalSent: metrics.sent,
|
|
totalOk: metrics.ok,
|
|
totalFailed: metrics.failed,
|
|
throughputEventsPerSec: throughput,
|
|
latencyMs: {
|
|
avg: avgLatency,
|
|
p95: p95,
|
|
max: maxLatency
|
|
},
|
|
crypto: {
|
|
avgSigningTimeMs: avgSignTime,
|
|
p95SigningTimeMs: p95SignTime
|
|
},
|
|
resources: {
|
|
cpuUser: cpuUsage.user,
|
|
cpuSystem: cpuUsage.system,
|
|
memoryRss: memUsage.rss,
|
|
memoryHeapTotal: memUsage.heapTotal,
|
|
memoryHeapUsed: memUsage.heapUsed
|
|
},
|
|
errorSample: metrics.errors.slice(0, 10)
|
|
},
|
|
bottlenecks: [
|
|
avgLatency > 1000 ? "High Average Latency (>1s)" : null,
|
|
avgSignTime > 10 ? "Cryptographic Bottleneck (Signing > 10ms)" : null,
|
|
metrics.failed > 0 ? "Failed Events Detected" : null
|
|
].filter(Boolean),
|
|
remediation: "If latency is high, consider scaling the relay. If signing is slow, review crypto library or CPU resources."
|
|
};
|
|
|
|
const dateStr = new Date().toISOString().split('T')[0].replace(/-/g, '');
|
|
const reportPath = path.join(ARTIFACTS_DIR, `load-report-${dateStr}.json`);
|
|
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
|
|
|
|
console.log(`Report saved to ${reportPath}`);
|
|
console.log(JSON.stringify(report.metrics, null, 2));
|
|
|
|
if (relayProcess) {
|
|
console.log('Stopping local relay...');
|
|
relayProcess.kill();
|
|
}
|
|
process.exit(0);
|
|
}
|
|
|
|
main().catch(err => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|