next.js/scripts/profile-next-dev-boot.js
profile-next-dev-boot.js300 lines9.2 KB
#!/usr/bin/env node
/**
 * Next.js CPU Profile Script
 *
 * Generates CPU profiles for Next.js startup and dev server boot.
 *
 * Usage:
 *   node scripts/profile-next-dev-boot.js [options]
 *
 * Options:
 *   --test-dir=PATH     Test project directory (default: /private/tmp/next-boot-test)
 *   --output-dir=PATH   Output directory for profiles (default: ./profiles)
 *   --turbopack         Use Turbopack (default)
 *   --webpack           Use Webpack
 *   --duration=MS       How long to profile after ready (default: 1000)
 *   --cli               Profile just the CLI entry point (runs next --help)
 *
 * Output files:
 *   - dev-turbopack-YYYY-MM-DDTHH-MM-SS.cpuprofile
 *   - cli-turbopack-YYYY-MM-DDTHH-MM-SS.cpuprofile
 *
 * The profile can be loaded in:
 *   - Chrome DevTools (Performance tab -> Load profile)
 *   - VS Code (JavaScript Profile Visualizer extension)
 *   - https://www.speedscope.app/
 *
 * Note: Currently profiles the parent process only. For child process profiling,
 * additional Next.js changes are needed (see future PRs).
 */

const { spawn, execSync } = require('child_process')
const path = require('path')
const fs = require('fs')

// Parse arguments
const args = process.argv.slice(2)
const getArg = (name, defaultValue) => {
  const arg = args.find((a) => a.startsWith(`--${name}=`))
  return arg ? arg.split('=')[1] : defaultValue
}
const hasFlag = (name) => args.includes(`--${name}`)

const testDir = getArg('test-dir', '/private/tmp/next-boot-test')
const baseOutputDir =
  getArg('output-dir', null) || path.join(process.cwd(), 'profiles')
const useWebpack = hasFlag('webpack')
const duration = parseInt(getArg('duration', '1000'), 10)
const profileCli = hasFlag('cli')
const bundlerFlag = useWebpack ? '--webpack' : '--turbopack'

// Generate meaningful profile names
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
const bundlerName = useWebpack ? 'webpack' : 'turbopack'
const profileType = profileCli ? 'cli' : 'dev'
const outputDir = baseOutputDir
const profileName = `${profileType}-${bundlerName}-${timestamp}`

const nextDir = path.join(__dirname, '..', 'packages', 'next')
const nextBin = path.join(nextDir, 'dist/bin/next')

if (profileCli) {
  console.log('\x1b[34m=== Next.js CLI Entry Point Profile ===\x1b[0m')
} else {
  console.log('\x1b[34m=== Next.js Dev Server CPU Profile ===\x1b[0m')
  console.log(`Test directory: ${testDir}`)
  console.log(`Bundler: ${useWebpack ? 'Webpack' : 'Turbopack'}`)
}
console.log(`Output directory: ${outputDir}`)
console.log('')

// Verify test directory (only for dev server profiling)
if (!profileCli && !fs.existsSync(testDir)) {
  console.error(
    `\x1b[31mError: Test directory does not exist: ${testDir}\x1b[0m`
  )
  process.exit(1)
}

// Create output directory
if (!fs.existsSync(outputDir)) {
  fs.mkdirSync(outputDir, { recursive: true })
}

// Kill existing processes
function killNextDev() {
  try {
    execSync('pkill -f "next dev"', { stdio: 'ignore' })
  } catch {}
}

