| | import { spawn } from 'node:child_process' |
| | import { writeFileSync } from 'node:fs' |
| | import { chromium } from 'playwright' |
| |
|
| | |
| | |
| | |
| | |
| | |
| |
|
| | 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 { |
| | |
| | server = await this.startServer() |
| |
|
| | |
| | console.log('Launching browser...') |
| | browser = await chromium.launch({ |
| | headless: true, |
| | args: ['--incognito'], |
| | }) |
| |
|
| | const context = await browser.newContext() |
| | const page = await context.newPage() |
| |
|
| | |
| | await page.goto('http://localhost:3000', { waitUntil: 'load' }) |
| |
|
| | |
| | const buttons = await page.locator('button').all() |
| |
|
| | for (let j = 0; j < buttons.length; j++) { |
| | await buttons[j].click() |
| | await this.sleep(this.buttonClickDelay) |
| | } |
| |
|
| | |
| | console.log('Capturing data from the page...') |
| | const textboxData = await this.capturePageData(page) |
| | console.log('Captured data from the page:', textboxData) |
| |
|
| | |
| | console.log('Closing browser...') |
| | await browser.close() |
| | browser = null |
| |
|
| | |
| | console.log('Shutting down server...') |
| | await this.stopServer(server) |
| | server = null |
| |
|
| | return textboxData |
| | } catch (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()) |
| |
|
| | |
| | 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`) |
| | ) |
| | } |
| | }) |
| |
|
| | |
| | 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() |
| | }) |
| |
|
| | |
| | server.kill('SIGTERM') |
| |
|
| | |
| | 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 |
| | } |
| |
|
| | |
| | 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) |
| |
|