#!/usr/bin/env node
/**
* Benchmark: Raw UDX streams (what the C++ GDExtension does)
*
* Measures the raw transport layer performance:
* - Direct UDP via libudx with BBR congestion control
* - No encryption, no hole-punching
*
* Both sides share one UDX instance with one socket for localhost.
* For cross-machine: run server/client modes on separate hosts.
*
* Usage:
* node benchmarks/bench-raw-udx.cjs # localhost (both sides)
* node benchmarks/bench-raw-udx.cjs server # server mode (bind 9600)
* node benchmarks/bench-raw-udx.cjs client HOST # client connects to HOST:9600
*/
const UDX = require('udx-native')
const b4a = require('b4a')
const PACKET_SIZES = [64, 256, 512, 1024]
const DURATION = 5000
const WARMUP = 500
const PORT = 9600
function stats (latencies) {
if (latencies.length === 0) return { avg: NaN, p50: NaN, p95: NaN, p99: NaN, jitter: NaN }
const sorted = latencies.slice().sort((a, b) => a - b)
const avg = latencies.reduce((a, b) => a + b, 0) / latencies.length
const jitter = latencies.length > 1
? Math.sqrt(latencies.reduce((s, l) => s + (l - avg) ** 2, 0) / (latencies.length - 1))
: 0
return {
avg,
p50: sorted[Math.floor(sorted.length * 0.5)],
p95: sorted[Math.floor(sorted.length * 0.95)],
p99: sorted[Math.floor(sorted.length * 0.99)],
jitter
}
}
async function runLocalhost () {
const udx = new UDX()
// Single socket, two streams crossing each other
const socketA = udx.createSocket()
socketA.bind(PORT, '127.0.0.1')
const socketB = udx.createSocket()
socketB.bind(PORT + 1, '127.0.0.1')
// Stream A (server): id=1, connects to stream id=2 on socketB's port
const streamA = udx.createStream(1)
streamA.connect(socketA, 2, PORT + 1, '127.0.0.1')
streamA.on('error', (e) => console.error('streamA error:', e.message))
// Stream B (client): id=2, connects to stream id=1 on socketA's port
const streamB = udx.createStream(2)
streamB.connect(socketB, 1, PORT, '127.0.0.1')
streamB.on('error', (e) => console.error('streamB error:', e.message))
// Server side: echo pings
streamA.on('data', (buf) => {
if (buf.length >= 4 && buf.readUInt32LE(0) === 0xBEEF0001) {
streamA.write(buf)
}
})
await new Promise(r => setTimeout(r, 200))
console.log('\n=== Raw UDX Benchmark ===')
console.log('Transport: Direct UDP via libudx (BBR congestion control)')
console.log('Encryption: None')
console.log('Mode: localhost (same-machine, measures software overhead)')
console.log('')
const allResults = []
// Phase 1: Latency tests (clean stream, no buffer pressure)
const latencyResults = {}
for (const size of PACKET_SIZES) {
const latencies = []
const pingBuf = b4a.alloc(size)
pingBuf.writeUInt32LE(0xBEEF0001, 0)
await new Promise((resolve) => {
let pending = null
let measuring = false
const onData = (buf) => {
if (buf.length >= 4 && buf.readUInt32LE(0) === 0xBEEF0001 && pending) {
const rtt = Number(process.hrtime.bigint() - pending) / 1e6
if (measuring) latencies.push(rtt)
pending = null
}
}
streamB.on('data', onData)
const start = Date.now()
const iv = setInterval(() => {
const elapsed = Date.now() - start
if (elapsed > WARMUP) measuring = true
if (elapsed > WARMUP + DURATION) {
clearInterval(iv)
streamB.removeListener('data', onData)
resolve()
return
}
if (pending) return
pingBuf.writeUInt32LE(0xBEEF0001, 0)
pending = process.hrtime.bigint()
streamB.write(b4a.from(pingBuf))
}, 1)
})
latencyResults[size] = latencies
console.log(`[${size}B latency] ${latencies.length} samples, avg=${(latencies.reduce((a,b)=>a+b,0)/latencies.length).toFixed(3)}ms`)
}
// Phase 2: Throughput tests (separate pass, doesn't pollute latency)
for (const size of PACKET_SIZES) {
const dataBuf = b4a.alloc(size)
dataBuf.writeUInt32LE(0xBEEF0002, 0)
let bytes = 0, packets = 0
const tStart = Date.now()
while (Date.now() - tStart < DURATION) {
streamB.write(b4a.from(dataBuf))
bytes += size
packets++
if (packets % 1000 === 0) await new Promise(r => setImmediate(r))
}
const tMs = Date.now() - tStart
// Let stream drain before next throughput test
await new Promise(r => setTimeout(r, 200))
const latencies = latencyResults[size]
const s = stats(latencies)
const result = {
packetSize: size,
samples: latencies.length,
avgRTT: s.avg.toFixed(3),
p50RTT: s.p50.toFixed(3),
p95RTT: s.p95.toFixed(3),
p99RTT: s.p99.toFixed(3),
jitter: s.jitter.toFixed(3),
throughputMBs: ((bytes / 1e6) / (tMs / 1000)).toFixed(2),
packetsSec: Math.round(packets / (tMs / 1000))
}
allResults.push(result)
console.log(`[${size}B] RTT avg=${result.avgRTT}ms p50=${result.p50RTT}ms p95=${result.p95RTT}ms p99=${result.p99RTT}ms jitter=${result.jitter}ms | ${result.throughputMBs}MB/s ${result.packetsSec}pkt/s (${result.samples} samples)`)
}
streamA.destroy()
streamB.destroy()
await socketA.close()
await socketB.close()
console.log('\n--- JSON ---')
console.log(JSON.stringify({ transport: 'raw-udx', results: allResults }, null, 2))
}
runLocalhost().then(() => process.exit(0)).catch(e => { console.error(e); process.exit(1) })