async function runProfile() {
  killNextDev()
  await new Promise((r) => setTimeout(r, 500))

  // Clean .next directory
  const nextCache = path.join(testDir, '.next')
  if (fs.existsSync(nextCache)) {
    fs.rmSync(nextCache, { recursive: true, force: true })
  }

  console.log('Starting dev server with CPU profiling...')
  console.log('(Profile will be saved after server is ready)')
  console.log('')

  return new Promise((resolve, reject) => {
    let resolved = false

    // Profile the parent process with --cpu-prof
    const spawnArgs = [
      process.execPath,
      [
        '--cpu-prof',
        `--cpu-prof-dir=${outputDir}`,
        `--cpu-prof-name=${profileName}`,
        nextBin,
        'dev',
        bundlerFlag,
      ],
    ]

    const child = spawn(spawnArgs[0], spawnArgs[1], {
      cwd: testDir,
      stdio: ['ignore', 'pipe', 'pipe'],
      env: { ...process.env, FORCE_COLOR: '0' },
    })

    let output = ''

    const onData = (data) => {
      const text = data.toString()
      output += text
      process.stdout.write(text)

      // Wait for "Ready in Xms"
      if (output.includes('Ready in') && !resolved) {
        resolved = true
        console.log('')
        console.log(
          `\x1b[33mServer ready, profiling for ${duration}ms more...\x1b[0m`
        )

        // Wait a bit then stop
        setTimeout(() => {
          console.log('Stopping server and saving profile...')
          child.kill('SIGINT')
        }, duration)
      }
    }

    child.stdout.on('data', onData)
    child.stderr.on('data', onData)

    child.on('close', (code) => {
      killNextDev()

      // Wait a moment for profile files to be written
      setTimeout(() => {
        // Find and rename profiles matching our name pattern
        // --cpu-prof-name creates files without extension
        const files = fs.readdirSync(outputDir)
        const rawFiles = files.filter(
          (f) => f.startsWith(profileName) && !f.endsWith('.cpuprofile')
        )

        // Rename raw files to have .cpuprofile extension
        rawFiles.forEach((f) => {
          const oldPath = path.join(outputDir, f)
          const newPath = path.join(outputDir, `${f}.cpuprofile`)
          fs.renameSync(oldPath, newPath)
        })

        // Now find all .cpuprofile files
        const profileFiles = fs
          .readdirSync(outputDir)
          .filter((f) => f.startsWith(profileName) && f.endsWith('.cpuprofile'))
        const profiles = profileFiles
          .map((f) => ({
            name: f,
            path: path.join(outputDir, f),
            size: fs.statSync(path.join(outputDir, f)).size,
          }))
          .filter((p) => p.size > 0)
          .sort((a, b) => b.size - a.size)

        if (profiles.length > 0) {
          console.log('')
          console.log(`\x1b[32mProfile(s) saved:\x1b[0m`)
          profiles.forEach((p, i) => {
            const sizeKB = Math.round(p.size / 1024)
            console.log(`  ${i + 1}. ${p.path} (${sizeKB} KB)`)
          })
          console.log('')
          console.log('To view the profile:')
          console.log('  1. Open Chrome DevTools -> Performance tab')
          console.log('  2. Click "Load profile" and select the file')
          console.log('  3. Or use https://www.speedscope.app/')
          console.log('')
          console.log(
            '\x1b[33mTip:\x1b[0m The largest profile is usually the child process (server worker)'
          )
          resolve(profiles[0].path)
        } else {
          console.log('')
          console.log(
            '\x1b[33mNo profiles found. Trying alternative method...\x1b[0m'
          )
          console.log('')
          console.log(
            'To profile the child process, modify next-dev.ts to add profiling flags.'
          )
          console.log(
            'Or use: node --cpu-prof --cpu-prof-dir=./profiles ./dist/bin/next dev'
          )
          reject(new Error('Profile file not found'))
        }
      }, 500)
    })

    child.on('error', reject)

    // Timeout
    setTimeout(() => {
      if (!resolved) {
        child.kill('SIGKILL')
        reject(new Error('Timeout waiting for server'))
      }
    }, 120000)
  })
}

async function runCliProfile() {
  console.log('Profiling CLI entry point (next --help)...')
  console.log('')

  return new Promise((resolve, reject) => {
    const child = spawn(
      process.execPath,
      [
        '--cpu-prof',
        `--cpu-prof-dir=${outputDir}`,
        `--cpu-prof-name=${profileName}`,
        nextBin,
        '--help',
      ],
      {
        cwd: process.cwd(),
        stdio: ['ignore', 'pipe', 'pipe'],
        env: { ...process.env, FORCE_COLOR: '0' },
      }
    )

    child.stdout.on('data', () => {})
    child.stderr.on('data', () => {})

    child.on('close', (code) => {
      // Wait for profile to be written
      setTimeout(() => {
        // --cpu-prof-name creates files without extension, rename to .cpuprofile
        const rawFile = path.join(outputDir, profileName)
        const finalFile = path.join(outputDir, `${profileName}.cpuprofile`)

        if (fs.existsSync(rawFile)) {
          fs.renameSync(rawFile, finalFile)
          const size = fs.statSync(finalFile).size
          console.log(`\x1b[32mProfile saved:\x1b[0m`)
          console.log(`  ${finalFile} (${Math.round(size / 1024)} KB)`)
          console.log('')
          console.log('To view the profile:')
          console.log('  1. Open Chrome DevTools -> Performance tab')
          console.log('  2. Click "Load profile" and select the file')
          console.log('  3. Or use https://www.speedscope.app/')
          console.log('')
          console.log(
            '\x1b[33mTip:\x1b[0m Look for heavy modules loaded at startup'
          )
          resolve(finalFile)
        } else if (fs.existsSync(finalFile)) {
          const size = fs.statSync(finalFile).size
          console.log(`\x1b[32mProfile saved:\x1b[0m`)
          console.log(`  ${finalFile} (${Math.round(size / 1024)} KB)`)
          resolve(finalFile)
        } else {
          reject(new Error('Profile file not found'))
        }
      }, 500)
    })

    child.on('error', reject)
  })
}

// Main execution
const main = profileCli ? runCliProfile : runProfile

main().catch((err) => {
  console.error('\x1b[31mError:\x1b[0m', err.message)
  if (!profileCli) killNextDev()
  process.exit(1)
})
Quest for Codev2.0.0
/
SIGN IN