Files
2026-03-28 02:53:32 -04:00

128 lines
4.8 KiB
TypeScript

import { spawn, spawnSync } from "bun";
/**
* Checks if a port is reachable to detect pre-existing servers.
*/
async function isPortReachable(url: string): Promise<boolean> {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 200);
const res = await fetch(url, { signal: controller.signal });
clearTimeout(timeout);
return true;
} catch {
return false;
}
}
/**
* Measures the cold start time of a command by polling an HTTP endpoint.
*/
async function measureColdStart(command: string, url: string, timeout = 30000) {
const startTime = performance.now();
const child = spawn({
cmd: [process.platform === "win32" ? "cmd" : "sh", process.platform === "win32" ? "/c" : "-c", command],
stdout: "ignore",
stderr: "ignore",
stdin: "ignore"
});
let responseTime = -1;
const pollStart = performance.now();
try {
while (performance.now() - pollStart < timeout) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 100);
const res = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
if (res.ok || res.status < 500) {
responseTime = performance.now() - startTime;
break;
}
} catch (e) {}
if (child.exitCode !== null) {
console.error(`\x1b[31mError: Command exited prematurely with code ${child.exitCode}\x1b[0m`);
return -2;
}
await Bun.sleep(5);
}
} catch (err) {
console.error(`\x1b[31mBenchmark error: ${err}\x1b[0m`);
} finally {
// Robust cleanup: kill entire process tree on Windows
if (process.platform === "win32") {
spawnSync(["taskkill", "/F", "/T", "/PID", child.pid.toString()]);
} else {
child.kill();
}
}
return responseTime;
}
async function main() {
const args = Bun.argv.slice(2);
const cmdIndex = args.indexOf("--cmd");
const urlIndex = args.indexOf("--url");
const runsIndex = args.indexOf("--runs");
const command = cmdIndex !== -1 ? args[cmdIndex + 1] : args[0];
const url = urlIndex !== -1 ? args[urlIndex + 1] : args[1];
const runs = runsIndex !== -1 ? parseInt(args[runsIndex + 1] ?? "1", 10) : (args[2] ? parseInt(args[2], 10) : 1);
if (!command || !url) {
console.log(`\n\x1b[1;35m⚡ BUN COLD START BENCHMARK\x1b[0m\nUsage: bun benchmark.ts <command> <url> [runs]\n`);
process.exit(1);
return;
}
// Check if port is already occupied
if (await isPortReachable(url)) {
console.error(`\x1b[31mError: Target URL ${url} is already responding.\x1b[0m`);
console.log(`Please ensure the server is NOT running before starting the benchmark.`);
process.exit(1);
return;
}
console.log(`\n\x1b[1;36m┌───────────────────────────────────────────────┐\x1b[0m`);
console.log(`\x1b[1;36m│\x1b[0m \x1b[1;35m⚡ Bun Cold Start Benchmark\x1b[0m \x1b[1;36m│\x1b[0m`);
console.log(`\x1b[1;36m├───────────────────────────────────────────────┤\x1b[0m`);
console.log(`\x1b[1;36m│\x1b[0m \x1b[32mCommand:\x1b[0m ${command.substring(0, 35).padEnd(35)} \x1b[1;36m│\x1b[0m`);
console.log(`\x1b[1;36m│\x1b[0m \x1b[32mTarget:\x1b[0m ${url.substring(0, 35).padEnd(35)} \x1b[1;36m│\x1b[0m`);
console.log(`\x1b[1;36m│\x1b[0m \x1b[32mRuns:\x1b[0m ${runs.toString().padEnd(35)} \x1b[1;36m│\x1b[0m`);
console.log(`\x1b[1;36m└───────────────────────────────────────────────┘\x1b[0m\n`);
const results: number[] = [];
for (let i = 0; i < runs; i++) {
const runLabel = `Run ${i + 1}/${runs}:`;
process.stdout.write(` ${runLabel.padEnd(12)} `);
const time = await measureColdStart(command, url);
if (time > 0) {
results.push(time);
console.log(`\x1b[1;33m${time.toFixed(2)}ms\x1b[0m`);
} else if (time === -2) console.log(`\x1b[31mCRASHED\x1b[0m`);
else console.log(`\x1b[31mTIMEOUT\x1b[0m`);
await Bun.sleep(1000);
}
if (results.length > 0) {
const sorted = [...results].sort((a, b) => a - b);
const avg = results.reduce((a, b) => a + b, 0) / results.length;
const median = sorted[Math.floor(sorted.length / 2)] ?? 0;
console.log(`\n\x1b[1;34mSummary Statistics:\x1b[0m`);
console.log(` \x1b[32mAverage:\x1b[0m \x1b[1m${avg.toFixed(2)}ms\x1b[0m`);
console.log(` \x1b[32mMedian:\x1b[0m \x1b[1m${median.toFixed(2)}ms\x1b[0m`);
console.log(` \x1b[32mMin:\x1b[0m \x1b[1m${sorted[0].toFixed(2)}ms\x1b[0m`);
console.log(` \x1b[32mMax:\x1b[0m \x1b[1m${sorted[sorted.length-1].toFixed(2)}ms\x1b[0m\n`);
}
}
main();