// HTTP/DB and transient signaling only: this does NOT simulate real RTC pairs. import { fork } from 'node:child_process'; import { randomBytes, randomUUID } from 'node:crypto'; import { mkdir, writeFile } from 'node:fs/promises'; import { MongoClient } from 'mongodb'; import { io } from 'socket.io-client'; const pairCount = 1500, seconds = Number(process.env.LOAD_SECONDS ?? 900); const databaseName = `dragon_ball_test_load_${randomUUID().replaceAll('-', '')}`; const child = fork(new URL('./webrtc-load-server.ts', import.meta.url), { env: { ...process.env, LOAD_DATABASE: databaseName }, stdio: ['ignore', 'inherit', 'inherit', 'ipc'], windowsHide: true }); let url = '', phase = 'ramp', signalingConnections = 0, failures = 0; const metrics: any[] = [], requests: Record }> = {}; child.on('message', (message: any) => { if (message.type === 'metrics') metrics.push({ ...message, phase, at: Date.now() }); }); await new Promise((resolve, reject) => { child.on('message', (m: any) => { if (m.type === 'ready') { url = m.url; resolve(); } }); child.once('error', reject); child.once('exit', code => reject(new Error(`SERVER_EXIT_${code}`))); }); const sleep = (ms: number) => new Promise(r => setTimeout(r, Math.max(0, ms))); async function post(path: string, data: unknown, session?: any) { const start = performance.now(); let status = 0; try { const response = await fetch(url + path, { method: 'POST', headers: { 'content-type': 'application/json', ...(session ? { authorization: `Bearer ${session.token}`, 'x-game-session': session.gameSessionId } : {}) }, body: JSON.stringify(data), signal: AbortSignal.timeout(10000) }); status = response.status; const body: any = await response.json(); if (!response.ok || !body.ok) throw new Error(`HTTP_${status}_${body.code}`); return body.data; } finally { const key = phase + ':' + path.replace(/\/rooms\/[^/]+\//, '/rooms/:id/'); const entry = requests[key] ??= { ms: [], status: {} }; entry.ms.push(performance.now() - start); entry.status[status] = (entry.status[status] ?? 0) + 1; } } async function signal(session: any, room: any) { const socket = io(url + '/signal', { transports: ['websocket'], forceNew: true, reconnection: false, auth: { gameSessionId: session.gameSessionId, token: session.token, roomId: room.id } }); try { await new Promise((resolve, reject) => { socket.once('connect', () => resolve()); socket.once('connect_error', reject); }); signalingConnections++; } finally { socket.disconnect(); } } const pairs: { sessions: any[]; room: any; active: any[]; seq: number }[] = []; async function prepare(index: number) { const sessions = await Promise.all([0, 1].map(() => post('/api/game-sessions', { createRequestId: randomBytes(32).toString('base64url') }))); let room = await post('/api/rooms', { requestId: randomUUID() }, sessions[0]); room = await post('/api/rooms/join', { code: room.code }, sessions[1]); await Promise.all(sessions.map(s => signal(s, room))); const active = await Promise.all(sessions.map(s => post('/api/game-session/matches', { requestId: randomUUID(), matchId: room.currentMatchId, roomId: room.id, mode: 'friend', expectedPreviousSeq: 0 }, s))); pairs[index] = { sessions, room, active, seq: 0 }; } const inflight = new Set>(); function launch(work: Promise) { const tracked = work.catch(error => { failures++; console.error(String(error)); }).finally(() => inflight.delete(tracked)); inflight.add(tracked); } const started = Date.now(); try { for (let i = 0; i < pairCount; i++) { await sleep(started + i * 40 - Date.now()); launch(prepare(i)); } await Promise.all(inflight); if (pairs.filter(Boolean).length !== pairCount) throw new Error('RAMP_FAILED'); const rampMs = Date.now() - started; console.log(JSON.stringify({ phase: 'ramp-complete', sessions: pairCount * 2, signalingConnections, rampMs })); phase = 'steady'; const steadyStart = Date.now(); let duplicateResults = 0; async function cycle(index: number) { const p = pairs[index % pairCount]; const results = p.active.map((active, i) => ({ ...active, outcome: i === 0 ? 'win' : 'loss', reason: 'hit' })); await Promise.all(p.sessions.flatMap((s, i) => [post('/api/game-session/results', results[i], s), post('/api/analytics/activity', { visible: true }, s)])); if (index % 10 === 0) { await Promise.all(p.sessions.map((s, i) => post('/api/game-session/results', results[i], s))); duplicateResults += 2; } const round = p.room.round; const rooms = await Promise.all(p.sessions.map(s => post('/api/rooms/' + p.room.id + '/rematch', { round, requestId: randomUUID() }, s))); p.room = rooms.sort((a, b) => b.round - a.round)[0]; p.seq++; p.active = await Promise.all(p.sessions.map(s => post('/api/game-session/matches', { requestId: randomUUID(), matchId: p.room.currentMatchId, roomId: p.room.id, mode: 'friend', expectedPreviousSeq: p.seq }, s))); } for (let i = 0; i < seconds * 25; i++) { await sleep(steadyStart + i * 40 - Date.now()); launch(cycle(i)); if (i && i % 1500 === 0) console.log(JSON.stringify({ phase, elapsedSeconds: Math.round((Date.now() - steadyStart) / 1000), completedPairCycles: i, inflight: inflight.size, failures })); } await Promise.all(inflight); const elapsedMs = Date.now() - steadyStart; const inspect = await new MongoClient(process.env.TEST_MONGODB_URI!).connect(); try { const db = inspect.db(databaseName); const totals = await db.collection('game_sessions').aggregate([{ $group: { _id: null, sessions: { $sum: 1 }, wins: { $sum: '$stats.friend.wins' }, losses: { $sum: '$stats.friend.losses' }, best: { $max: '$stats.friend.best' } } }]).toArray(); const percent = (values: number[], n: number) => values.sort((a,b) => a-b)[Math.min(values.length-1, Math.floor(values.length*n))] ?? null; const summary = Object.fromEntries(Object.entries(requests).map(([key,v]) => [key, { count: v.ms.length, status: v.status, p50: percent(v.ms,.5), p95: percent(v.ms,.95), p99: percent(v.ms,.99) }])); const steady = metrics.filter(m => m.phase === 'steady'); const output = { scope: 'HTTP + Standalone MongoDB + transient signaling; no real RTC load or TURN', sessions: 3000, pairs: pairCount, requestedSeconds: seconds, elapsedMs, rampMs, signalingConnections, duplicateResults, failures, totals, legacyMatches: await db.collection('matches').countDocuments(), resources: { peakRssBytes: Math.max(...steady.map(m => m.rss)), meanCpuPercentOneCore: steady.reduce((n,m) => n+m.cpuPercent,0)/steady.length, peakWebSockets: Math.max(...metrics.map(m=>m.peakSockets)), steadyPeakWebSockets: Math.max(...steady.map(m=>m.sockets)), writeMethodCalls: metrics.at(-1)?.writes }, summary, metrics }; await mkdir('docs/verification', { recursive: true }); await writeFile('docs/verification/webrtc-load.json', JSON.stringify(output, null, 2)); console.log(JSON.stringify({ ...output, metrics: undefined })); // This invocation exclusively owns this randomly named test database. await db.dropDatabase(); } finally { await inspect.close(); } } finally { child.send('stop'); }