Skip to content

Commit 685b78b

Browse files
committed
vibe a runner
1 parent 526ab5e commit 685b78b

File tree

4 files changed

+265
-2
lines changed

4 files changed

+265
-2
lines changed

bench/module-cost/.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
commonjs/*
22
esm/*
3-
CPU*
3+
CPU*
4+
benchmark-results-*.json

bench/module-cost/components/client.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import { useEffect, useRef, useState } from 'react'
44
import { format, measure } from '../lib/measure'
55

66
function report(result, element, textarea) {
7+
if (!globalThis.BENCHMARK_RESULTS) {
8+
globalThis.BENCHMARK_RESULTS = []
9+
}
10+
globalThis.BENCHMARK_RESULTS.push(result)
11+
712
const formattedResult = format(result)
813
element.textContent += `: ${formattedResult}`
914
textarea.current.value += `\n ${formattedResult}`

bench/module-cost/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"name": "module-cost",
33
"scripts": {
44
"prepare-bench": "node scripts/prepare-bench.mjs",
5+
"benchmark": "node scripts/benchmark-runner.mjs",
56
"dev-webpack": "next dev",
67
"dev-turbopack": "next dev --turbo",
78
"build-webpack": "next build",
@@ -10,6 +11,7 @@
1011
},
1112
"devDependencies": {
1213
"rimraf": "6.0.1",
13-
"next": "workspace:*"
14+
"next": "workspace:*",
15+
"playwright": "^1.40.0"
1416
}
1517
}
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import { spawn } from 'node:child_process'
2+
import { writeFileSync } from 'node:fs'
3+
import { chromium } from 'playwright'
4+
5+
/// To use:
6+
/// - Install Playwright: `npx playwright install chromium`
7+
/// - Install dependencies: `pnpm install`
8+
/// - Build the application: `pnpm build-webpack` or pnpm build-turbopack`
9+
/// - Run the benchmark: `pnpm benchmark`
10+
11+
class BenchmarkRunner {
12+
constructor(options) {
13+
this.name = options.name
14+
this.samples = options.samples ?? 50
15+
this.buttonClickDelay = options.buttonClickDelay ?? 500
16+
this.results = []
17+
}
18+
19+
async runBenchmark() {
20+
for (let i = 1; i <= this.samples; i++) {
21+
console.log(`\n--- Running sample ${i}/${this.samples} ---`)
22+
23+
const result = await this.runSingleSample()
24+
this.results.push(...result)
25+
}
26+
27+
this.saveResults()
28+
console.log('\nBenchmark completed!')
29+
}
30+
31+
async runSingleSample() {
32+
let server
33+
let browser
34+
35+
try {
36+
// 1. Launch the server
37+
server = await this.startServer()
38+
39+
// 2. Launch Chrome incognito
40+
console.log('Launching browser...')
41+
browser = await chromium.launch({
42+
headless: true, // Set to true if you don't want to see the browser
43+
args: ['--incognito'],
44+
})
45+
46+
const context = await browser.newContext()
47+
const page = await context.newPage()
48+
49+
// 3. Navigate to localhost:3000
50+
await page.goto('http://localhost:3000', { waitUntil: 'load' })
51+
52+
// 4. Find and click all buttons
53+
const buttons = await page.locator('button').all()
54+
55+
for (let j = 0; j < buttons.length; j++) {
56+
await buttons[j].click()
57+
await this.sleep(this.buttonClickDelay)
58+
}
59+
60+
// 5. Capture data from textbox
61+
console.log('Capturing data from the page...')
62+
const textboxData = await this.capturePageData(page)
63+
console.log('Captured data from the page:', textboxData)
64+
65+
// 6. Close browser
66+
console.log('Closing browser...')
67+
await browser.close()
68+
browser = null
69+
70+
// 7. Shut down server
71+
console.log('Shutting down server...')
72+
await this.stopServer(server)
73+
server = null
74+
75+
return textboxData
76+
} catch (error) {
77+
// Cleanup in case of error
78+
if (browser) {
79+
try {
80+
await browser.close()
81+
} catch (e) {
82+
console.error('Error closing browser:', e.message)
83+
}
84+
}
85+
if (server) {
86+
try {
87+
await this.stopServer(server)
88+
} catch (e) {
89+
console.error('Error stopping server:', e.message)
90+
}
91+
}
92+
throw error
93+
}
94+
}
95+
96+
async startServer() {
97+
return new Promise((resolve, reject) => {
98+
const server = spawn('pnpm', ['start'], {
99+
stdio: ['pipe', 'pipe', 'pipe'],
100+
shell: true,
101+
})
102+
103+
let serverReady = false
104+
105+
server.stdout.on('data', (data) => {
106+
const output = data.toString()
107+
console.log('Server:', output.trim())
108+
109+
// Look for common Next.js ready indicators
110+
if (
111+
output.includes('Ready') ||
112+
output.includes('started server') ||
113+
output.includes('Local:')
114+
) {
115+
if (!serverReady) {
116+
serverReady = true
117+
resolve(server)
118+
}
119+
}
120+
})
121+
122+
server.stderr.on('data', (data) => {
123+
console.error('Server Error:', data.toString().trim())
124+
})
125+
126+
server.on('error', (error) => {
127+
reject(new Error(`Failed to start server: ${error.message}`))
128+
})
129+
130+
server.on('close', (code) => {
131+
if (!serverReady) {
132+
reject(
133+
new Error(`Server exited with code ${code} before becoming ready`)
134+
)
135+
}
136+
})
137+
138+
// Timeout after 30 seconds
139+
setTimeout(() => {
140+
if (!serverReady) {
141+
server.kill()
142+
reject(new Error('Server startup timeout'))
143+
}
144+
}, 30000)
145+
})
146+
}
147+
148+
async stopServer(server) {
149+
return new Promise((resolve) => {
150+
if (!server || server.killed) {
151+
resolve()
152+
return
153+
}
154+
155+
server.on('close', () => {
156+
resolve()
157+
})
158+
159+
// Try graceful shutdown first
160+
server.kill('SIGTERM')
161+
162+
// Force kill after 5 seconds
163+
setTimeout(() => {
164+
if (!server.killed) {
165+
server.kill('SIGKILL')
166+
}
167+
resolve()
168+
}, 5000)
169+
})
170+
}
171+
172+
async capturePageData(page) {
173+
return await page.evaluate(() => globalThis.BENCHMARK_RESULTS)
174+
}
175+
176+
async sleep(ms) {
177+
return new Promise((resolve) => setTimeout(resolve, ms))
178+
}
179+
180+
saveResults() {
181+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
182+
const filename = `benchmark-results-${this.name}-${timestamp}.json`
183+
184+
writeFileSync(
185+
filename,
186+
JSON.stringify(summarizeDurations(this.results), null, 2)
187+
)
188+
console.log(`Results saved to ${filename}`)
189+
}
190+
}
191+
192+
const summarizeDurations = (data) => {
193+
if (!Array.isArray(data) || data.length === 0) {
194+
throw new Error('No data to summarize')
195+
}
196+
197+
const byName = new Map()
198+
for (const item of data) {
199+
const name = item.name
200+
if (!byName.has(name)) {
201+
byName.set(name, [])
202+
}
203+
byName.get(name).push(item)
204+
}
205+
const results = []
206+
for (const [name, data] of byName) {
207+
const loadDurations = data
208+
.map((item) => item.loadDuration)
209+
.sort((a, b) => a - b)
210+
const executeDurations = data
211+
.map((item) => item.executeDuration)
212+
.sort((a, b) => a - b)
213+
214+
const getSummary = (durations) => {
215+
const sum = durations.reduce((acc, val) => acc + val, 0)
216+
const average = sum / durations.length
217+
218+
const middle = Math.floor(durations.length / 2)
219+
const median =
220+
durations.length % 2 === 0
221+
? (durations[middle - 1] + durations[middle]) / 2
222+
: durations[middle]
223+
224+
const percentile75Index = Math.floor(durations.length * 0.75)
225+
const percentile75 = durations[percentile75Index]
226+
227+
return {
228+
average,
229+
median,
230+
percentile75,
231+
}
232+
}
233+
234+
results.push({
235+
name,
236+
totalSamples: data.length,
237+
loadDuration: getSummary(loadDurations),
238+
executeDuration: getSummary(executeDurations),
239+
})
240+
}
241+
242+
return results
243+
}
244+
245+
// CLI usage
246+
const args = process.argv.slice(2)
247+
const samples = args.length > 0 ? Number.parseInt(args[0]) : undefined
248+
const name = args.length > 1 ? args[1] : undefined
249+
250+
const runner = new BenchmarkRunner({
251+
name,
252+
samples,
253+
})
254+
255+
runner.runBenchmark().catch(console.error)

0 commit comments

Comments
 (0)