Skip to content

Commit e2c9686

Browse files
authored
feat(build,dev): add --profile support (#1243)
1 parent 77f8619 commit e2c9686

10 files changed

Lines changed: 255 additions & 52 deletions

File tree

packages/nuxi/bin/nuxi.mjs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/usr/bin/env node
22

3+
import inspector from 'node:inspector'
34
import nodeModule from 'node:module'
45
import process from 'node:process'
56
import { fileURLToPath } from 'node:url'
@@ -23,6 +24,22 @@ globalThis.__nuxt_cli__ = {
2324
startTime: Date.now(),
2425
entry: fileURLToPath(import.meta.url),
2526
devEntry: fileURLToPath(new URL('../dist/dev/index.mjs', import.meta.url)),
27+
cpuProfileSession: undefined,
28+
}
29+
30+
if (
31+
process.argv.includes('--profile')
32+
|| process.argv.some(a => a.startsWith('--profile='))
33+
) {
34+
const session = new inspector.Session()
35+
session.connect()
36+
// eslint-disable-next-line antfu/no-top-level-await
37+
await new Promise((resolve) => {
38+
session.post('Profiler.enable', () => {
39+
session.post('Profiler.start', resolve)
40+
})
41+
})
42+
globalThis.__nuxt_cli__.cpuProfileSession = session
2643
}
2744

2845
// eslint-disable-next-line antfu/no-top-level-await

packages/nuxi/src/commands/_shared.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ export const extendsArgs = {
4040
},
4141
} as const satisfies Record<string, ArgDef>
4242

