Skip to content

Commit 6c197b4

Browse files
committed
fix(bench): isolate measured workloads
1 parent 78edb22 commit 6c197b4

11 files changed

Lines changed: 159 additions & 76 deletions

File tree

bench/memory/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ and Stacks dev fixtures before becoming executable profiles.
2222

2323
The runner uses the same byte-for-byte response parity checks as
2424
`bench/routing`. It samples the entire server process tree, so launchers cannot
25-
hide worker memory. The separately launched load generator is not counted.
25+
hide worker memory. The separately launched load generator is not counted. Each
26+
server registers only the selected scenario, so an unrelated validator or
27+
database route cannot inflate one framework's static JSON result. This applies
28+
identically to every target.
2629

2730
## Reported value
2831

bench/memory/run.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ async function measure(
196196
| { skipped: string }
197197
| { measurement: Omit<MemoryMeasurement, 'targetId' | 'run'>, samples: MemorySample[], load: LoadResult }
198198
> {
199-
const booted = await boot(target, Boolean(scenario.requiresDb))
199+
const booted = await boot(target, Boolean(scenario.requiresDb), scenario)
200200
if ('skipped' in booted) return booted
201201

202202
try {

bench/routing/bunfig.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Benchmark servers must not inherit the application's environment and
2+
# auto-import preloads. Each target imports only the framework it measures.
3+
preload = []

bench/routing/runtime.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { describe, expect, it } from 'bun:test'
2+
import { BENCH_ROOT, serverCommand, serverEnvironment } from './runtime'
3+
4+
describe('benchmark server isolation', () => {
5+
it('boots targets without the application bunfig preloads', () => {
6+
expect(serverCommand('bun-raw.ts')).toEqual([
7+
process.execPath,
8+
`--config=${BENCH_ROOT}bunfig.toml`,
9+
`${BENCH_ROOT}servers/bun-raw.ts`,
10+
])
11+
})
12+
13+
it('boots every framework in production mode', () => {
14+
const env = serverEnvironment({
15+
id: 'test',
16+
label: 'test',
17+
server: 'bun-raw.ts',
18+
env: { BENCH_MODE: 'minimal' },
19+
}, false, 'static-json')
20+
21+
expect(env.APP_ENV).toBe('production')
22+
expect(env.NODE_ENV).toBe('production')
23+
expect(env.BENCH_MODE).toBe('minimal')
24+
expect(env.BENCH_SCENARIO).toBe('static-json')
25+
})
26+
})

bench/routing/runtime.ts

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,36 @@ export interface BootedServer {
1818
pid: number
1919
}
2020

21-
export async function boot(target: Target, withDb: boolean): Promise<BootedServer | { skipped: string }> {
22-
const proc = Bun.spawn([process.execPath, join(BENCH_ROOT, 'servers', target.server)], {
21+
/** Keep benchmark targets out of the application's preload graph. */
22+
export function serverCommand(server: string): string[] {
23+
return [
24+
process.execPath,
25+
`--config=${join(BENCH_ROOT, 'bunfig.toml')}`,
26+
join(BENCH_ROOT, 'servers', server),
27+
]
28+
}
29+
30+
/** Give every framework the same production environment. */
31+
export function serverEnvironment(target: Target, withDb: boolean, scenarioId?: string): Record<string, string> {
32+
return {
33+
...process.env,
34+
APP_ENV: 'production',
35+
NODE_ENV: 'production',
36+
BENCH_PORT: String(PORT),
37+
BENCH_DB: withDb ? '1' : '0',
38+
BENCH_DB_FILE: FIXTURE,
39+
DB_DATABASE_PATH: FIXTURE,
40+
...(scenarioId ? { BENCH_SCENARIO: scenarioId } : {}),
41+
...target.env,
42+
} as Record<string, string>
43+
}
44+
45+
export async function boot(target: Target, withDb: boolean, scenario?: Scenario): Promise<BootedServer | { skipped: string }> {
46+
const proc = Bun.spawn(serverCommand(target.server), {
2347
cwd: REPO_ROOT,
2448
stdout: 'pipe',
2549
stderr: 'pipe',
26-
env: {
27-
...process.env,
28-
BENCH_PORT: String(PORT),
29-
BENCH_DB: withDb ? '1' : '0',
30-
BENCH_DB_FILE: FIXTURE,
31-
DB_DATABASE_PATH: FIXTURE,
32-
...target.env,
33-
} as Record<string, string>,
50+
env: serverEnvironment(target, withDb, scenario?.id),
3451
})
3552

3653
const deadline = Date.now() + 60_000
@@ -43,7 +60,7 @@ export async function boot(target: Target, withDb: boolean): Promise<BootedServe
4360
throw new Error(`${target.id} server exited ${proc.exitCode}:\n${err}`)
4461
}
4562
try {
46-
const res = await fetch(`http://127.0.0.1:${PORT}/bench/json`)
63+
const res = await fetch(`http://127.0.0.1:${PORT}${scenario?.path ?? '/bench/json'}`)
4764
if (res.ok) {
4865
await res.arrayBuffer()
4966
break

bench/routing/servers/bun-raw.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,18 @@
77
* already believed.
88
*/
99

10-
import { Database } from 'bun:sqlite'
1110
import process from 'node:process'
1211

1312
const port = Number(process.env.BENCH_PORT ?? 3999)
1413
const withDb = process.env.BENCH_DB === '1'
14+
const scenario = process.env.BENCH_SCENARIO
15+
const serves = (id: string) => !scenario || scenario === id
1516

1617
const JSON_HEADERS = { 'content-type': 'application/json' } as const
1718

1819
let selectItem: import('bun:sqlite').Statement | undefined
19-
if (withDb) {
20+
if (withDb && serves('db-roundtrip')) {
21+
const { Database } = await import('bun:sqlite')
2022
const db = new Database(process.env.BENCH_DB_FILE!, { readonly: true })
2123
selectItem = db.prepare('SELECT id, name FROM bench_items WHERE id = 1')
2224
}
@@ -28,13 +30,13 @@ Bun.serve({
2830
const pathStart = url.indexOf('/', url.indexOf('://') + 3)
2931
const path = pathStart === -1 ? '/' : url.slice(pathStart)
3032

31-
if (path === '/bench/json')
33+
if (serves('static-json') && path === '/bench/json')
3234
return new Response('{"hello":"world"}', { headers: JSON_HEADERS })
3335

34-
if (path.startsWith('/bench/users/'))
36+
if (serves('path-param') && path.startsWith('/bench/users/'))
3537
return new Response(JSON.stringify({ id: path.slice('/bench/users/'.length) }), { headers: JSON_HEADERS })
3638

37-
if (path === '/bench/echo' && req.method === 'POST') {
39+
if (serves('post-validate') && path === '/bench/echo' && req.method === 'POST') {
3840
const body = await req.json() as { name?: unknown, count?: unknown }
3941
if (typeof body.name !== 'string' || typeof body.count !== 'number')
4042
return new Response('{"errors":{}}', { status: 422, headers: JSON_HEADERS })

bench/routing/servers/elysia.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import process from 'node:process'
1111

1212
const port = Number(process.env.BENCH_PORT ?? 3999)
1313
const withDb = process.env.BENCH_DB === '1'
14+
const scenario = process.env.BENCH_SCENARIO
15+
const serves = (id: string) => !scenario || scenario === id
1416

1517
let Elysia: any
1618
let t: any
@@ -23,15 +25,19 @@ catch {
2325
}
2426

2527
const app = new Elysia()
26-
.get('/bench/json', () => ({ hello: 'world' }))
27-
.get('/bench/users/:id', ({ params }: any) => ({ id: params.id }))
28-
.post(
28+
if (serves('static-json'))
29+
app.get('/bench/json', () => ({ hello: 'world' }))
30+
if (serves('path-param'))
31+
app.get('/bench/users/:id', ({ params }: any) => ({ id: params.id }))
32+
if (serves('post-validate')) {
33+
app.post(
2934
'/bench/echo',
3035
({ body }: any) => ({ name: body.name, count: body.count }),
3136
{ body: t.Object({ name: t.String(), count: t.Number() }) },
3237
)
38+
}
3339

34-
if (withDb) {
40+
if (withDb && serves('db-roundtrip')) {
3541
const { Database } = await import('bun:sqlite')
3642
const db = new Database(process.env.BENCH_DB_FILE!, { readonly: true })
3743
const selectItem = db.prepare('SELECT id, name FROM bench_items WHERE id = 1')

bench/routing/servers/express.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import process from 'node:process'
22

33
const port = Number(process.env.BENCH_PORT ?? 3999)
44
const withDb = process.env.BENCH_DB === '1'
5+
const scenario = process.env.BENCH_SCENARIO
6+
const serves = (id: string) => !scenario || scenario === id
57

68
let express: any
79
try {
@@ -13,16 +15,20 @@ catch {
1315
}
1416

1517
const app = express()
16-
app.use(express.json())
17-
app.get('/bench/json', (_req: any, res: any) => res.json({ hello: 'world' }))
18-
app.get('/bench/users/:id', (req: any, res: any) => res.json({ id: req.params.id }))
19-
app.post('/bench/echo', (req: any, res: any) => {
20-
const { name, count } = req.body ?? {}
21-
if (typeof name !== 'string' || typeof count !== 'number') return res.status(422).json({ errors: {} })
22-
return res.json({ name, count })
23-
})
18+
if (serves('static-json'))
19+
app.get('/bench/json', (_req: any, res: any) => res.json({ hello: 'world' }))
20+
if (serves('path-param'))
21+
app.get('/bench/users/:id', (req: any, res: any) => res.json({ id: req.params.id }))
22+
if (serves('post-validate')) {
23+
app.use(express.json())
24+
app.post('/bench/echo', (req: any, res: any) => {
25+
const { name, count } = req.body ?? {}
26+
if (typeof name !== 'string' || typeof count !== 'number') return res.status(422).json({ errors: {} })
27+
return res.json({ name, count })
28+
})
29+
}
2430

25-
if (withDb) {
31+
if (withDb && serves('db-roundtrip')) {
2632
const { Database } = await import('bun:sqlite')
2733
const db = new Database(process.env.BENCH_DB_FILE!, { readonly: true })
2834
const selectItem = db.prepare('SELECT id, name FROM bench_items WHERE id = 1')

bench/routing/servers/fastify.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import process from 'node:process'
22

33
const port = Number(process.env.BENCH_PORT ?? 3999)
44
const withDb = process.env.BENCH_DB === '1'
5+
const scenario = process.env.BENCH_SCENARIO
6+
const serves = (id: string) => !scenario || scenario === id
57

68
let fastify: any
79
try {
@@ -13,19 +15,23 @@ catch {
1315
}
1416

1517
const app = fastify({ logger: false })
16-
app.get('/bench/json', () => ({ hello: 'world' }))
17-
app.get('/bench/users/:id', (request: any) => ({ id: request.params.id }))
18-
app.post('/bench/echo', {
19-
schema: {
20-
body: {
21-
type: 'object',
22-
required: ['name', 'count'],
23-
properties: { name: { type: 'string' }, count: { type: 'number' } },
18+
if (serves('static-json'))
19+
app.get('/bench/json', () => ({ hello: 'world' }))
20+
if (serves('path-param'))
21+
app.get('/bench/users/:id', (request: any) => ({ id: request.params.id }))
22+
if (serves('post-validate')) {
23+
app.post('/bench/echo', {
24+
schema: {
25+
body: {
26+
type: 'object',
27+
required: ['name', 'count'],
28+
properties: { name: { type: 'string' }, count: { type: 'number' } },
29+
},
2430
},
25-
},
26-
}, (request: any) => ({ name: request.body.name, count: request.body.count }))
31+
}, (request: any) => ({ name: request.body.name, count: request.body.count }))
32+
}
2733

28-
if (withDb) {
34+
if (withDb && serves('db-roundtrip')) {
2935
const { Database } = await import('bun:sqlite')
3036
const db = new Database(process.env.BENCH_DB_FILE!, { readonly: true })
3137
const selectItem = db.prepare('SELECT id, name FROM bench_items WHERE id = 1')

bench/routing/servers/hono.ts

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ import process from 'node:process'
1212

1313
const port = Number(process.env.BENCH_PORT ?? 3999)
1414
const withDb = process.env.BENCH_DB === '1'
15+
const scenario = process.env.BENCH_SCENARIO
16+
const serves = (id: string) => !scenario || scenario === id
1517

1618
let Hono: any
17-
let validator: any
1819
try {
1920
;({ Hono } = await import('hono') as any)
20-
;({ validator } = await import('hono/validator') as any)
2121
}
2222
catch {
2323
console.error('[bench] hono is not installed — run `bun add -d hono` to include it')
@@ -26,22 +26,27 @@ catch {
2626

2727
const app = new Hono()
2828

29-
app.get('/bench/json', (c: any) => c.json({ hello: 'world' }))
30-
app.get('/bench/users/:id', (c: any) => c.json({ id: c.req.param('id') }))
31-
app.post(
32-
'/bench/echo',
33-
validator('json', (value: any, c: any) => {
34-
if (typeof value.name !== 'string' || typeof value.count !== 'number')
35-
return c.json({ errors: {} }, 422)
36-
return value
37-
}),
38-
(c: any) => {
39-
const body = c.req.valid('json')
40-
return c.json({ name: body.name, count: body.count })
41-
},
42-
)
29+
if (serves('static-json'))
30+
app.get('/bench/json', (c: any) => c.json({ hello: 'world' }))
31+
if (serves('path-param'))
32+
app.get('/bench/users/:id', (c: any) => c.json({ id: c.req.param('id') }))
33+
if (serves('post-validate')) {
34+
const { validator } = await import('hono/validator') as any
35+
app.post(
36+
'/bench/echo',
37+
validator('json', (value: any, c: any) => {
38+
if (typeof value.name !== 'string' || typeof value.count !== 'number')
39+
return c.json({ errors: {} }, 422)
40+
return value
41+
}),
42+
(c: any) => {
43+
const body = c.req.valid('json')
44+
return c.json({ name: body.name, count: body.count })
45+
},
46+
)
47+
}
4348

44-
if (withDb) {
49+
if (withDb && serves('db-roundtrip')) {
4550
const { Database } = await import('bun:sqlite')
4651
const db = new Database(process.env.BENCH_DB_FILE!, { readonly: true })
4752
const selectItem = db.prepare('SELECT id, name FROM bench_items WHERE id = 1')

0 commit comments

Comments
 (0)