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)