UNPKG

@babblevoice/projectrtp

Version:
211 lines (181 loc) 7.64 kB
/* * perfbench — measure projectrtp scheduler / pipeline overhead under load. * * Opens N channels with echo enabled, sends RTP into each at ptime, listens * for the echoed-back packets, and reports drop rate + latency percentiles + * CPU usage. The same harness runs against both the C++ and Rust builds — * swap `build/Release/projectrtp.node` between runs and compare. * * Usage: * CHANNELS=500 DURATION_MS=10000 node stress/perfbench.js * * Typical comparison: * # C++ build * npm run build && node stress/perfbench.js * # Rust build * (cd rust && cargo build --release) && \ * ln -f rust/target/release/libprojectrtp.so build/Release/projectrtp.node && \ * node stress/perfbench.js */ const dgram = require( "dgram" ) const os = require( "os" ) const { performance } = require( "perf_hooks" ) const { projectrtp } = require( "../index.js" ) const CHANNELS = Number( process.env.CHANNELS || 500 ) const DURATION_MS = Number( process.env.DURATION_MS || 10_000 ) const PTIME_MS = Number( process.env.PTIME_MS || 20 ) const MODE = process.env.MODE || "echo" // "echo" | "mix2" | "idle" const PAYLOAD_BYTES = 160 // PCMU @ 8kHz, 20ms const PT = 0 // PCMU function buildpk( sn, ssrc ) { const pkt = Buffer.alloc( 12 + PAYLOAD_BYTES ) pkt[ 0 ] = 0x80 pkt[ 1 ] = PT pkt.writeUInt16BE( sn & 0xffff, 2 ) pkt.writeUInt32BE( ( sn * PAYLOAD_BYTES ) >>> 0, 4 ) pkt.writeUInt32BE( ssrc >>> 0, 8 ) pkt.fill( 0xff, 12 ) // PCMU silence return pkt } function percentile( sorted, q ) { if ( sorted.length === 0 ) return 0 const idx = Math.min( sorted.length - 1, Math.floor( sorted.length * q ) ) return sorted[ idx ] } /* Memory snapshot — node's process.memoryUsage(). `rss` is resident * set size (bytes), the useful proxy for "how much RAM is Rust + * the Node shim using right now". We diff before/after channel open * to report per-channel overhead. */ function memSnapshot() { const m = process.memoryUsage() return { rss: m.rss, heap: m.heapUsed } } async function main() { projectrtp.run() const baselineMem = memSnapshot() const channels = [] const endpoints = [] const sendTimes = new Map() // key: `${chanIdx}:${sn}` → sendTime (ms) let totalSent = 0 let totalReceived = 0 const latencies = [] console.log( `perfbench: mode=${MODE} opening ${CHANNELS} channels...` ) // In "mix2" mode we open channels in pairs and mix them. Each pair // shares ONE endpoint (even-indexed channel sends; odd-indexed // receives) — simulates the typical "A calls B" bridging scenario. // Latency is measured round-trip: A's endpoint sends, the mix // forwards to B's endpoint, B echoes back, mix forwards to A. for ( let i = 0; i < CHANNELS; i++ ) { const endpoint = dgram.createSocket( "udp4" ) endpoint.bind() await new Promise( ( r ) => endpoint.on( "listening", r ) ) const port = endpoint.address().port const chan = await projectrtp.openchannel( { forcelocal: true, remote: { address: "127.0.0.1", port, codec: PT }, }, () => {} ) if ( MODE === "echo" ) chan.echo() const chanIdx = i endpoint.on( "message", ( msg ) => { if ( msg.length < 12 ) return // In mix2 mode the odd-side endpoints echo back so A hears its // own audio — same round-trip shape as echo mode for latency. if ( MODE === "mix2" && ( chanIdx % 2 === 1 ) ) { endpoint.send( msg, chan.local.port, "127.0.0.1" ) return } const sn = msg.readUInt16BE( 2 ) const key = `${chanIdx}:${sn}` const t0 = sendTimes.get( key ) if ( t0 !== undefined ) { latencies.push( performance.now() - t0 ) sendTimes.delete( key ) totalReceived++ } } ) endpoints.push( endpoint ) channels.push( chan ) } if ( MODE === "mix2" ) { for ( let i = 0; i < CHANNELS; i += 2 ) { if ( i + 1 < CHANNELS ) channels[ i ].mix( channels[ i + 1 ] ) } } const afterOpenMem = memSnapshot() const perChannelMemKb = ( ( afterOpenMem.rss - baselineMem.rss ) / CHANNELS ) / 1024 // Give channels a beat to settle before we start timing. await new Promise( ( r ) => setTimeout( r, 200 ) ) console.log( `perfbench: sending for ${DURATION_MS}ms (ptime=${PTIME_MS}ms)...` ) const cpuStart = process.cpuUsage() const wallStart = performance.now() let sn = 0 // In mix2 mode only the even-indexed endpoints originate traffic; // the odd-indexed ones just echo, so sending them packets would be // redundant and would also mess with the latency measurement. In // echo / idle modes every channel originates. const sendEvery = ( MODE === "mix2" ) ? 2 : 1 const sendTimer = setInterval( () => { const now = performance.now() if ( MODE !== "idle" ) { for ( let i = 0; i < CHANNELS; i += sendEvery ) { const pkt = buildpk( sn, 0x10000000 + i ) const key = `${i}:${sn & 0xffff}` sendTimes.set( key, now ) totalSent++ endpoints[ i ].send( pkt, channels[ i ].local.port, "127.0.0.1" ) } } sn++ if ( now - wallStart >= DURATION_MS ) { clearInterval( sendTimer ) finalize() } }, PTIME_MS ) async function finalize() { // Let in-flight echoes arrive. await new Promise( ( r ) => setTimeout( r, 500 ) ) const cpu = process.cpuUsage( cpuStart ) const wallMs = performance.now() - wallStart const peakMem = memSnapshot() for ( const ch of channels ) ch.close() for ( const ep of endpoints ) ep.close() latencies.sort( ( a, b ) => a - b ) const dropPct = totalSent ? ( ( totalSent - totalReceived ) / totalSent * 100 ) : 0 const userMs = cpu.user / 1000 const sysMs = cpu.system / 1000 const cpuMs = userMs + sysMs const coreCount = os.cpus().length console.log() console.log( `Config:` ) console.log( ` mode: ${MODE}` ) console.log( ` channels: ${CHANNELS}` ) console.log( ` duration: ${DURATION_MS}ms (wall ${wallMs.toFixed( 0 )}ms)` ) console.log( ` ptime: ${PTIME_MS}ms` ) console.log( ` cores: ${coreCount}` ) console.log() console.log( `Throughput:` ) console.log( ` sent: ${totalSent}` ) console.log( ` received: ${totalReceived}` ) console.log( ` drop rate: ${dropPct.toFixed( 3 )}%` ) console.log() console.log( `Echo latency (ms):` ) console.log( ` p50: ${percentile( latencies, 0.50 ).toFixed( 2 )}` ) console.log( ` p95: ${percentile( latencies, 0.95 ).toFixed( 2 )}` ) console.log( ` p99: ${percentile( latencies, 0.99 ).toFixed( 2 )}` ) console.log( ` max: ${percentile( latencies, 1 ).toFixed( 2 )}` ) console.log() console.log( `CPU:` ) console.log( ` user: ${userMs.toFixed( 0 )}ms` ) console.log( ` sys: ${sysMs.toFixed( 0 )}ms` ) console.log( ` cpu / wall: ${( cpuMs / wallMs * 100 ).toFixed( 1 )}% (of 1 core)` ) console.log( ` cpu / all: ${( cpuMs / wallMs / coreCount * 100 ).toFixed( 1 )}% (of ${coreCount} cores)` ) console.log() console.log( `Memory:` ) console.log( ` baseline rss: ${( baselineMem.rss / 1024 / 1024 ).toFixed( 1 )} MiB` ) console.log( ` after-open: ${( afterOpenMem.rss / 1024 / 1024 ).toFixed( 1 )} MiB` ) console.log( ` peak: ${( peakMem.rss / 1024 / 1024 ).toFixed( 1 )} MiB` ) console.log( ` per channel: ${perChannelMemKb.toFixed( 1 )} KiB` ) setTimeout( () => process.exit( 0 ), 100 ) } } main().catch( ( e ) => { console.error( e ); process.exit( 1 ) } )