The relay no longer stores per-room credentials. It reads one origins-only config with a base64url master secret and derives each room name and capability token by HMAC, byte for byte with caption-local/keys.py, accepting short-lived expiring tokens. Rooms are created on first authorized join and retired when idle, so users are isolated without per-event configuration. Probes, benchmarks and tests generate their credentials from the same master; a vector test pins the derivation against the Python implementation.
76 lines
4.4 KiB
JavaScript
76 lines
4.4 KiB
JavaScript
'use strict';
|
|
// Loopback only. Synthetic captions; no inference or hosted service calls.
|
|
const {once} = require('node:events');
|
|
const {randomBytes} = require('node:crypto');
|
|
const {writeFileSync} = require('node:fs');
|
|
const {performance, monitorEventLoopDelay} = require('node:perf_hooks');
|
|
const WebSocket = require('ws');
|
|
const {createRelay, deriveRoom, deriveToken} = require('./server.cjs');
|
|
async function main() {
|
|
const seconds = Number(process.argv[2] || 60), filename = process.argv[3];
|
|
if (!filename || !Number.isFinite(seconds) || seconds < 1 || seconds > 3600)
|
|
throw new Error('Usage: node benchmark.cjs SECONDS OUTPUT.json');
|
|
const count = 32, readers = 2, hz = 5;
|
|
const masterValue = randomBytes(32).toString('base64url'), secret = Buffer.from(masterValue, 'base64url');
|
|
const expires = Math.floor(Date.now() / 1000) + 3600;
|
|
const names = Array.from({length: count}, () => deriveRoom(secret, 'source',
|
|
randomBytes(48).toString('base64url').slice(0, 64)));
|
|
const rooms = Object.fromEntries(names.map(room => [room,
|
|
{read: deriveToken(secret, 'read', room, expires), write: deriveToken(secret, 'write', room, expires)}]));
|
|
const relay = createRelay({origins: ['http://localhost:8080'], master: masterValue});
|
|
const address = await relay.listen(0), url = `ws://127.0.0.1:${address.port}`;
|
|
const writers = [], latency = [], received = new Map(); let errors = 0, sent = 0, peakRss = 0;
|
|
const eventLoop = monitorEventLoopDelay({resolution: 10});
|
|
try {
|
|
for (let i = 0; i < count; i++) {
|
|
const room = names[i];
|
|
for (let j = 0; j <= readers; j++) {
|
|
const role = j === 0 ? 'write' : 'read', ws = new WebSocket(url);
|
|
await once(ws, 'open');
|
|
const joined = once(ws, 'message'); ws.send(JSON.stringify({join: room, role, token: rooms[room][role]})); await joined;
|
|
ws.on('error', () => errors++); ws.on('close', () => errors++);
|
|
if (!j) writers.push(ws);
|
|
else {
|
|
let last = -1;
|
|
ws.on('message', raw => {
|
|
const data = JSON.parse(raw);
|
|
if (data.label !== room || data.id !== last + 1) errors++;
|
|
last = data.id;
|
|
latency.push(performance.now() - Number(data.ln));
|
|
received.set(room + ':' + j, (received.get(room + ':' + j) || 0) + 1);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
const cpuStart = process.cpuUsage(), start = performance.now(); eventLoop.enable();
|
|
const rounds = Math.floor(seconds * hz);
|
|
for (let id = 0; id < rounds; id++) {
|
|
const next = start + id * 1000/hz;
|
|
await new Promise(resolve => setTimeout(resolve, Math.max(0, next-performance.now())));
|
|
for (let i = 0; i < writers.length; i++) {
|
|
writers[i].send(JSON.stringify({msg: true, id, label: names[i], ln: String(performance.now()),
|
|
final: ['Hello world.', 'Hola, mundo.', 'Bonjour le monde.', 'Hallo Welt.', 'こんにちは。', 'مرحبا بالعالم.'][i % 6]}));
|
|
sent++;
|
|
}
|
|
peakRss = Math.max(peakRss, process.memoryUsage().rss);
|
|
}
|
|
const deadline = Date.now() + 5000;
|
|
while (latency.length < sent * readers && Date.now() < deadline)
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
const elapsed = performance.now() - start, cpu = process.cpuUsage(cpuStart); eventLoop.disable();
|
|
latency.sort((a, b) => a-b);
|
|
const pct = q => latency[Math.min(latency.length-1, Math.floor(q*latency.length))];
|
|
const result = {scope: 'loopback relay + Node producers/viewers in one process; no speech inference',
|
|
node: process.version, platform: process.platform, streams: count, viewers_per_stream: readers,
|
|
connections: count*(readers+1), caption_hz_per_stream: hz, requested_seconds: seconds,
|
|
elapsed_seconds: elapsed/1000, published: sent, delivered: latency.length,
|
|
errors, passed: errors === 0 && latency.length === sent*readers && received.size === count*readers,
|
|
latency_ms: {median: pct(.5), p95: pct(.95), p99: pct(.99), max: latency.at(-1)},
|
|
event_loop_p99_ms: eventLoop.percentile(99)/1e6, peak_rss_mib: peakRss/1048576,
|
|
mean_cpu_cores: (cpu.user+cpu.system)/(elapsed*1000), relay_counters: relay.stats};
|
|
writeFileSync(filename, JSON.stringify(result, null, 2)+'\n'); console.log(JSON.stringify(result, null, 2));
|
|
if (!result.passed) process.exitCode = 1;
|
|
} finally { eventLoop.disable(); await relay.close(); }
|
|
}
|
|
main().catch(error => { console.error(error.message); process.exitCode = 1; });
|