next.js/bench/module-cost/scripts/benchmark-runner.mjs
benchmark-runner.mjs256 lines6.3 KB
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)
Quest for Codev2.0.0
/
SIGN IN