Skip to content

Commit 7083bcb

Browse files
authored
test: refactor client-nav benchmarks for flame profiling (#6816)
1 parent 2217f7c commit 7083bcb

19 files changed

Lines changed: 529 additions & 263 deletions

benchmarks/client-nav/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:perf:solid --output
2828
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:perf:vue --outputStyle=stream --skipRemoteCache
2929
```
3030

31+
Run framework-specific flame benchmarks (10 second loop, profiled with `@platformatic/flame`, forced to `NODE_ENV=production`):
32+
33+
```bash
34+
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:flame:react --outputStyle=stream --skipRemoteCache
35+
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:flame:solid --outputStyle=stream --skipRemoteCache
36+
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:flame:vue --outputStyle=stream --skipRemoteCache
37+
```
38+
3139
Typecheck benchmark sources:
3240

3341
```bash

benchmarks/client-nav/jsdom.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { JSDOM } from 'jsdom'
2+
3+
const dom = new JSDOM('<!doctype html><html><body></body></html>', {
4+
url: 'http://localhost/',
5+
})
6+
7+
const { window } = dom
8+
9+
function setGlobal(name: string, value: unknown) {
10+
Object.defineProperty(globalThis, name, {
11+
value,
12+
configurable: true,
13+
writable: true,
14+
})
15+
}
16+
17+
setGlobal('window', window)
18+
setGlobal('document', window.document)
19+
setGlobal('self', window)
20+
setGlobal('navigator', window.navigator)
21+
setGlobal('location', window.location)
22+
setGlobal('history', window.history)
23+
setGlobal('HTMLElement', window.HTMLElement)
24+
setGlobal('Element', window.Element)
25+
setGlobal('SVGElement', window.SVGElement)
26+
setGlobal('DocumentFragment', window.DocumentFragment)
27+
setGlobal('Node', window.Node)
28+
setGlobal('MutationObserver', window.MutationObserver)
29+
setGlobal('sessionStorage', window.sessionStorage)
30+
setGlobal('localStorage', window.localStorage)
31+
setGlobal('getComputedStyle', window.getComputedStyle.bind(window))
32+
33+
setGlobal(
34+
'requestAnimationFrame',
35+
window.requestAnimationFrame?.bind(window) ??
36+
((callback: (time: number) => void) =>
37+
setTimeout(() => callback(performance.now()), 16)),
38+
)
39+
40+
setGlobal(
41+
'cancelAnimationFrame',
42+
window.cancelAnimationFrame?.bind(window) ??
43+
((handle: number) => clearTimeout(handle)),
44+
)
45+
46+
window.scrollTo = () => {}
47+
48+
export { window }

benchmarks/client-nav/package.json

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@
66
"build:react": "vite build --config ./react/vite.config.ts",
77
"build:solid": "vite build --config ./solid/vite.config.ts",
88
"build:vue": "vite build --config ./vue/vite.config.ts",
9-
"test:perf": "vitest bench",
10-
"test:perf:react": "vitest bench --config ./react/vite.config.ts ./react/speed.bench.tsx",
11-
"test:perf:solid": "vitest bench --config ./solid/vite.config.ts ./solid/speed.bench.tsx",
12-
"test:perf:vue": "vitest bench --config ./vue/vite.config.ts ./vue/speed.bench.tsx",
9+
"test:flame:react": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./react/speed.flame.ts",
10+
"test:flame:solid": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./solid/speed.flame.ts",
11+
"test:flame:vue": "NODE_ENV=production flame run --md-format=detailed --delay=none --node-options=\"--stack-size=65500\" ./vue/speed.flame.ts",
12+
"test:perf": "NODE_ENV=production vitest bench --config ./vitest.config.ts",
13+
"test:perf:react": "NODE_ENV=production vitest bench --config ./react/vite.config.ts ./react/speed.bench.ts",
14+
"test:perf:solid": "NODE_ENV=production vitest bench --config ./solid/vite.config.ts ./solid/speed.bench.ts",
15+
"test:perf:vue": "NODE_ENV=production vitest bench --config ./vue/vite.config.ts ./vue/speed.bench.ts",
1316
"test:types": "pnpm run test:types:react && pnpm run test:types:solid && pnpm run test:types:vue",
1417
"test:types:react": "tsc -p ./react/tsconfig.json --noEmit",
1518
"test:types:solid": "tsc -p ./solid/tsconfig.json --noEmit",
@@ -26,13 +29,13 @@
2629
"vue": "^3.5.16"
2730
},
2831
"devDependencies": {
32+
"@platformatic/flame": "^1.6.0",
2933
"@codspeed/vitest-plugin": "^5.0.1",
30-
"@solidjs/testing-library": "^0.8.10",
3134
"@testing-library/react": "^16.2.0",
32-
"@testing-library/vue": "^8.1.0",
3335
"@vitejs/plugin-react": "^4.3.4",
3436
"@vitejs/plugin-vue": "^5.2.3",
3537
"@vitejs/plugin-vue-jsx": "^4.1.2",
38+
"@types/jsdom": "28.0.0",
3639
"typescript": "^5.7.2",
3740
"vite": "^7.3.1",
3841
"vite-plugin-solid": "^2.11.10",
@@ -43,30 +46,62 @@
4346
"build:react": {
4447
"cache": false,
4548
"dependsOn": [
46-
"^build"
49+
{
50+
"projects": [
51+
"@tanstack/react-router"
52+
],
53+
"target": "build"
54+
}
4755
]
4856
},
4957
"build:solid": {
5058
"cache": false,
5159
"dependsOn": [
52-
"^build"
60+
{
61+
"projects": [
62+
"@tanstack/solid-router"
63+
],
64+
"target": "build"
65+
}
5366
]
5467
},
5568
"build:vue": {
5669
"cache": false,
5770
"dependsOn": [
58-
"^build"
71+
{
72+
"projects": [
73+
"@tanstack/vue-router"
74+
],
75+
"target": "build"
76+
}
5977
]
6078
},
6179
"test:perf": {
6280
"cache": false,
6381
"dependsOn": [
64-
"^build",
6582
"build:react",
6683
"build:solid",
6784
"build:vue"
6885
]
6986
},
87+
"test:flame:react": {
88+
"cache": false,
89+
"dependsOn": [
90+
"build:react"
91+
]
92+
},
93+
"test:flame:solid": {
94+
"cache": false,
95+
"dependsOn": [
96+
"build:solid"
97+
]
98+
},
99+
"test:flame:vue": {
100+
"cache": false,
101+
"dependsOn": [
102+
"build:vue"
103+
]
104+
},
70105
"test:perf:react": {
71106
"cache": false,
72107
"dependsOn": [
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import type { NavigateOptions } from '@tanstack/router-core'
2+
import type * as App from './app'
3+
import type { Root } from 'react-dom/client'
4+
5+
const appModulePath = './dist/app.js'
6+
const { createTestRouter } = (await import(appModulePath)) as typeof App
7+
8+
export function setup() {
9+
if (process.env.NODE_ENV !== 'production') {
10+
console.warn(
11+
'client-nav benchmark is running without NODE_ENV=production; React dev overhead will dominate results.',
12+
)
13+
}
14+
let id = 0
15+
let root: Root | undefined = undefined
16+
let container: HTMLDivElement | undefined = undefined
17+
let unsub = () => {}
18+
let next: () => Promise<void> = () => Promise.reject('Test not initialized')
19+
20+
async function before() {
21+
id = 0
22+
const { router, component } = createTestRouter()
23+
let resolve: () => void = () => {}
24+
unsub = router.subscribe('onRendered', () => resolve())
25+
26+
const navigate = (opts: NavigateOptions) =>
27+
new Promise<void>((resolveNext) => {
28+
resolve = resolveNext
29+
router.navigate(opts)
30+
})
31+
32+
next = () => {
33+
const nextId = id++
34+
35+
return navigate({
36+
to: '/$id',
37+
params: { id: nextId },
38+
// update search every 2 navigations, to still test them, but also measure the impact of granular re-rendering
39+
search: { id: Math.floor(nextId / 2) },
40+
replace: true,
41+
})
42+
}
43+
44+
const { createRoot } = await import('react-dom/client')
45+
46+
container = document.createElement('div')
47+
document.body.append(container)
48+
root = createRoot(container)
49+
root.render(component)
50+
await router.load()
51+
}
52+
53+
function after() {
54+
root?.unmount()
55+
container?.remove()
56+
unsub()
57+
}
58+
59+
function tick() {
60+
return next()
61+
}
62+
63+
return {
64+
before,
65+
tick,
66+
after,
67+
}
68+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { afterAll, beforeAll, bench, describe } from 'vitest'
2+
import { setup } from './setup'
3+
4+
describe('client-nav', () => {
5+
const test = setup()
6+
7+
/**
8+
* Running `vitest bench` ignores "suite hooks" like `beforeAll` and `afterAll`,
9+
* so we use tinybench's `setup` and `teardown` options to run our setup and teardown logic.
10+
*
11+
* But CodSpeed calls the benchmarked function directly, bypassing `setup` and `teardown`,
12+
* but it does support `beforeAll` and `afterAll`.
13+
*
14+
* So it looks like we're setting up in duplicate, but in reality, it's only running once per environment, as intended.
15+
*/
16+
17+
beforeAll(test.before)
18+
afterAll(test.after)
19+
20+
bench(
21+
'client-side navigation loop (react)',
22+
async () => {
23+
for (let i = 0; i < 10; i++) {
24+
await test.tick()
25+
}
26+
},
27+
{
28+
warmupIterations: 100,
29+
time: 10_000,
30+
setup: test.before,
31+
teardown: test.after,
32+
},
33+
)
34+
})

benchmarks/client-nav/react/speed.bench.tsx

Lines changed: 0 additions & 75 deletions
This file was deleted.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { window } from '../jsdom.ts'
2+
import { setup } from './setup.ts'
3+
4+
const DURATION_MS = 10_000
5+
6+
const test = setup()
7+
8+
try {
9+
await test.before()
10+
11+
const startedAt = performance.now()
12+
while (performance.now() - startedAt < DURATION_MS) {
13+
await test.tick()
14+
}
15+
} finally {
16+
test.after()
17+
window.close()
18+
}

benchmarks/client-nav/react/tsconfig.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,16 @@
22
"extends": "../../../tsconfig.json",
33
"compilerOptions": {
44
"jsx": "react-jsx",
5+
"allowImportingTsExtensions": true,
56
"jsxImportSource": "react",
67
"types": ["node", "vite/client", "vitest/globals"]
78
},
8-
"include": ["speed.bench.tsx", "vite.config.ts", "../vitest.setup.ts"]
9+
"include": [
10+
"speed.bench.ts",
11+
"speed.flame.ts",
12+
"../jsdom.ts",
13+
"setup.ts",
14+
"vite.config.ts",
15+
"../vitest.setup.ts"
16+
]
917
}

0 commit comments

Comments
 (0)