OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / benchmarks / test-relay.cjs

168 lines · javascript ref:
#!/usr/bin/env node
/**
 * Hyperswarm relay test — proves game data flows through real Hyperswarm connections.
 *
 * Creates two sidecars with a local DHT testnet, discovers peers, and sends
 * ping/pong messages through the Hyperswarm encrypted stream.
 *
 * This is the Node.js equivalent of the GDScript test_relay.tscn scene.
 *
 * Usage:
 *   node benchmarks/test-relay.cjs           # local testnet (automatic)
 *   node benchmarks/test-relay.cjs real      # real DHT (needs two machines)
 */

const net = require('net')
const createTestnet = require('hyperdht/testnet')

const PING_COUNT = 50
const DISCOVERY_TIMEOUT = 20000  // DHT discovery can take 10-15s on testnet
const TEST_TIMEOUT = 60000

async function runLocal () {
  const testnet = await createTestnet(3)
  console.log('Local DHT testnet ready')

  const { startSidecar } = await import('../sidecar.js')

  const s1 = await startSidecar(3200, { bootstrap: testnet.bootstrap })
  const s2 = await startSidecar(3201, { bootstrap: testnet.bootstrap })

  const clientA = net.createConnection(3200, '127.0.0.1')
  const clientB = net.createConnection(3201, '127.0.0.1')

  await Promise.all([
    new Promise(r => clientA.on('connect', r)),
    new Promise(r => clientB.on('connect', r))
  ])

  let bufA = '', bufB = ''
  let peerOfA = ''
  let pingsSent = 0
  let pongsReceived = 0
  const rtts = []

  function sendTo (s, m) { s.write(JSON.stringify(m) + '\n') }

  function parse (buf, cb) {
    let idx
    while ((idx = buf.indexOf('\n')) !== -1) {
      const line = buf.slice(0, idx).trim()
      buf = buf.slice(idx + 1)
      if (line) try { cb(JSON.parse(line)) } catch {}
    }
    return buf
  }

  // Client A: sender — discovers peer, sends pings, collects pongs
  clientA.on('data', c => { bufA = parse(bufA + c.toString(), handleA) })
  clientB.on('data', c => { bufB = parse(bufB + c.toString(), handleB) })

  function handleA (d) {
    if (d.event === 'identity') console.log(`[sender] Identity: ${d.name} (${d.publicKey.slice(0, 12)}...)`)
    if (d.event === 'lobby-ready') console.log('[sender] Lobby ready, waiting for peer...')
    if (d.event === 'peer-discovered') {
      peerOfA = d.publicKey
      console.log(`[sender] Peer discovered: ${d.name} — starting ${PING_COUNT} pings`)
      sendPing()
    }
    if (d.event === 'relay-data' && d.data?.type === 'pong') {
      const sentUs = d.data.ts
      const nowUs = process.hrtime.bigint()
      const rttMs = Number(nowUs - BigInt(sentUs)) / 1e6
      rtts.push(rttMs)
      pongsReceived++
      if (pongsReceived <= 5 || pongsReceived % 10 === 0) {
        console.log(`  pong #${pongsReceived} RTT=${rttMs.toFixed(2)}ms`)
      }
      if (pongsReceived < PING_COUNT) sendPing()
      else finish()
    }
    if (d.event === 'error') console.log('[sender] ERROR:', d.message)
  }

  // Client B: responder — echoes pings as pongs
  function handleB (d) {
    if (d.event === 'identity') console.log(`[echo] Identity: ${d.name}`)
    if (d.event === 'peer-discovered') console.log(`[echo] Peer discovered: ${d.name}`)
    if (d.event === 'relay-data' && d.data?.type === 'ping') {
      sendTo(clientB, {
        method: 'relay',
        target: d.from,
        data: { type: 'pong', ts: d.data.ts }
      })
    }
  }

  function sendPing () {
    pingsSent++
    const ts = Number(process.hrtime.bigint())
    sendTo(clientA, {
      method: 'relay',
      target: peerOfA,
      data: { type: 'ping', ts, seq: pingsSent }
    })
  }

  async function finish () {
    console.log('')
    console.log('=== HYPERSWARM RELAY TEST RESULTS ===')
    console.log(`Pings: ${pingsSent}  Pongs: ${pongsReceived}  Loss: ${((1 - pongsReceived / pingsSent) * 100).toFixed(1)}%`)

    if (rtts.length > 0) {
      const sorted = rtts.slice().sort((a, b) => a - b)
      const avg = rtts.reduce((a, b) => a + b, 0) / rtts.length
      const p50 = sorted[Math.floor(sorted.length * 0.5)]
      const p95 = sorted[Math.floor(sorted.length * 0.95)]
      const p99 = sorted[Math.min(Math.floor(sorted.length * 0.99), sorted.length - 1)]
      const min = sorted[0]
      const max = sorted[sorted.length - 1]

      console.log(`RTT: avg=${avg.toFixed(2)}ms min=${min.toFixed(2)}ms max=${max.toFixed(2)}ms`)
      console.log(`     p50=${p50.toFixed(2)}ms p95=${p95.toFixed(2)}ms p99=${p99.toFixed(2)}ms`)
      console.log('')
      console.log('Path: Godot→IPC→Sidecar→Hyperswarm(Noise XX encrypted)→Sidecar→IPC→Godot')
      console.log('')
      console.log(pongsReceived === PING_COUNT ? 'PASS: All pongs received through Hyperswarm!' : 'PARTIAL: Some pongs lost')
    }

    console.log('\n--- JSON ---')
    console.log(JSON.stringify({
      test: 'hyperswarm-relay',
      pings: pingsSent,
      pongs: pongsReceived,
      rtts: rtts.length > 0 ? {
        avg: (rtts.reduce((a, b) => a + b, 0) / rtts.length).toFixed(3),
        min: Math.min(...rtts).toFixed(3),
        max: Math.max(...rtts).toFixed(3),
        p50: rtts.slice().sort((a, b) => a - b)[Math.floor(rtts.length * 0.5)]?.toFixed(3),
        p95: rtts.slice().sort((a, b) => a - b)[Math.floor(rtts.length * 0.95)]?.toFixed(3),
        samples: rtts.length
      } : null
    }, null, 2))

    clientA.destroy()
    clientB.destroy()
    await s1.close()
    await s2.close()
    await testnet.destroy()
    process.exit(pongsReceived >= PING_COUNT ? 0 : 1)
  }

  // Start discovery
  sendTo(clientA, { method: 'start', gameName: 'hyper-tps-relay-test' })
  await new Promise(r => setTimeout(r, 1000))
  sendTo(clientB, { method: 'start', gameName: 'hyper-tps-relay-test' })

  // Global timeout
  setTimeout(async () => {
    console.log(`\nTIMEOUT after ${TEST_TIMEOUT / 1000}s — pings:${pingsSent} pongs:${pongsReceived}`)
    if (pingsSent === 0) console.log('Peers never discovered each other (DHT too slow?)')
    clientA.destroy(); clientB.destroy()
    await s1.close(); await s2.close(); await testnet.destroy()
    process.exit(1)
  }, TEST_TIMEOUT)
}

runLocal().catch(e => { console.error(e); process.exit(1) })