next.js / scripts /profile-next-dev-boot.js
AbdulElahGwaith's picture
Upload folder using huggingface_hub
b91e262 verified
#!/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)
})