#!/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) })