Mercurial
comparison benchmark/bun-http-framework-benchmark/bench.ts @ 186:8cf4ec5e2191 hg-web
Fixed merge conflict.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Fri, 23 Jan 2026 22:38:59 -0800 |
| parents | a8976a008a9d |
| children |
comparison
equal
deleted
inserted
replaced
| 176:fed99fc04e12 | 186:8cf4ec5e2191 |
|---|---|
| 1 import { | |
| 2 readdirSync, | |
| 3 mkdirSync, | |
| 4 existsSync, | |
| 5 lstatSync, | |
| 6 readFileSync, | |
| 7 writeFileSync | |
| 8 } from 'fs' | |
| 9 import killPort from 'kill-port' | |
| 10 import { $, pathToFileURL } from 'bun' | |
| 11 | |
| 12 const whitelists = <string[]>[] | |
| 13 | |
| 14 // ? Not working | |
| 15 const blacklists = [ | |
| 16 // Not booting up in test | |
| 17 'node/adonis/index', | |
| 18 // Not setting content-type header for some reason | |
| 19 'node/nest/index', | |
| 20 // 'Not booting up in test' | |
| 21 'node/hapi', | |
| 22 // Body: Result not match | |
| 23 'bun/xirelta', | |
| 24 // Crash | |
| 25 'bun/bagel', | |
| 26 // Crash | |
| 27 'bun/bunrest', | |
| 28 // Doesn't work properly | |
| 29 'bun/colston', | |
| 30 // Crash on 0.6.2 | |
| 31 'bun/zarf', | |
| 32 // Crash due to invalid npm version requirement of uWebSockets | |
| 33 'deno/byte', | |
| 34 // Crash | |
| 35 'bun/fastify', | |
| 36 // failed to parse body in benchmark | |
| 37 'bun/byte', | |
| 38 // doesn't run | |
| 39 'bun/vixeny', | |
| 40 'node/elysia', | |
| 41 'c/BUILD/index', | |
| 42 'deno/acorn', | |
| 43 'deno/deno', | |
| 44 'deno/deno-web-standard', | |
| 45 'deno/hono', | |
| 46 'deno/oak' | |
| 47 ] as const | |
| 48 | |
| 49 const time = 10 | |
| 50 | |
| 51 const commands = [ | |
| 52 `/home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d ${time}s http://127.0.0.1:3000/`, | |
| 53 `/home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d ${time}s http://127.0.0.1:3000/id/1?name=bun`, | |
| 54 `/home/june/zenbu/benchmark/bun-http-framework-benchmark/bombardier-linux-386 --fasthttp -c 500 -d ${time}s -m POST -H 'Content-Type:application/json' -f ./scripts/body.json http://127.0.0.1:3000/json` | |
| 55 ] as const | |
| 56 | |
| 57 const runtimeCommand = { | |
| 58 node: 'node', | |
| 59 deno: 'deno run --allow-net --allow-env', | |
| 60 bun: 'bun', | |
| 61 c: '' // C binaries are run directly after bazel build | |
| 62 } as const | |
| 63 | |
| 64 // Path to the bazel-bin directory relative to project root | |
| 65 const getBazelBinaryPath = (framework: string) => | |
| 66 `../../bazel-bin/benchmark/bun-http-framework-benchmark/src/c/${framework}` | |
| 67 | |
| 68 const catchNumber = /Reqs\/sec\s+(\d+[.|,]\d+)/m | |
| 69 const format = (value: string | number) => | |
| 70 Intl.NumberFormat('en-US').format(+value) | |
| 71 const sleep = (s = 1) => new Promise((resolve) => setTimeout(resolve, s * 1000)) | |
| 72 | |
| 73 const secToMin = (seconds: number) => | |
| 74 Math.floor(seconds / 60) + | |
| 75 ':' + | |
| 76 (seconds % 60 < 10 ? '0' : '') + | |
| 77 (seconds % 60) | |
| 78 | |
| 79 // Fetch with retry | |
| 80 const retryFetch = ( | |
| 81 url: string, | |
| 82 options?: RequestInit, | |
| 83 time = 0, | |
| 84 resolveEnd?: Function, | |
| 85 rejectEnd?: Function | |
| 86 ) => { | |
| 87 return new Promise<Response>((resolve, reject) => { | |
| 88 fetch(url, options) | |
| 89 .then((a) => { | |
| 90 if (resolveEnd) resolveEnd(a) | |
| 91 | |
| 92 resolve(a) | |
| 93 }) | |
| 94 .catch((e) => { | |
| 95 if (time > 7) { | |
| 96 if (rejectEnd) rejectEnd(e) | |
| 97 | |
| 98 return reject(e) | |
| 99 } | |
| 100 setTimeout( | |
| 101 () => retryFetch(url, options, time + 1, resolve, reject), | |
| 102 200 | |
| 103 ) | |
| 104 }) | |
| 105 }) | |
| 106 } | |
| 107 | |
| 108 const test = async () => { | |
| 109 try { | |
| 110 const index = await retryFetch('http://127.0.0.1:3000/') | |
| 111 | |
| 112 if ((await index.text()) !== 'Hi') | |
| 113 throw new Error('Index: Result not match') | |
| 114 | |
| 115 if (!index.headers.get('Content-Type')?.includes('text/plain')) | |
| 116 throw new Error('Index: Content-Type not match') | |
| 117 | |
| 118 const query = await retryFetch('http://127.0.0.1:3000/id/1?name=bun') | |
| 119 if ((await query.text()) !== '1 bun') | |
| 120 throw new Error('Query: Result not match') | |
| 121 | |
| 122 if (!query.headers.get('Content-Type')?.includes('text/plain')) | |
| 123 throw new Error('Query: Content-Type not match') | |
| 124 | |
| 125 if (!query.headers.get('X-Powered-By')?.includes('benchmark')) | |
| 126 throw new Error('Query: X-Powered-By not match') | |
| 127 | |
| 128 const body = await retryFetch('http://127.0.0.1:3000/json', { | |
| 129 method: 'POST', | |
| 130 headers: { | |
| 131 'Content-Type': 'application/json' | |
| 132 }, | |
| 133 body: JSON.stringify({ | |
| 134 hello: 'world' | |
| 135 }) | |
| 136 }) | |
| 137 | |
| 138 if ((await body.text()) !== JSON.stringify({ hello: 'world' })) | |
| 139 throw new Error('Body: Result not match') | |
| 140 | |
| 141 if (!body.headers.get('Content-Type')?.includes('application/json')) | |
| 142 throw new Error('Body: Content-Type not match') | |
| 143 } catch (error) { | |
| 144 throw error | |
| 145 } | |
| 146 } | |
| 147 | |
| 148 const spawn = (target: string, title = true) => { | |
| 149 let [runtime, framework, index] = target.split('/') as [ | |
| 150 keyof typeof runtimeCommand, | |
| 151 string, | |
| 152 string | |
| 153 ] | |
| 154 if (index) framework += '/index' | |
| 155 | |
| 156 const name = framework.replace('/index', '') | |
| 157 | |
| 158 if (title) { | |
| 159 console.log('\n', name) | |
| 160 console.log(' >', runtime, framework, '\n') | |
| 161 } | |
| 162 | |
| 163 let cmd: string[] | |
| 164 | |
| 165 if (runtime === 'c') { | |
| 166 // For C, run the pre-built bazel binary directly | |
| 167 const binaryPath = getBazelBinaryPath(framework) | |
| 168 cmd = [binaryPath] | |
| 169 } else { | |
| 170 const file = existsSync(`./src/${runtime}/${framework}.ts`) | |
| 171 ? `src/${runtime}/${framework}.ts` | |
| 172 : `src/${runtime}/${framework}.js` | |
| 173 cmd = [...runtimeCommand[runtime].split(" "), file] | |
| 174 } | |
| 175 | |
| 176 const server = Bun.spawn({ | |
| 177 cmd, | |
| 178 env: { | |
| 179 ...Bun.env, | |
| 180 NODE_ENV: 'production' | |
| 181 } | |
| 182 }) | |
| 183 | |
| 184 return async () => { | |
| 185 await server.kill() | |
| 186 await sleep(0.3) | |
| 187 | |
| 188 try { | |
| 189 await fetch('http://127.0.0.1:3000') | |
| 190 await sleep(0.6) | |
| 191 await fetch('http://127.0.0.1:3000') | |
| 192 | |
| 193 await killPort(3000) | |
| 194 } catch { | |
| 195 // Empty | |
| 196 } | |
| 197 } | |
| 198 } | |
| 199 | |
| 200 try { | |
| 201 if (lstatSync('results').isDirectory()) rimraf.sync('results') | |
| 202 } catch {} | |
| 203 await Bun.$`rm -rf ./results` | |
| 204 mkdirSync('results') | |
| 205 writeFileSync('results/results.md', '') | |
| 206 const resultFile = Bun.file('results/results.md') | |
| 207 const result = resultFile.writer() | |
| 208 | |
| 209 const main = async () => { | |
| 210 try { | |
| 211 await fetch('http://127.0.0.1:3000') | |
| 212 await killPort(3000) | |
| 213 } catch { | |
| 214 // Empty | |
| 215 } | |
| 216 | |
| 217 const runtimes = <string[]>[] | |
| 218 | |
| 219 let frameworks = readdirSync('src') | |
| 220 .flatMap((runtime) => { | |
| 221 if (!lstatSync(`src/${runtime}`).isDirectory()) return | |
| 222 | |
| 223 if (!existsSync(`results/${runtime}`)) | |
| 224 mkdirSync(`results/${runtime}`) | |
| 225 | |
| 226 return readdirSync(`src/${runtime}`) | |
| 227 .filter( | |
| 228 (a) => | |
| 229 a.endsWith('.ts') || | |
| 230 a.endsWith('.js') || | |
| 231 a.endsWith('.c') || | |
| 232 !a.includes('.') | |
| 233 ) | |
| 234 .map((a) => | |
| 235 a.includes('.') | |
| 236 ? `${runtime}/` + a.replace(/\.(j|t|c)s?$/, '') | |
| 237 : `${runtime}/${a}/index` | |
| 238 ) | |
| 239 .filter( | |
| 240 (a) => | |
| 241 !blacklists.includes(a as (typeof blacklists)[number]) | |
| 242 ) | |
| 243 }) | |
| 244 .filter((x) => x) | |
| 245 .sort() | |
| 246 | |
| 247 // Overwrite test here | |
| 248 frameworks = whitelists?.length ? whitelists : frameworks | |
| 249 | |
| 250 console.log(`${frameworks.length} frameworks`) | |
| 251 for (const framework of frameworks) console.log(`- ${framework}`) | |
| 252 | |
| 253 console.log('\nTest:') | |
| 254 for (const target of frameworks) { | |
| 255 const kill = spawn(target!, false) | |
| 256 | |
| 257 let [runtime, framework] = target!.split('/') | |
| 258 await sleep(0.1) | |
| 259 | |
| 260 if (runtimes.includes(runtime)) { | |
| 261 const folder = `results/${runtime}` | |
| 262 | |
| 263 if (!lstatSync(folder).isDirectory()) rimraf(folder) | |
| 264 } | |
| 265 | |
| 266 try { | |
| 267 const kill = await test() | |
| 268 | |
| 269 console.log(`✅ ${framework} (${runtime})`) | |
| 270 } catch (error) { | |
| 271 console.log(`❌ ${framework} (${runtime})`) | |
| 272 console.log(' ', (error as Error)?.message || error) | |
| 273 | |
| 274 frameworks.splice(frameworks.indexOf(target!), 1) | |
| 275 } finally { | |
| 276 await kill() | |
| 277 } | |
| 278 } | |
| 279 | |
| 280 const estimateTime = frameworks.length * (commands.length * time + 1) | |
| 281 | |
| 282 console.log() | |
| 283 console.log(`${frameworks.length} frameworks`) | |
| 284 for (const framework of frameworks) console.log(`- ${framework}`) | |
| 285 | |
| 286 console.log(`\nEstimate time: ${secToMin(estimateTime)} min`) | |
| 287 | |
| 288 // process.exit() | |
| 289 | |
| 290 result.write( | |
| 291 ` | |
| 292 | Framework | Runtime | Average | Ping | Query | Body | | |
| 293 | ---------------- | ------- | ------- | ---------- | ---------- | ---------- | | |
| 294 ` | |
| 295 ) | |
| 296 | |
| 297 for (const target of frameworks) { | |
| 298 const kill = spawn(target!) | |
| 299 | |
| 300 let [runtime, framework, index] = target!.split('/') as [ | |
| 301 keyof typeof runtimeCommand, | |
| 302 string, | |
| 303 string | |
| 304 ] | |
| 305 | |
| 306 const name = framework.replace('/index', '') | |
| 307 | |
| 308 const frameworkResultFile = Bun.file(`results/${runtime}/${name}.txt`) | |
| 309 const frameworkResult = frameworkResultFile.writer() | |
| 310 | |
| 311 result.write(`| ${name} | ${runtime} `) | |
| 312 | |
| 313 // Wait .3 second for server to bootup | |
| 314 await sleep(0.4) | |
| 315 | |
| 316 let content = '' | |
| 317 const total = [] | |
| 318 | |
| 319 for (const command of commands) { | |
| 320 frameworkResult.write(`${command}\n`) | |
| 321 | |
| 322 console.log(command) | |
| 323 | |
| 324 const res = await Bun.spawn({ | |
| 325 cmd: command.split(' '), | |
| 326 env: Bun.env | |
| 327 }) | |
| 328 | |
| 329 const stdout = await new Response(res.stdout).text() | |
| 330 console.log(stdout) | |
| 331 | |
| 332 const results = catchNumber.exec(stdout) | |
| 333 if (!results?.[1]) continue | |
| 334 | |
| 335 content += `| ${format(results[1])} ` | |
| 336 total.push(toNumber(results[1])) | |
| 337 | |
| 338 frameworkResult.write(results + '\n') | |
| 339 } | |
| 340 | |
| 341 content = | |
| 342 `| ${format( | |
| 343 total.reduce((a, b) => +a + +b, 0) / commands.length | |
| 344 )} ` + | |
| 345 content + | |
| 346 '|\n' | |
| 347 | |
| 348 result.write(content) | |
| 349 await result.flush() | |
| 350 | |
| 351 await kill() | |
| 352 } | |
| 353 } | |
| 354 | |
| 355 const toNumber = (a: string) => +a.replaceAll(',', '') | |
| 356 | |
| 357 const arrange = () => { | |
| 358 const table = readFileSync('results/results.md', { | |
| 359 encoding: 'utf-8' | |
| 360 }) | |
| 361 | |
| 362 const orders = [] | |
| 363 | |
| 364 const [title, divider, ...rows] = table.split('\n') | |
| 365 for (const row of rows) { | |
| 366 const data = row | |
| 367 .replace(/\ /g, '') | |
| 368 .split('|') | |
| 369 .filter((a) => a) | |
| 370 | |
| 371 if (data.length !== commands.length + 3) continue | |
| 372 | |
| 373 const [name, runtime, total] = data | |
| 374 orders.push({ | |
| 375 name, | |
| 376 runtime, | |
| 377 total: toNumber(total), | |
| 378 row | |
| 379 }) | |
| 380 } | |
| 381 | |
| 382 const content = [ | |
| 383 title, | |
| 384 divider, | |
| 385 ...orders.sort((a, b) => b.total - a.total).map((a) => a.row) | |
| 386 ].join('\n') | |
| 387 | |
| 388 console.log(content) | |
| 389 writeFileSync('results/results.md', content) | |
| 390 | |
| 391 process.exit(0) | |
| 392 } | |
| 393 | |
| 394 process.on('beforeExit', async () => { | |
| 395 await killPort(3000) | |
| 396 }) | |
| 397 | |
| 398 main().then(arrange) |