Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled
300 lines
9.2 KiB
JavaScript
300 lines
9.2 KiB
JavaScript
#!/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)
|
|
})
|