OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / benchmarks / bench-protomux.cjs

200 lines · javascript ref:
#!/usr/bin/env node
/**
 * Benchmark: Protomux channel over Hyperswarm
 *
 * Measures game data via a dedicated protomux protocol channel:
 * - Hyperswarm connection (UDX + Noise encryption + hole-punching)
 * - Protomux multiplexing (protocol/channel framing overhead)
 * - compact-encoding for binary message encoding
 *
 * This is the "multiplexed game protocol" approach — game data runs
 * on a dedicated channel alongside discovery metadata.
 *
 * Usage:
 *   node benchmarks/bench-protomux.cjs              # localhost (local testnet)
 *   node benchmarks/bench-protomux.cjs server        # server mode (real DHT)
 *   node benchmarks/bench-protomux.cjs client        # client mode (real DHT)
 */

const Hyperswarm = require('hyperswarm')
const Protomux = require('protomux')
const c = require('compact-encoding')
const createTestnet = require('hyperdht/testnet')
const b4a = require('b4a')
const crypto = require('crypto')

const PACKET_SIZES = [64, 256, 512, 1024]
const DURATION = 5000
const WARMUP = 500
const TOPIC = crypto.createHash('sha256').update('hyper-tps-bench-protomux-2026').digest()

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
  }
}

function createChannel (mux, role, onPing, onPong) {
  const ch = mux.createChannel({
    protocol: 'hyper-tps-game',
    id: b4a.from('bench'),
    messages: [
      { encoding: c.raw, onmessage: onPing || (() => {}) },   // 0: ping
      { encoding: c.raw, onmessage: onPong || (() => {}) },   // 1: pong
      { encoding: c.raw, onmessage: () => {} }                 // 2: data (throughput)
    ]
  })
  ch.open()
  return ch
}

async function runLocalhost () {
  const testnet = await createTestnet(3)
  const bootstrap = testnet.bootstrap

  const swarmServer = new Hyperswarm({ bootstrap })
  const swarmClient = new Hyperswarm({ bootstrap })

  const startTime = Date.now()

  // Server: echo pings via pong channel
  const serverReady = new Promise((resolve) => {
    swarmServer.on('connection', (socket) => {
      const mux = Protomux.from(socket)
      let ch = null
      ch = createChannel(mux, 'server', (buf) => {
        // ping received → send as pong
        ch.messages[1].send(buf)
      })
      socket.on('error', () => {})
      resolve()
    })
  })

  const disc = swarmServer.join(TOPIC, { server: true, client: false })
  await disc.flushed()

  // Client: connect and get channel
  const clientResult = new Promise((resolve) => {
    swarmClient.on('connection', (socket) => {
      const mux = Protomux.from(socket)
      resolve({ socket, mux })
    })
  })

  swarmClient.join(TOPIC, { server: false, client: true })
  const { socket: conn, mux } = await clientResult
  await serverReady

  const setupMs = Date.now() - startTime

  // Create client channel with pong handler
  let pongHandler = () => {}
  const ch = createChannel(mux, 'client', null, (buf) => pongHandler(buf))

  // Wait for channel to fully open
  await new Promise(r => setTimeout(r, 300))

  console.log('\n=== Protomux Channel Benchmark ===')
  console.log('Transport: Hyperswarm + Protomux (multiplexed game protocol)')
  console.log('Encryption: XChaCha20-Poly1305 (via secret-stream)')
  console.log('Encoding: raw binary via protomux messages')
  console.log(`Setup time: ${setupMs}ms (DHT + Noise + protomux channel open)`)
  console.log('Mode: local testnet (measures mux + encoding overhead)')
  console.log('')

  const allResults = []

  // Phase 1: Latency
  const latencyResults = {}
  for (const size of PACKET_SIZES) {
    const latencies = []
    const pingBuf = b4a.alloc(size)

    await new Promise((resolve) => {
      let pending = null
      let measuring = false

      pongHandler = (buf) => {
        if (pending) {
          const rtt = Number(process.hrtime.bigint() - pending) / 1e6
          if (measuring) latencies.push(rtt)
          pending = null
        }
      }

      const start = Date.now()
      const iv = setInterval(() => {
        const elapsed = Date.now() - start
        if (elapsed > WARMUP) measuring = true
        if (elapsed > WARMUP + DURATION) {
          clearInterval(iv)
          pongHandler = () => {}
          resolve()
          return
        }
        if (pending) return
        pending = process.hrtime.bigint()
        ch.messages[0].send(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
  for (const size of PACKET_SIZES) {
    const dataBuf = b4a.alloc(size)
    let bytes = 0, packets = 0

    const tStart = Date.now()
    while (Date.now() - tStart < DURATION) {
      ch.messages[2].send(b4a.from(dataBuf))
      bytes += size
      packets++
      if (packets % 1000 === 0) await new Promise(r => setImmediate(r))
    }
    const tMs = Date.now() - tStart
    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)`)
  }

  ch.close()
  conn.destroy()
  await swarmClient.destroy()
  await swarmServer.destroy()
  await testnet.destroy()

  console.log('\n--- JSON ---')
  console.log(JSON.stringify({ transport: 'protomux', setupMs, results: allResults }, null, 2))
}

runLocalhost().then(() => process.exit(0)).catch(e => { console.error(e); process.exit(1) })