Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled
256 lines
6.3 KiB
JavaScript
256 lines
6.3 KiB
JavaScript
import { spawn } from 'node:child_process'
|
|
import { writeFileSync } from 'node:fs'
|
|
import { chromium } from 'playwright'
|
|
|
|
/// To use:
|
|
/// - Install Playwright: `npx playwright install chromium`
|
|
/// - Install dependencies: `pnpm install`
|
|
/// - Build the application: `pnpm build-webpack` or pnpm build-turbopack`
|
|
/// - Run the benchmark: `pnpm benchmark`
|
|
|
|
class BenchmarkRunner {
|
|
constructor(options) {
|
|
this.name = options.name
|
|
this.samples = options.samples ?? 50
|
|
this.buttonClickDelay = options.buttonClickDelay ?? 500
|
|
this.results = []
|
|
}
|
|
|
|
async runBenchmark() {
|
|
for (let i = 1; i <= this.samples; i++) {
|
|
console.log(`\n--- Running sample ${i}/${this.samples} ---`)
|
|
|
|
const result = await this.runSingleSample()
|
|
this.results.push(...result)
|
|
}
|
|
|
|
this.saveResults()
|
|
console.log('\nBenchmark completed!')
|
|
}
|
|
|
|
async runSingleSample() {
|
|
let server
|
|
let browser
|
|
|
|
try {
|
|
// 1. Launch the server
|
|
server = await this.startServer()
|
|
|
|
// 2. Launch Chrome incognito
|
|
console.log('Launching browser...')
|
|
browser = await chromium.launch({
|
|
headless: true, // Set to true if you don't want to see the browser
|
|
args: ['--incognito'],
|
|
})
|
|
|
|
const context = await browser.newContext()
|
|
const page = await context.newPage()
|
|
|
|
// 3. Navigate to localhost:3000
|
|
await page.goto('http://localhost:3000', { waitUntil: 'load' })
|
|
|
|
// 4. Find and click all buttons
|
|
const buttons = await page.locator('button').all()
|
|
|
|
for (let j = 0; j < buttons.length; j++) {
|
|
await buttons[j].click()
|
|
await this.sleep(this.buttonClickDelay)
|
|
}
|
|
|
|
// 5. Capture data from textbox
|
|
console.log('Capturing data from the page...')
|
|
const textboxData = await this.capturePageData(page)
|
|
console.log('Captured data from the page:', textboxData)
|
|
|
|
// 6. Close browser
|
|
console.log('Closing browser...')
|
|
await browser.close()
|
|
browser = null
|
|
|
|
// 7. Shut down server
|
|
console.log('Shutting down server...')
|
|
await this.stopServer(server)
|
|
server = null
|
|
|
|
return textboxData
|
|
} catch (error) {
|
|
// Cleanup in case of error
|
|
if (browser) {
|
|
try {
|
|
await browser.close()
|
|
} catch (e) {
|
|
console.error('Error closing browser:', e.message)
|
|
}
|
|
}
|
|
if (server) {
|
|
try {
|
|
await this.stopServer(server)
|
|
} catch (e) {
|
|
console.error('Error stopping server:', e.message)
|
|
}
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
async startServer() {
|
|
return new Promise((resolve, reject) => {
|
|
const server = spawn('pnpm', ['start'], {
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
shell: true,
|
|
})
|
|
|
|
let serverReady = false
|
|
|
|
server.stdout.on('data', (data) => {
|
|
const output = data.toString()
|
|
console.log('Server:', output.trim())
|
|
|
|
// Look for common Next.js ready indicators
|
|
if (
|
|
output.includes('Ready') ||
|
|
output.includes('started server') ||
|
|
output.includes('Local:')
|
|
) {
|
|
if (!serverReady) {
|
|
serverReady = true
|
|
resolve(server)
|
|
}
|
|
}
|
|
})
|
|
|
|
server.stderr.on('data', (data) => {
|
|
console.error('Server Error:', data.toString().trim())
|
|
})
|
|
|
|
server.on('error', (error) => {
|
|
reject(new Error(`Failed to start server: ${error.message}`))
|
|
})
|
|
|
|
server.on('close', (code) => {
|
|
if (!serverReady) {
|
|
reject(
|
|
new Error(`Server exited with code ${code} before becoming ready`)
|
|
)
|
|
}
|
|
})
|
|
|
|
// Timeout after 30 seconds
|
|
setTimeout(() => {
|
|
if (!serverReady) {
|
|
server.kill()
|
|
reject(new Error('Server startup timeout'))
|
|
}
|
|
}, 30000)
|
|
})
|
|
}
|
|
|
|
async stopServer(server) {
|
|
return new Promise((resolve) => {
|
|
if (!server || server.killed) {
|
|
resolve()
|
|
return
|
|
}
|
|
|
|
server.on('close', () => {
|
|
resolve()
|
|
})
|
|
|
|
// Try graceful shutdown first
|
|
server.kill('SIGTERM')
|
|
|
|
// Force kill after 5 seconds
|
|
setTimeout(() => {
|
|
if (!server.killed) {
|
|
server.kill('SIGKILL')
|
|
}
|
|
resolve()
|
|
}, 5000)
|
|
})
|
|
}
|
|
|
|
async capturePageData(page) {
|
|
return await page.evaluate(() => globalThis.BENCHMARK_RESULTS)
|
|
}
|
|
|
|
async sleep(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
}
|
|
|
|
saveResults() {
|
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
|
|
const filename = `benchmark-results-${this.name}-${timestamp}.json`
|
|
|
|
writeFileSync(
|
|
filename,
|
|
JSON.stringify(summarizeDurations(this.results), null, 2)
|
|
)
|
|
console.log(`Results saved to ${filename}`)
|
|
}
|
|
}
|
|
|
|
const summarizeDurations = (data) => {
|
|
if (!Array.isArray(data) || data.length === 0) {
|
|
throw new Error('No data to summarize')
|
|
}
|
|
|
|
const byName = new Map()
|
|
for (const item of data) {
|
|
const name = item.name
|
|
if (!byName.has(name)) {
|
|
byName.set(name, [])
|
|
}
|
|
byName.get(name).push(item)
|
|
}
|
|
const results = []
|
|
for (const [name, data] of byName) {
|
|
const loadDurations = data
|
|
.map((item) => item.loadDuration)
|
|
.sort((a, b) => a - b)
|
|
const executeDurations = data
|
|
.map((item) => item.executeDuration)
|
|
.sort((a, b) => a - b)
|
|
|
|
const getSummary = (durations) => {
|
|
const sum = durations.reduce((acc, val) => acc + val, 0)
|
|
const average = sum / durations.length
|
|
|
|
const middle = Math.floor(durations.length / 2)
|
|
const median =
|
|
durations.length % 2 === 0
|
|
? (durations[middle - 1] + durations[middle]) / 2
|
|
: durations[middle]
|
|
|
|
const percentile75Index = Math.floor(durations.length * 0.75)
|
|
const percentile75 = durations[percentile75Index]
|
|
|
|
return {
|
|
average,
|
|
median,
|
|
percentile75,
|
|
}
|
|
}
|
|
|
|
results.push({
|
|
name,
|
|
totalSamples: data.length,
|
|
loadDuration: getSummary(loadDurations),
|
|
executeDuration: getSummary(executeDurations),
|
|
})
|
|
}
|
|
|
|
return results
|
|
}
|
|
|
|
// CLI usage
|
|
const args = process.argv.slice(2)
|
|
const samples = args.length > 0 ? Number.parseInt(args[0]) : undefined
|
|
const name = args.length > 1 ? args[1] : undefined
|
|
|
|
const runner = new BenchmarkRunner({
|
|
name,
|
|
samples,
|
|
})
|
|
|
|
runner.runBenchmark().catch(console.error)
|