| | #!/usr/bin/env node |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | const { spawn, execSync } = require('child_process') |
| | const path = require('path') |
| | const fs = require('fs') |
| |
|
| | |
| | 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' |
| |
|
| | |
| | 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('') |
| |
|
| | |
| | if (!profileCli && !fs.existsSync(testDir)) { |
| | console.error( |
| | `\x1b[31mError: Test directory does not exist: ${testDir}\x1b[0m` |
| | ) |
| | process.exit(1) |
| | } |
| |
|
| | |
| | if (!fs.existsSync(outputDir)) { |
| | fs.mkdirSync(outputDir, { recursive: true }) |
| | } |
| |
|
| | |
| | function killNextDev() { |
| | try { |
| | execSync('pkill -f "next dev"', { stdio: 'ignore' }) |
| | } catch {} |
| | } |
| |
|
| | async function runProfile() { |
| | killNextDev() |
| | await new Promise((r) => setTimeout(r, 500)) |
| |
|
| | |
| | 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 |
| |
|
| | |
| | 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) |
| |
|
| | |
| | if (output.includes('Ready in') && !resolved) { |
| | resolved = true |
| | console.log('') |
| | console.log( |
| | `\x1b[33mServer ready, profiling for ${duration}ms more...\x1b[0m` |
| | ) |
| |
|
| | |
| | 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() |
| |
|
| | |
| | setTimeout(() => { |
| | |
| | |
| | const files = fs.readdirSync(outputDir) |
| | const rawFiles = files.filter( |
| | (f) => f.startsWith(profileName) && !f.endsWith('.cpuprofile') |
| | ) |
| |
|
| | |
| | rawFiles.forEach((f) => { |
| | const oldPath = path.join(outputDir, f) |
| | const newPath = path.join(outputDir, `${f}.cpuprofile`) |
| | fs.renameSync(oldPath, newPath) |
| | }) |
| |
|
| | |
| | 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) |
| |
|
| | |
| | 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) => { |
| | |
| | setTimeout(() => { |
| | |
| | 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) |
| | }) |
| | } |
| |
|
| | |
| | const main = profileCli ? runCliProfile : runProfile |
| |
|
| | main().catch((err) => { |
| | console.error('\x1b[31mError:\x1b[0m', err.message) |
| | if (!profileCli) killNextDev() |
| | process.exit(1) |
| | }) |
| |
|