43+
export const profileArgs = {
44+
profile: {
45+
type: 'string',
46+
description: 'Profile performance. Use --profile for CPU only, --profile=verbose for full report.',
47+
default: undefined as string | undefined,
48+
valueHint: 'verbose',
49+
},
50+
} as const satisfies Record<string, ArgDef>
51+
4352
export const legacyRootDirArgs = {
4453
// cwd falls back to rootDir's default (indirect default)
4554
cwd: {

packages/nuxi/src/commands/build.ts

Lines changed: 74 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import { overrideEnv } from '../utils/env'
1010
import { clearBuildDir } from '../utils/fs'
1111
import { loadKit } from '../utils/kit'
1212
import { logger } from '../utils/logger'
13-
import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared'
13+
import { startCpuProfile, stopCpuProfile } from '../utils/profile'
14+
import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs, profileArgs } from './_shared'
1415

1516
export default defineCommand({
1617
meta: {
@@ -31,74 +32,98 @@ export default defineCommand({
3132
...dotEnvArgs,
3233
...envNameArgs,
3334
...extendsArgs,
35+
...profileArgs,
3436
...legacyRootDirArgs,
3537
},
3638
async run(ctx) {
3739
overrideEnv('production')
3840

3941
const cwd = resolve(ctx.args.cwd || ctx.args.rootDir)
4042

41-
intro(colors.cyan('Building Nuxt for production...'))
43+
const profileArg = ctx.args.profile
44+
const perfValue = profileArg === 'verbose' ? true : profileArg ? 'quiet' : undefined
45+
if (profileArg) {
46+
await startCpuProfile()
47+
}
48+
49+
try {
50+
intro(colors.cyan('Building Nuxt for production...'))
4251

43-
const kit = await loadKit(cwd)
52+
const kit = await loadKit(cwd)
4453

45-
await showVersions(cwd, kit, ctx.args.dotenv)
46-
const nuxt = await kit.loadNuxt({
47-
cwd,
48-
dotenv: {
54+
await showVersions(cwd, kit, ctx.args.dotenv)
55+
56+
const nuxt = await kit.loadNuxt({
4957
cwd,
50-
fileName: ctx.args.dotenv,
51-
},
52-
envName: ctx.args.envName, // c12 will fall back to NODE_ENV
53-
overrides: {
54-
logLevel: ctx.args.logLevel as 'silent' | 'info' | 'verbose',
55-
// TODO: remove in 3.8
56-
_generate: ctx.args.prerender,
57-
nitro: {
58-
static: ctx.args.prerender,
59-
preset: ctx.args.preset || process.env.NITRO_PRESET || process.env.SERVER_PRESET,
58+
dotenv: {
59+
cwd,
60+
fileName: ctx.args.dotenv,
61+
},
62+
envName: ctx.args.envName, // c12 will fall back to NODE_ENV
63+
overrides: {
64+
logLevel: ctx.args.logLevel as 'silent' | 'info' | 'verbose',
65+
// TODO: remove in 3.8
66+
_generate: ctx.args.prerender,
67+
nitro: {
68+
static: ctx.args.prerender,
69+
preset: ctx.args.preset || process.env.NITRO_PRESET || process.env.SERVER_PRESET,
70+
},
71+
...(ctx.args.extends && { extends: ctx.args.extends }),
72+
...ctx.data?.overrides,
73+
...((perfValue || ctx.data?.overrides?.debug) && {
74+
debug: {
75+
...ctx.data?.overrides?.debug,
76+
...(perfValue && { perf: perfValue }),
77+
},
78+
}),
6079
},
61-
...(ctx.args.extends && { extends: ctx.args.extends }),
62-
...ctx.data?.overrides,
63-
},
64-
})
80+
})
6581

66-
let nitro: ReturnType<typeof kit.useNitro> | undefined
67-
// In Bridge, if Nitro is not enabled, useNitro will throw an error
68-
try {
69-
// Use ? for backward compatibility for Nuxt <= RC.10
70-
nitro = kit.useNitro?.()
71-
if (nitro) {
72-
logger.info(`Nitro preset: ${colors.cyan(nitro.options.preset)}`)
82+
let nitro: ReturnType<typeof kit.useNitro> | undefined
83+
// In Bridge, if Nitro is not enabled, useNitro will throw an error
84+
try {
85+
// Use ? for backward compatibility for Nuxt <= RC.10
86+
nitro = kit.useNitro?.()
87+
if (nitro) {
88+
logger.info(`Nitro preset: ${colors.cyan(nitro.options.preset)}`)
89+
}
90+
}
91+
catch {
92+
//
7393
}
74-
}
75-
catch {
76-
//
77-
}
7894

79-
await clearBuildDir(nuxt.options.buildDir)
95+
await clearBuildDir(nuxt.options.buildDir)
8096

81-
await kit.writeTypes(nuxt)
97+
await kit.writeTypes(nuxt)
8298

83-
nuxt.hook('build:error', (err) => {
84-
logger.error(`Nuxt build error: ${err}`)
85-
process.exit(1)
86-
})
99+
nuxt.hook('build:error', async (err) => {
100+
logger.error(`Nuxt build error: ${err}`)
101+
if (profileArg) {
102+
await stopCpuProfile(cwd, 'build')
103+
}
104+
process.exit(1)
105+
})
87106

88-
await kit.buildNuxt(nuxt)
107+
await kit.buildNuxt(nuxt)
89108

90-
if (ctx.args.prerender) {
91-
if (!nuxt.options.ssr) {
92-
logger.warn(`HTML content not prerendered because ${colors.cyan('ssr: false')} was set.`)
93-
logger.info(`You can read more in ${colors.cyan('https://nuxt.com/docs/getting-started/deployment#static-hosting')}.`)
109+
if (ctx.args.prerender) {
110+
if (!nuxt.options.ssr) {
111+
logger.warn(`HTML content not prerendered because ${colors.cyan('ssr: false')} was set.`)
112+
logger.info(`You can read more in ${colors.cyan('https://nuxt.com/docs/getting-started/deployment#static-hosting')}.`)
113+
}
114+
// TODO: revisit later if/when nuxt build --prerender will output hybrid
115+
const dir = nitro?.options.output.publicDir
116+
const publicDir = dir ? relative(process.cwd(), dir) : '.output/public'
117+
outro(`✨ You can now deploy ${colors.cyan(publicDir)} to any static hosting!`)
118+
}
119+
else {
120+
outro('✨ Build complete!')
94121
}
95-
// TODO: revisit later if/when nuxt build --prerender will output hybrid
96-
const dir = nitro?.options.output.publicDir
97-
const publicDir = dir ? relative(process.cwd(), dir) : '.output/public'
98-
outro(`✨ You can now deploy ${colors.cyan(publicDir)} to any static hosting!`)
99122
}
100-
else {
101-
outro('✨ Build complete!')
123+
finally {
124+
if (profileArg) {
125+
await stopCpuProfile(cwd, 'build')
126+
}
102127
}
103128
},
104129
})

packages/nuxi/src/commands/dev.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { isBun, isTest } from 'std-env'
1313
import { initialize } from '../dev'
1414
import { ForkPool } from '../dev/pool'
1515
import { debug, logger } from '../utils/logger'
16-
import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared'
16+
import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs, profileArgs } from './_shared'
1717

1818
const startTime: number | undefined = Date.now()
1919
const forkSupported = !isTest && (!isBun || isBunForkSupported())
@@ -62,6 +62,7 @@ const command = defineCommand({
6262
},
6363
clipboard: { ...listhenArgs.clipboard, default: false },
6464
},
65+
...profileArgs,
6566
sslCert: {
6667
type: 'string',
6768
description: '(DEPRECATED) Use `--https.cert` instead.',
@@ -84,7 +85,8 @@ const command = defineCommand({
8485
showBanner: true,
8586
})
8687

87-
if (!ctx.args.fork) {
88+
// Disable forking when profiling to capture all activity in one process
89+
if (!ctx.args.fork || ctx.args.profile) {
8890
return {
8991
listener,
9092
close,

packages/nuxi/src/commands/generate.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { defineCommand } from 'citty'
22

3-
import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared'
3+
import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs, profileArgs } from './_shared'
44
import buildCommand from './build'
55

66
export default defineCommand({
@@ -18,6 +18,7 @@ export default defineCommand({
1818
...dotEnvArgs,
1919
...envNameArgs,
2020
...extendsArgs,
21+
...profileArgs,
2122
...legacyRootDirArgs,
2223
},
2324
async run(ctx) {

packages/nuxi/src/dev/index.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { NuxtDevContext, NuxtDevIPCMessage, NuxtParentIPCMessage } from './
55
import process from 'node:process'
66
import defu from 'defu'
77
import { overrideEnv } from '../utils/env.ts'
8+
import { startCpuProfile, stopCpuProfile } from '../utils/profile.ts'
89
import { NuxtDevServer } from './utils'
910

1011
const start = Date.now()
@@ -55,11 +56,22 @@ interface InitializeReturn {
5556
export async function initialize(devContext: NuxtDevContext, ctx: InitializeOptions = {}): Promise<InitializeReturn> {
5657
overrideEnv('development')
5758

59+
const profileArg = devContext.args.profile
60+
const perfValue = profileArg === 'verbose' ? true : profileArg ? 'quiet' : undefined
61+
const perfOverrides = perfValue
62+
? { debug: { perf: perfValue } } as NuxtConfig
63+
: {}
64+
65+
if (profileArg) {
66+
await startCpuProfile()
67+
}
68+
5869
const devServer = new NuxtDevServer({
5970
cwd: devContext.cwd,
6071
overrides: defu(
6172
ctx.data?.overrides,
6273
({ extends: devContext.args.extends } satisfies NuxtConfig) as NuxtConfig,
74+
perfOverrides,
6375
),
6476
logLevel: devContext.args.logLevel as 'silent' | 'info' | 'verbose',
6577
clear: devContext.args.clear,
@@ -107,6 +119,17 @@ export async function initialize(devContext: NuxtDevContext, ctx: InitializeOpti
107119
console.debug(`Dev server (internal) initialized in ${Date.now() - start}ms`)
108120
}
109121

122+
if (profileArg) {
123+
for (const signal of [
124+
'exit',
125+
'SIGTERM' /* Graceful shutdown */,
126+
'SIGINT' /* Ctrl-C */,
127+
'SIGQUIT' /* Ctrl-\ */,
128+
] as const) {
129+
process.once(signal, () => stopCpuProfile(devContext.cwd, 'dev'))
130+
}
131+
}
132+
110133
return {
111134
listener: devServer.listener,
112135
close: async () => {

packages/nuxi/src/dev/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export interface NuxtDevContext {
4848
dotenv?: string
4949
envName?: string
5050
extends?: string
51+
profile?: string | boolean
5152
}
5253
}
5354

packages/nuxi/src/utils/profile.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import type { Session } from 'node:inspector'
2+
import { mkdirSync, writeFileSync } from 'node:fs'
3+
import process from 'node:process'
4+
import { box } from '@clack/prompts'
5+
import { colors } from 'consola/utils'
6+
import { join, relative } from 'pathe'
7+
import { themeColor } from './ascii'
8+
9+
let session: Session | undefined
10+
let profileCount = 0
11+
12+
export async function startCpuProfile(): Promise<void> {
13+
const cli = globalThis.__nuxt_cli__
14+
if (cli?.cpuProfileSession) {
15+
session = cli.cpuProfileSession
16+
delete cli.cpuProfileSession
17+
return
18+
}
19+
const inspector = await import('node:inspector')
20+
session = new inspector.Session()
21+
session.connect()
22+
try {
23+
await new Promise<void>((res, rej) => {
24+
session!.post('Profiler.enable', (err) => {
25+
if (err) {
26+
return rej(err)
27+
}
28+
session!.post('Profiler.start', (err) => {
29+
if (err) {
30+
return rej(err)
31+
}
32+
res()
33+
})
34+
})
35+
})
36+
}
37+
catch (err) {
38+
session.disconnect()
39+
session = undefined
40+
throw err
41+
}
42+
}
43+
44+
export async function stopCpuProfile(outDir: string, command: string): Promise<string | undefined> {
45+
if (!session) {
46+
return
47+
}
48+
const s = session
49+
session = undefined
50+
const count = profileCount++
51+
const outPath = join(outDir, `nuxt-${command}${count ? `-${count}` : ''}.cpuprofile`)
52+
const relativeOutPath = relative(process.cwd(), outPath).replace(/^(?![^.]{1,2}\/)/, './')
53+
try {
54+
await new Promise<any>((resolve, reject) => {
55+
s.post('Profiler.stop', (err, params) => {
56+
if (err) {
57+
return reject(err)
58+
}
59+
60+
if (!params?.profile) {
61+
return resolve(params)
62+
}
63+
64+
try {
65+
mkdirSync(outDir, { recursive: true })
66+
writeFileSync(outPath, JSON.stringify(params.profile))
67+
const nextSteps = [
68+
`CPU profile written to ${colors.cyan(relativeOutPath)}.`,
69+
`Open it in a CPU profile viewer like your IDE, or ${colors.cyan('https://discoveryjs.github.io/cpupro')}.`,
70+
]
71+
box(`\n${nextSteps.map(step => ` › ${step}`).join('\n')}\n`, '', {
72+
contentAlign: 'left',
73+
titleAlign: 'left',
74+
width: 'auto',
75+
titlePadding: 2,
76+
contentPadding: 2,
77+
rounded: true,
78+
withGuide: false,
79+
formatBorder: (text: string) => `${themeColor + text}\x1B[0m`,
80+
})
81+
}
82+
catch {}
83+
84+
resolve(params)
85+
})
86+
})
87+
}
88+
finally {
89+
s.disconnect()
90+
}
91+
}

0 commit comments

Comments
 (0)