OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / benchmarks / bench-raw-udx.cjs

166 lines · javascript ref:
#!/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) })