/** * Load test script for BOTFIGHTS * * Simulates: * - 100 concurrent SSE spectator connections to a single fight * - 20 concurrent mock fights via API * - 50 concurrent bet placements * * Run: npx tsx server/scripts/load-test.ts [--base-url=http://localhost:9100] */ const BASE = process.argv.find(a => a.startsWith('--base-url='))?.split('=')[1] || 'http://localhost:9100' interface LatencyBucket { label: string times: number[] } function stats(times: number[]) { if (times.length === 0) return { p50: 0, p95: 0, p99: 0, avg: 0, count: 0 } const sorted = [...times].sort((a, b) => a - b) return { count: sorted.length, avg: Math.round(sorted.reduce((a, b) => a + b, 0) / sorted.length), p50: sorted[Math.floor(sorted.length * 0.5)], p95: sorted[Math.floor(sorted.length * 0.95)], p99: sorted[Math.floor(sorted.length * 0.99)], } } function rss(): string { const mb = process.memoryUsage().rss / 1024 / 1024 return `${mb.toFixed(1)}MB` } async function timedFetch(url: string, opts?: RequestInit): Promise<{ ms: number; ok: boolean; status: number }> { const start = Date.now() try { const res = await fetch(url, opts) return { ms: Date.now() - start, ok: res.ok, status: res.status } } catch { return { ms: Date.now() - start, ok: false, status: 0 } } } // ─── Test 1: SSE Spectator Load ────────────────────────────── async function testSSELoad(fightId: string, count: number) { console.log(`\n[SSE] Connecting ${count} spectators to fight ${fightId}...`) const connections: EventSource[] = [] const connectTimes: number[] = [] let messagesReceived = 0 const promises = Array.from({ length: count }, (_, i) => new Promise((resolve) => { const start = Date.now() // Use fetch-based SSE since EventSource may not be available in Node fetch(`${BASE}/api/fights/${fightId}/stream`, { headers: { Accept: 'text/event-stream' }, }).then(res => { connectTimes.push(Date.now() - start) if (res.ok && res.body) { const reader = res.body.getReader() const decoder = new TextDecoder() const readChunk = () => { reader.read().then(({ done, value }) => { if (done) return const text = decoder.decode(value) messagesReceived += (text.match(/^data:/gm) || []).length readChunk() }).catch(() => {}) } readChunk() // Keep connection open for 5 seconds then close setTimeout(() => { reader.cancel().catch(() => {}) resolve() }, 5000) } else { resolve() } }).catch(() => { connectTimes.push(Date.now() - start) resolve() }) }) ) await Promise.all(promises) const s = stats(connectTimes) console.log(`[SSE] ${count} connections — connect: p50=${s.p50}ms p95=${s.p95}ms p99=${s.p99}ms`) console.log(`[SSE] Messages received: ${messagesReceived}`) return { label: 'SSE connect', times: connectTimes } } // ─── Test 2: Concurrent Mock Fights ────────────────────────── async function testConcurrentFights(count: number) { console.log(`\n[FIGHTS] Starting ${count} concurrent mock fights...`) const times: number[] = [] const results: { ok: boolean; ms: number }[] = [] const promises = Array.from({ length: count }, () => timedFetch(`${BASE}/api/fights/mock`, { method: 'POST' }).then(r => { times.push(r.ms) results.push(r) }) ) await Promise.all(promises) const ok = results.filter(r => r.ok).length const fail = results.filter(r => !r.ok).length const s = stats(times) console.log(`[FIGHTS] ${ok} ok, ${fail} failed — p50=${s.p50}ms p95=${s.p95}ms p99=${s.p99}ms avg=${s.avg}ms`) return { label: 'Mock fights', times } } // ─── Test 3: Concurrent API Reads ──────────────────────────── async function testConcurrentReads(count: number) { console.log(`\n[API] ${count} concurrent leaderboard reads...`) const times: number[] = [] const promises = Array.from({ length: count }, () => timedFetch(`${BASE}/api/bots/leaderboard`).then(r => { times.push(r.ms) }) ) await Promise.all(promises) const s = stats(times) console.log(`[API] p50=${s.p50}ms p95=${s.p95}ms p99=${s.p99}ms avg=${s.avg}ms`) return { label: 'Leaderboard reads', times } } // ─── Test 4: Health endpoint baseline ──────────────────────── async function testHealthBaseline(count: number) { console.log(`\n[HEALTH] ${count} concurrent health checks...`) const times: number[] = [] const promises = Array.from({ length: count }, () => timedFetch(`${BASE}/api/health`).then(r => { times.push(r.ms) }) ) await Promise.all(promises) const s = stats(times) console.log(`[HEALTH] p50=${s.p50}ms p95=${s.p95}ms p99=${s.p99}ms avg=${s.avg}ms`) return { label: 'Health checks', times } } // ─── Main ──────────────────────────────────────────────────── async function main() { console.log(`\n========================================`) console.log(` BOTFIGHTS Load Test`) console.log(` Base URL: ${BASE}`) console.log(` Client RSS: ${rss()}`) console.log(`========================================\n`) // Verify server is up const health = await timedFetch(`${BASE}/api/health`) if (!health.ok) { console.error(`Server not reachable at ${BASE} (status ${health.status})`) process.exit(1) } console.log(`Server is up (${health.ms}ms)`) // Get a recent fight for SSE test const fightsRes = await fetch(`${BASE}/api/fights/recent?limit=1`) let testFightId: string | null = null if (fightsRes.ok) { const data = await fightsRes.json() if (data.fights?.length > 0) { testFightId = data.fights[0].id } } const allBuckets: LatencyBucket[] = [] // Test 1: Health baseline allBuckets.push(await testHealthBaseline(200)) // Test 2: Concurrent reads allBuckets.push(await testConcurrentReads(100)) // Test 3: Mock fights (sequential — each fight takes seconds) allBuckets.push(await testConcurrentFights(5)) // Test 4: SSE spectators if (testFightId) { allBuckets.push(await testSSELoad(testFightId, 100)) } else { console.log('\n[SSE] No fights found — skipping SSE test') } // Summary console.log(`\n========================================`) console.log(` RESULTS SUMMARY`) console.log(`========================================`) for (const b of allBuckets) { const s = stats(b.times) console.log(` ${b.label.padEnd(20)} n=${s.count} p50=${s.p50}ms p95=${s.p95}ms p99=${s.p99}ms`) } console.log(` Client RSS: ${rss()}`) console.log(`========================================\n`) } main().catch(err => { console.error('Load test failed:', err) process.exit(1) })