Created
June 16, 2025 11:50
-
-
Save nazarhussain/38c7c0c782cab065c95c96fe19887387 to your computer and use it in GitHub Desktop.
Perfomance comparison for `Bun.spawn` vs `node:child_process` compatibility layer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bun | |
import { spawn, exec } from 'node:child_process'; | |
import { promisify } from 'node:util'; | |
const execAsync = promisify(exec); | |
// Utility functions | |
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); | |
const formatBytes = (bytes) => { | |
const sizes = ['B', 'KB', 'MB', 'GB']; | |
if (bytes === 0) return '0 B'; | |
const i = Math.floor(Math.log(bytes) / Math.log(1024)); | |
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; | |
}; | |
const getMemoryUsage = () => { | |
const usage = process.memoryUsage(); | |
return { | |
rss: usage.rss, | |
heapUsed: usage.heapUsed, | |
heapTotal: usage.heapTotal, | |
external: usage.external | |
}; | |
}; | |
const calculateStats = (times) => { | |
const sorted = times.sort((a, b) => a - b); | |
const len = sorted.length; | |
const sum = sorted.reduce((a, b) => a + b, 0); | |
return { | |
min: sorted[0], | |
max: sorted[len - 1], | |
mean: sum / len, | |
median: len % 2 === 0 ? (sorted[len/2-1] + sorted[len/2]) / 2 : sorted[Math.floor(len/2)], | |
p95: sorted[Math.floor(len * 0.95)], | |
p99: sorted[Math.floor(len * 0.99)] | |
}; | |
}; | |
// Benchmark functions using Bun.spawn | |
const bunSpawnBenchmarks = { | |
async simpleCommand() { | |
const start = performance.now(); | |
const proc = Bun.spawn(['echo', 'hello']); | |
await proc.exited; | |
return performance.now() - start; | |
}, | |
async longRunningProcess() { | |
const start = performance.now(); | |
const proc = Bun.spawn(['sleep', '0.5']); | |
await proc.exited; | |
return performance.now() - start; | |
}, | |
async ioHeavyOperation() { | |
const start = performance.now(); | |
const proc = Bun.spawn(['find', '/tmp', '-name', '*.txt'], { | |
stdout: 'pipe', | |
stderr: 'pipe' | |
}); | |
await proc.exited; | |
return performance.now() - start; | |
}, | |
async nodeSubprocess() { | |
const start = performance.now(); | |
const proc = Bun.spawn(['node', '-e', 'console.log("test")'], { | |
stdout: 'pipe' | |
}); | |
await proc.exited; | |
return performance.now() - start; | |
}, | |
async withStdio() { | |
const start = performance.now(); | |
const proc = Bun.spawn(['cat'], { | |
stdin: 'pipe', | |
stdout: 'pipe' | |
}); | |
proc.stdin.write('hello world\n'); | |
proc.stdin.end(); | |
await proc.exited; | |
return performance.now() - start; | |
} | |
}; | |
// Benchmark functions using node:child_process | |
const nodeSpawnBenchmarks = { | |
async simpleCommand() { | |
const start = performance.now(); | |
return new Promise((resolve, reject) => { | |
const proc = spawn('echo', ['hello']); | |
proc.on('close', () => { | |
resolve(performance.now() - start); | |
}); | |
proc.on('error', reject); | |
}); | |
}, | |
async longRunningProcess() { | |
const start = performance.now(); | |
return new Promise((resolve, reject) => { | |
const proc = spawn('sleep', ['0.5']); | |
proc.on('close', () => { | |
resolve(performance.now() - start); | |
}); | |
proc.on('error', reject); | |
}); | |
}, | |
async ioHeavyOperation() { | |
const start = performance.now(); | |
return new Promise((resolve, reject) => { | |
const proc = spawn('find', ['/tmp', '-name', '*.txt']); | |
proc.on('close', () => { | |
resolve(performance.now() - start); | |
}); | |
proc.on('error', reject); | |
}); | |
}, | |
async nodeSubprocess() { | |
const start = performance.now(); | |
return new Promise((resolve, reject) => { | |
const proc = spawn('node', ['-e', 'console.log("test")']); | |
proc.on('close', () => { | |
resolve(performance.now() - start); | |
}); | |
proc.on('error', reject); | |
}); | |
}, | |
async withStdio() { | |
const start = performance.now(); | |
return new Promise((resolve, reject) => { | |
const proc = spawn('cat', [], { stdio: 'pipe' }); | |
proc.stdin.write('hello world\n'); | |
proc.stdin.end(); | |
proc.on('close', () => { | |
resolve(performance.now() - start); | |
}); | |
proc.on('error', reject); | |
}); | |
} | |
}; | |
// Concurrent benchmarks | |
const concurrentBenchmarks = { | |
async bunConcurrent(count = 10) { | |
const start = performance.now(); | |
const promises = Array.from({ length: count }, () => { | |
const proc = Bun.spawn(['echo', 'concurrent']); | |
return proc.exited; | |
}); | |
await Promise.all(promises); | |
return performance.now() - start; | |
}, | |
async nodeConcurrent(count = 10) { | |
const start = performance.now(); | |
const promises = Array.from({ length: count }, () => { | |
return new Promise((resolve, reject) => { | |
const proc = spawn('echo', ['concurrent']); | |
proc.on('close', resolve); | |
proc.on('error', reject); | |
}); | |
}); | |
await Promise.all(promises); | |
return performance.now() - start; | |
} | |
}; | |
// Sequential rapid spawning benchmarks | |
const rapidSequentialBenchmarks = { | |
async bunRapidSequential(count = 50) { | |
const times = []; | |
for (let i = 0; i < count; i++) { | |
const start = performance.now(); | |
const proc = Bun.spawn(['echo', `test-${i}`]); | |
await proc.exited; | |
times.push(performance.now() - start); | |
} | |
return times; | |
}, | |
async nodeRapidSequential(count = 50) { | |
const times = []; | |
for (let i = 0; i < count; i++) { | |
const start = performance.now(); | |
const time = await new Promise((resolve, reject) => { | |
const proc = spawn('echo', [`test-${i}`]); | |
proc.on('close', () => { | |
resolve(performance.now() - start); | |
}); | |
proc.on('error', reject); | |
}); | |
times.push(time); | |
} | |
return times; | |
} | |
}; | |
// Main benchmark runner | |
async function runBenchmark(name, bunFn, nodeFn, iterations = 100) { | |
console.log(`\nπ₯ Running ${name} benchmark (${iterations} iterations)`); | |
console.log('=' .repeat(60)); | |
// Warmup | |
console.log('Warming up...'); | |
for (let i = 0; i < 5; i++) { | |
try { await bunFn(); } catch {} | |
try { await nodeFn(); } catch {} | |
} | |
// Force garbage collection if available | |
if (global.gc) { | |
global.gc(); | |
} | |
const bunTimes = []; | |
const nodeTimes = []; | |
const bunMemory = []; | |
const nodeMemory = []; | |
// Bun.spawn benchmark | |
console.log('Testing Bun.spawn...'); | |
for (let i = 0; i < iterations; i++) { | |
const memBefore = getMemoryUsage(); | |
try { | |
const time = await bunFn(); | |
bunTimes.push(time); | |
const memAfter = getMemoryUsage(); | |
bunMemory.push(memAfter.rss - memBefore.rss); | |
} catch (error) { | |
console.warn(`Bun iteration ${i} failed:`, error.message); | |
} | |
} | |
// node:child_process benchmark | |
console.log('Testing node:child_process...'); | |
for (let i = 0; i < iterations; i++) { | |
const memBefore = getMemoryUsage(); | |
try { | |
const time = await nodeFn(); | |
nodeTimes.push(time); | |
const memAfter = getMemoryUsage(); | |
nodeMemory.push(memAfter.rss - memBefore.rss); | |
} catch (error) { | |
console.warn(`Node iteration ${i} failed:`, error.message); | |
} | |
} | |
// Calculate and display results | |
const bunStats = calculateStats(bunTimes); | |
const nodeStats = calculateStats(nodeTimes); | |
const bunMemStats = calculateStats(bunMemory); | |
const nodeMemStats = calculateStats(nodeMemory); | |
console.log('\nπ Results:'); | |
console.log('\nTime (ms):'); | |
console.log(`Bun.spawn - Mean: ${bunStats.mean.toFixed(2)}, Median: ${bunStats.median.toFixed(2)}, P95: ${bunStats.p95.toFixed(2)}, P99: ${bunStats.p99.toFixed(2)}`); | |
console.log(`node:child_process - Mean: ${nodeStats.mean.toFixed(2)}, Median: ${nodeStats.median.toFixed(2)}, P95: ${nodeStats.p95.toFixed(2)}, P99: ${nodeStats.p99.toFixed(2)}`); | |
const speedup = nodeStats.mean / bunStats.mean; | |
console.log(`\nπ Bun.spawn is ${speedup.toFixed(2)}x ${speedup > 1 ? 'faster' : 'slower'} than node:child_process`); | |
console.log('\nMemory Delta:'); | |
console.log(`Bun.spawn - Mean: ${formatBytes(bunMemStats.mean)}, P95: ${formatBytes(bunMemStats.p95)}`); | |
console.log(`node:child_process - Mean: ${formatBytes(nodeMemStats.mean)}, P95: ${formatBytes(nodeMemStats.p95)}`); | |
} | |
async function runConcurrentBenchmark(name, bunFn, nodeFn, counts = [10, 50, 100]) { | |
console.log(`\nπ₯ Running ${name} concurrent benchmark`); | |
console.log('=' .repeat(60)); | |
for (const count of counts) { | |
console.log(`\nTesting with ${count} concurrent processes:`); | |
// Bun test | |
const bunTimes = []; | |
for (let i = 0; i < 5; i++) { | |
const time = await bunFn(count); | |
bunTimes.push(time); | |
} | |
const bunStats = calculateStats(bunTimes); | |
// Node test | |
const nodeTimes = []; | |
for (let i = 0; i < 5; i++) { | |
const time = await nodeFn(count); | |
nodeTimes.push(time); | |
} | |
const nodeStats = calculateStats(nodeTimes); | |
console.log(`Bun.spawn: ${bunStats.mean.toFixed(2)}ms (${(count / (bunStats.mean / 1000)).toFixed(0)} processes/sec)`); | |
console.log(`node:child_process: ${nodeStats.mean.toFixed(2)}ms (${(count / (nodeStats.mean / 1000)).toFixed(0)} processes/sec)`); | |
const speedup = nodeStats.mean / bunStats.mean; | |
console.log(`Bun is ${speedup.toFixed(2)}x ${speedup > 1 ? 'faster' : 'slower'}`); | |
} | |
} | |
async function runRapidSequentialBenchmark() { | |
console.log(`\nπ₯ Running rapid sequential benchmark`); | |
console.log('=' .repeat(60)); | |
const count = 50; | |
console.log('Testing Bun.spawn rapid sequential...'); | |
const bunTimes = await rapidSequentialBenchmarks.bunRapidSequential(count); | |
console.log('Testing node:child_process rapid sequential...'); | |
const nodeTimes = await rapidSequentialBenchmarks.nodeRapidSequential(count); | |
const bunStats = calculateStats(bunTimes); | |
const nodeStats = calculateStats(nodeTimes); | |
console.log('\nπ Rapid Sequential Results:'); | |
console.log(`Bun.spawn - Mean: ${bunStats.mean.toFixed(2)}ms, P95: ${bunStats.p95.toFixed(2)}ms`); | |
console.log(`node:child_process - Mean: ${nodeStats.mean.toFixed(2)}ms, P95: ${nodeStats.p95.toFixed(2)}ms`); | |
const speedup = nodeStats.mean / bunStats.mean; | |
console.log(`\nπ Bun.spawn is ${speedup.toFixed(2)}x ${speedup > 1 ? 'faster' : 'slower'} than node:child_process`); | |
} | |
// Main execution | |
async function main() { | |
console.log('π§ͺ Bun.spawn vs node:child_process Performance Benchmark'); | |
console.log(`Running on Bun ${Bun.version}`); | |
console.log(`Node.js compatibility layer enabled`); | |
try { | |
// Individual benchmarks | |
await runBenchmark('Simple Command', bunSpawnBenchmarks.simpleCommand, nodeSpawnBenchmarks.simpleCommand); | |
await runBenchmark('Long Running Process', bunSpawnBenchmarks.longRunningProcess, nodeSpawnBenchmarks.longRunningProcess); | |
await runBenchmark('I/O Heavy Operation', bunSpawnBenchmarks.ioHeavyOperation, nodeSpawnBenchmarks.ioHeavyOperation); | |
await runBenchmark('Node.js Subprocess', bunSpawnBenchmarks.nodeSubprocess, nodeSpawnBenchmarks.nodeSubprocess); | |
await runBenchmark('With stdio', bunSpawnBenchmarks.withStdio, nodeSpawnBenchmarks.withStdio); | |
// Concurrent benchmarks | |
await runConcurrentBenchmark('Concurrent', concurrentBenchmarks.bunConcurrent, concurrentBenchmarks.nodeConcurrent); | |
// Rapid sequential benchmark | |
await runRapidSequentialBenchmark(); | |
console.log('\nβ Benchmark completed!'); | |
console.log('\nπ‘ Tips:'); | |
console.log('- Run with --expose-gc for more accurate memory measurements'); | |
console.log('- Run multiple times and average results for production decisions'); | |
console.log('- Consider your specific use case when interpreting results'); | |
} catch (error) { | |
console.error('β Benchmark failed:', error); | |
process.exit(1); | |
} | |
} | |
// Run the benchmark | |
if (import.meta.main) { | |
main(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Results on
Darwin Nazars-MacBook-Pro-2.fritz.box 24.5.0 Darwin Kernel Version 24.5.0: Tue Apr 22 19:54:29 PDT 2025; root:xnu-11417.121.6~2/RELEASE_ARM64_T6030 arm64