OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / benchmarks / bench-hyperswarm.cjs

294 lines · javascript ref:
#!/usr/bin/env node
/**
 * Benchmark: Hyperswarm stream (hole-punched + encrypted path)
 *
 * Measures the full Hyperswarm connection stack:
 * - HyperDHT discovery + NAT hole-punching
 * - Noise XX handshake (XChaCha20-Poly1305 encryption)
 * - UDX reliable transport underneath
 *
 * This is the path game data takes through a Hyperswarm connection.
 *
 * Usage:
 *   node benchmarks/bench-hyperswarm.cjs              # localhost (local testnet)
 *   node benchmarks/bench-hyperswarm.cjs server        # server mode (real DHT)
 *   node benchmarks/bench-hyperswarm.cjs client        # client mode (real DHT)
 */

const Hyperswarm = require('hyperswarm')
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-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
  }
}

async function runLocalhost () {
  // Create local DHT testnet (no internet required)
  const testnet = await createTestnet(3)
  const bootstrap = testnet.bootstrap

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

  const startTime = Date.now()

  // Server: announce and echo pings
  const serverReady = new Promise((resolve) => {
    swarmServer.on('connection', (socket) => {
      socket.on('data', (buf) => {
        if (buf.length >= 4) {
          const tag = buf.readUInt32LE(0)
          if (tag === 0xBEEF0001) socket.write(buf)  // echo ping
        }
      })
      socket.on('error', () => {})
      resolve(socket)
    })
  })

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

  // Client: connect and run benchmark
  const clientConn = new Promise((resolve) => {
    swarmClient.on('connection', (socket) => resolve(socket))
  })

  swarmClient.join(TOPIC, { server: false, client: true })
  const conn = await clientConn
  const setupMs = Date.now() - startTime

  console.log('\n=== Hyperswarm Stream Benchmark ===')
  console.log('Transport: Hyperswarm (UDX + Noise XX encryption + HyperDHT)')
  console.log('Encryption: XChaCha20-Poly1305 (via secret-stream)')
  console.log(`Setup time: ${setupMs}ms (DHT discovery + Noise handshake)`)
  console.log('Mode: local testnet (measures encryption + framing overhead)')
  console.log('')

  const allResults = []

  // Phase 1: Latency for all sizes
  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
        }
      }
      conn.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)
          conn.removeListener('data', onData)
          resolve()
          return
        }
        if (pending) return
        pingBuf.writeUInt32LE(0xBEEF0001, 0)
        pending = process.hrtime.bigint()
        conn.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 for all sizes
  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) {
      conn.write(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)`)
  }

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

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

async function runReal (isServer) {
  const swarm = new Hyperswarm()
  const role = isServer ? 'server' : 'client'

  return new Promise((resolve) => {
    swarm.on('connection', (socket) => {
      if (isServer) {
        socket.on('data', (buf) => {
          if (buf.length >= 4 && buf.readUInt32LE(0) === 0xBEEF0001) socket.write(buf)
          if (buf.length >= 4 && buf.readUInt32LE(0) === 0xBEEF00FF) {
            socket.destroy()
            swarm.destroy().then(resolve)
          }
        })
        socket.on('error', () => {})
      } else {
        console.log(`Connected to ${socket.rawStream?.remoteHost || 'unknown'}`)
        // Run the same benchmark but with real DHT
        runBenchmarkOn(socket).then(() => {
          socket.destroy()
          swarm.destroy().then(resolve)
        })
      }
    })

    const disc = swarm.join(TOPIC, { server: isServer, client: !isServer })
    if (isServer) disc.flushed().then(() => console.log('[server] Announcing, waiting for client...'))
    else console.log('[client] Looking for server...')
  })
}

async function runBenchmarkOn (conn) {
  const allResults = []

  console.log('\n=== Hyperswarm Stream Benchmark (Real Network) ===')
  console.log('Transport: Hyperswarm (UDX + Noise XX + HyperDHT hole-punch)')
  console.log('')

  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
        }
      }
      conn.on('data', onData)
      const start = Date.now()
      const iv = setInterval(() => {
        if (Date.now() - start > WARMUP) measuring = true
        if (Date.now() - start > WARMUP + DURATION) {
          clearInterval(iv)
          conn.removeListener('data', onData)
          resolve()
          return
        }
        if (pending) return
        pingBuf.writeUInt32LE(0xBEEF0001, 0)
        pending = process.hrtime.bigint()
        conn.write(b4a.from(pingBuf))
      }, 1)
    })
    latencyResults[size] = latencies
    console.log(`[${size}B latency] ${latencies.length} samples`)
  }

  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) {
      conn.write(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`)
  }

  const doneBuf = b4a.alloc(8)
  doneBuf.writeUInt32LE(0xBEEF00FF, 0)
  conn.write(doneBuf)
  await new Promise(r => setTimeout(r, 300))

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

const mode = process.argv[2]
if (mode === 'server') {
  runReal(true).then(() => process.exit(0))
} else if (mode === 'client') {
  runReal(false).then(() => process.exit(0))
} else {
  runLocalhost().then(() => process.exit(0)).catch(e => { console.error(e); process.exit(1) })
}