Skip to content

Commit

Permalink
Add option for debugging CPU usage (#51174)
Browse files Browse the repository at this point in the history
Add a `NEXT_CPU_PROF` environment variable to programmatically profile CPU for the router worker and two render workers. This is helpful because the `--cpu-prof` isn't reliably writing the result (#27406) especially with our multi-process architecture.

![CleanShot-2023-06-12-GS9p3SbN@2x](https://github.com/vercel/next.js/assets/3676859/b7cf80d2-a35a-4e90-978d-dec70be89f90)
  • Loading branch information
shuding committed Jun 12, 2023
1 parent e5a45c8 commit b664cdb
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 36 deletions.
28 changes: 28 additions & 0 deletions packages/next/src/server/lib/cpu-profile.ts
@@ -0,0 +1,28 @@
if (process.env.__NEXT_PRIVATE_CPU_PROFILE) {
const { Session } = require('inspector') as typeof import('inspector')
const fs = require('fs')

const session = new Session()
session.connect()

session.post('Profiler.enable')
session.post('Profiler.start')

function saveProfile() {
session.post('Profiler.stop', (error, param) => {
if (error) {
console.error('Cannot generate CPU profiling:', error)
return
}

// Write profile to disk
const filename = `${
process.env.__NEXT_PRIVATE_CPU_PROFILE
}.${Date.now()}.cpuprofile`
fs.writeFileSync(`./${filename}`, JSON.stringify(param.profile))
process.exit(0)
})
}
process.on('SIGINT', saveProfile)
process.on('SIGTERM', saveProfile)
}
1 change: 1 addition & 0 deletions packages/next/src/server/lib/render-server.ts
@@ -1,5 +1,6 @@
import type { RequestHandler } from '../next'

import './cpu-profile'
import v8 from 'v8'
import http from 'http'
import { isIPv6 } from 'net'
Expand Down
9 changes: 5 additions & 4 deletions packages/next/src/server/lib/server-ipc/index.ts
Expand Up @@ -4,7 +4,7 @@ import { getNodeOptionsWithoutInspect } from '../utils'
import { deserializeErr, errorToJSON } from '../../render'
import crypto from 'crypto'
import isError from '../../../lib/is-error'
import { genRenderExecArgv, getFreePort } from '../worker-utils'
import { genRenderExecArgv } from '../worker-utils'

// we can't use process.send as jest-worker relies on
// it already and can cause unexpected message errors
Expand Down Expand Up @@ -115,10 +115,11 @@ export const createWorker = async (
: 'next',
}
: {}),
...(process.env.NEXT_CPU_PROF
? { __NEXT_PRIVATE_CPU_PROFILE: `CPU.${type}-renderer` }
: {}),
},
execArgv: isNodeDebugging
? genRenderExecArgv(await getFreePort(), type)
: undefined,
execArgv: await genRenderExecArgv(isNodeDebugging, type),
},
exposedMethods: [
'initialize',
Expand Down
15 changes: 7 additions & 8 deletions packages/next/src/server/lib/start-server.ts
Expand Up @@ -7,8 +7,7 @@ import { isIPv6 } from 'net'
import * as Log from '../../build/output/log'
import { normalizeRepeatedSlashes } from '../../shared/lib/utils'
import { initialEnv } from '@next/env'
import { genExecArgv, getNodeOptionsWithoutInspect } from './utils'
import { getFreePort } from './worker-utils'
import { genRouterWorkerExecArgv, getNodeOptionsWithoutInspect } from './utils'

export interface StartServerOptions {
dir: string
Expand Down Expand Up @@ -187,17 +186,17 @@ export async function startServer({
// TODO: do we want to allow more than 10 OOM restarts?
maxRetries: 10,
forkOptions: {
execArgv: isNodeDebugging
? genExecArgv(
isNodeDebugging === undefined ? false : isNodeDebugging,
await getFreePort()
)
: undefined,
execArgv: await genRouterWorkerExecArgv(
isNodeDebugging === undefined ? false : isNodeDebugging
),
env: {
FORCE_COLOR: '1',
...((initialEnv || process.env) as typeof process.env),
PORT: port + '',
NODE_OPTIONS: getNodeOptionsWithoutInspect(),
...(process.env.NEXT_CPU_PROF
? { __NEXT_PRIVATE_CPU_PROFILE: `CPU.router` }
: {}),
},
},
exposedMethods: ['initialize'],
Expand Down
12 changes: 8 additions & 4 deletions packages/next/src/server/lib/utils.ts
@@ -1,4 +1,5 @@
import type arg from 'next/dist/compiled/arg/index.js'
import { getFreePort } from './worker-utils'

export function printAndExit(message: string, code = 1) {
if (code === 0) {
Expand All @@ -10,24 +11,27 @@ export function printAndExit(message: string, code = 1) {
process.exit(code)
}

export const genExecArgv = (enabled: boolean | 'brk', debugPort: number) => {
export const genRouterWorkerExecArgv = async (
isNodeDebugging: boolean | 'brk'
) => {
const execArgv = process.execArgv.filter((localArg) => {
return (
!localArg.startsWith('--inspect') && !localArg.startsWith('--inspect-brk')
)
})

if (enabled) {
if (isNodeDebugging) {
const debugPort = await getFreePort()
execArgv.push(
`--inspect${enabled === 'brk' ? '-brk' : ''}=${debugPort + 1}`
`--inspect${isNodeDebugging === 'brk' ? '-brk' : ''}=${debugPort + 1}`
)
}

return execArgv
}

const NODE_INSPECT_RE = /--inspect(-brk)?(=\S+)?( |$)/
export function getNodeOptionsWithoutInspect() {
const NODE_INSPECT_RE = /--inspect(-brk)?(=\S+)?( |$)/
return (process.env.NODE_OPTIONS || '').replace(NODE_INSPECT_RE, '')
}

Expand Down
48 changes: 28 additions & 20 deletions packages/next/src/server/lib/worker-utils.ts
Expand Up @@ -17,32 +17,40 @@ export const getFreePort = async (): Promise<number> => {
})
}

export const genRenderExecArgv = (debugPort: number, type: 'pages' | 'app') => {
const isDebugging =
process.execArgv.some((localArg) => localArg.startsWith('--inspect')) ||
process.env.NODE_OPTIONS?.match?.(/--inspect(=\S+)?( |$)/)

const isDebuggingWithBrk =
process.execArgv.some((localArg) => localArg.startsWith('--inspect-brk')) ||
process.env.NODE_OPTIONS?.match?.(/--inspect-brk(=\S+)?( |$)/)

if (isDebugging || isDebuggingWithBrk) {
Log.info(
`the --inspect${
isDebuggingWithBrk ? '-brk' : ''
} option was detected, the Next.js server${
type === 'pages' ? ' for pages' : type === 'app' ? ' for app' : ''
} should be inspected at port ${debugPort}.`
)
}
export const genRenderExecArgv = async (
isNodeDebugging: string | boolean | undefined,
type: 'pages' | 'app'
) => {
const execArgv = process.execArgv.filter((localArg) => {
return (
!localArg.startsWith('--inspect') && !localArg.startsWith('--inspect-brk')
)
})

if (isDebugging || isDebuggingWithBrk) {
execArgv.push(`--inspect${isDebuggingWithBrk ? '-brk' : ''}=${debugPort}`)
if (isNodeDebugging) {
const debugPort = await getFreePort()
const isDebugging =
process.execArgv.some((localArg) => localArg.startsWith('--inspect')) ||
process.env.NODE_OPTIONS?.match?.(/--inspect(=\S+)?( |$)/)

const isDebuggingWithBrk =
process.execArgv.some((localArg) =>
localArg.startsWith('--inspect-brk')
) || process.env.NODE_OPTIONS?.match?.(/--inspect-brk(=\S+)?( |$)/)

if (isDebugging || isDebuggingWithBrk) {
Log.info(
`the --inspect${
isDebuggingWithBrk ? '-brk' : ''
} option was detected, the Next.js server${
type === 'pages' ? ' for pages' : type === 'app' ? ' for app' : ''
} should be inspected at port ${debugPort}.`
)
}

if (isDebugging || isDebuggingWithBrk) {
execArgv.push(`--inspect${isDebuggingWithBrk ? '-brk' : ''}=${debugPort}`)
}
}

return execArgv
Expand Down

0 comments on commit b664cdb

Please sign in to comment.