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