Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/nuxi/bin/nuxi.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env node

import inspector from 'node:inspector'
import nodeModule from 'node:module'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
Expand All @@ -25,6 +26,21 @@ globalThis.__nuxt_cli__ = {
devEntry: fileURLToPath(new URL('../dist/dev/index.mjs', import.meta.url)),
}

if (
process.argv.includes('--profile')
|| process.argv.some(a => a.startsWith('--profile='))
) {
const session = new inspector.Session()
session.connect()
// eslint-disable-next-line antfu/no-top-level-await
await new Promise((resolve) => {
session.post('Profiler.enable', () => {
session.post('Profiler.start', resolve)
})
})
globalThis.__nuxt_cli__.cpuProfileSession = session
}

// eslint-disable-next-line antfu/no-top-level-await
const { runMain } = await import('../dist/index.mjs')

Expand Down
9 changes: 9 additions & 0 deletions packages/nuxi/src/commands/_shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ export const extendsArgs = {
},
} as const satisfies Record<string, ArgDef>

export const profileArgs = {
profile: {
type: 'string',
description: 'Profile performance. Use --profile for CPU only, --profile=verbose for full report.',
default: undefined as string | undefined,
valueHint: 'verbose',
},
} as const satisfies Record<string, ArgDef>

export const legacyRootDirArgs = {
// cwd falls back to rootDir's default (indirect default)
cwd: {
Expand Down
22 changes: 21 additions & 1 deletion packages/nuxi/src/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { overrideEnv } from '../utils/env'
import { clearBuildDir } from '../utils/fs'
import { loadKit } from '../utils/kit'
import { logger } from '../utils/logger'
import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared'
import { installSignalHandlers, startCpuProfile, stopCpuProfile } from '../utils/profile'
import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs, profileArgs } from './_shared'

export default defineCommand({
meta: {
Expand All @@ -31,18 +32,27 @@ export default defineCommand({
...dotEnvArgs,
...envNameArgs,
...extendsArgs,
...profileArgs,
...legacyRootDirArgs,
},
async run(ctx) {
overrideEnv('production')

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

const profileArg = ctx.args.profile
const perfValue = profileArg === 'verbose' ? true : profileArg ? 'quiet' : undefined
if (profileArg) {
await startCpuProfile()
installSignalHandlers(cwd)
}

intro(colors.cyan('Building Nuxt for production...'))

const kit = await loadKit(cwd)

await showVersions(cwd, kit, ctx.args.dotenv)

const nuxt = await kit.loadNuxt({
cwd,
dotenv: {
Expand All @@ -59,6 +69,12 @@ export default defineCommand({
preset: ctx.args.preset || process.env.NITRO_PRESET || process.env.SERVER_PRESET,
},
...(ctx.args.extends && { extends: ctx.args.extends }),
...((perfValue || ctx.data?.overrides?.debug) && {
debug: {
...ctx.data?.overrides?.debug,
...(perfValue && { perf: perfValue }),
},
}),
...ctx.data?.overrides,
},
})
Expand Down Expand Up @@ -87,6 +103,10 @@ export default defineCommand({

await kit.buildNuxt(nuxt)

if (profileArg) {
await stopCpuProfile(cwd).catch(() => {})
}

if (ctx.args.prerender) {
if (!nuxt.options.ssr) {
logger.warn(`HTML content not prerendered because ${colors.cyan('ssr: false')} was set.`)
Expand Down
3 changes: 2 additions & 1 deletion packages/nuxi/src/commands/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { isBun, isTest } from 'std-env'
import { initialize } from '../dev'
import { ForkPool } from '../dev/pool'
import { debug, logger } from '../utils/logger'
import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared'
import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs, profileArgs } from './_shared'

const startTime: number | undefined = Date.now()
const forkSupported = !isTest && (!isBun || isBunForkSupported())
Expand Down Expand Up @@ -62,6 +62,7 @@ const command = defineCommand({
},
clipboard: { ...listhenArgs.clipboard, default: false },
},
...profileArgs,
sslCert: {
type: 'string',
description: '(DEPRECATED) Use `--https.cert` instead.',
Expand Down
3 changes: 2 additions & 1 deletion packages/nuxi/src/commands/generate.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { defineCommand } from 'citty'

import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs } from './_shared'
import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs, profileArgs } from './_shared'
import buildCommand from './build'

export default defineCommand({
Expand All @@ -18,6 +18,7 @@ export default defineCommand({
...dotEnvArgs,
...envNameArgs,
...extendsArgs,
...profileArgs,
...legacyRootDirArgs,
},
async run(ctx) {
Expand Down
16 changes: 16 additions & 0 deletions packages/nuxi/src/dev/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { NuxtDevContext, NuxtDevIPCMessage, NuxtParentIPCMessage } from './
import process from 'node:process'
import defu from 'defu'
import { overrideEnv } from '../utils/env.ts'
import { installSignalHandlers, startCpuProfile, stopCpuProfile } from '../utils/profile.ts'
import { NuxtDevServer } from './utils'

const start = Date.now()
Expand Down Expand Up @@ -55,11 +56,23 @@ interface InitializeReturn {
export async function initialize(devContext: NuxtDevContext, ctx: InitializeOptions = {}): Promise<InitializeReturn> {
overrideEnv('development')

const profileArg = devContext.args.profile
const perfValue = profileArg === 'verbose' ? true : profileArg ? 'quiet' : undefined
const perfOverrides = perfValue
? { debug: { perf: perfValue } } as NuxtConfig
: {}

if (profileArg) {
await startCpuProfile()
installSignalHandlers(devContext.cwd)
}

const devServer = new NuxtDevServer({
cwd: devContext.cwd,
overrides: defu(
ctx.data?.overrides,
({ extends: devContext.args.extends } satisfies NuxtConfig) as NuxtConfig,
perfOverrides,
),
logLevel: devContext.args.logLevel as 'silent' | 'info' | 'verbose',
clear: devContext.args.clear,
Expand Down Expand Up @@ -115,6 +128,9 @@ export async function initialize(devContext: NuxtDevContext, ctx: InitializeOpti
devServer.listener.close(),
devServer.close(),
])
if (profileArg) {
await stopCpuProfile(devContext.cwd).catch(() => {})
}
},
onReady: (callback: (address: string) => void) => {
if (address) {
Expand Down
1 change: 1 addition & 0 deletions packages/nuxi/src/dev/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface NuxtDevContext {
dotenv?: string
envName?: string
extends?: string
profile?: string | boolean
}
}

Expand Down
99 changes: 99 additions & 0 deletions packages/nuxi/src/utils/profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import type { Session } from 'node:inspector'
import { mkdirSync, writeFileSync } from 'node:fs'
import { mkdir, writeFile } from 'node:fs/promises'
import process from 'node:process'
import { colors } from 'consola/utils'
import { join } from 'pathe'
import { logger } from './logger'

let session: Session | undefined
let profileCount = 0

export async function startCpuProfile(): Promise<void> {
// Adopt session started in bin/nuxi.mjs
const cli = globalThis.__nuxt_cli__ as typeof globalThis.__nuxt_cli__ & { cpuProfileSession?: import('node:inspector').Session }
if (cli?.cpuProfileSession) {
session = cli.cpuProfileSession
delete cli.cpuProfileSession
return
}
const inspector = await import('node:inspector')
session = new inspector.Session()
session.connect()
await new Promise<void>((res, rej) => {
session!.post('Profiler.enable', () => {
session!.post('Profiler.start', (err) => {
if (err) {
rej(err)
}
else { res() }
})
})
})
}

export function stopCpuProfile(outDir: string): Promise<string | undefined> {
if (!session) {
return Promise.resolve(undefined)
}
const s = session
session = undefined
return new Promise((res, rej) => {
s.post('Profiler.stop', (err, { profile }) => {
if (err) {
return rej(err)
}
const outPath = join(outDir, `profile-${profileCount++}.cpuprofile`)
mkdir(outDir, { recursive: true })
.then(() => writeFile(outPath, JSON.stringify(profile)))
.then(() => {
logger.info(`CPU profile written to ${colors.cyan(outPath)}`)
logger.info(`Open it in ${colors.cyan('https://www.speedscope.app')} or Chrome DevTools`)
s.disconnect()
res(outPath)
})
.catch(rej)
})
})
}

function stopCpuProfileSync(outDir: string): string | undefined {
if (!session) {
return
}
const s = session
session = undefined
let outPath: string | undefined
s.post('Profiler.stop', (_err, params) => {
if (_err || !params?.profile) {
return
}
outPath = join(outDir, `profile-${profileCount++}.cpuprofile`)
try {
mkdirSync(outDir, { recursive: true })
writeFileSync(outPath, JSON.stringify(params.profile))
logger.info(`CPU profile written to ${colors.cyan(outPath)}`)
logger.info(`Open it in ${colors.cyan('https://www.speedscope.app')} or Chrome DevTools`)
}
catch {}
s.disconnect()
})
return outPath
}

/**
* Install signal handlers that flush the CPU profile before exit.
* Returns a cleanup function to remove the handlers.
*/
export function installSignalHandlers(outDir: string): () => void {
const onSignal = (signal: NodeJS.Signals) => {
stopCpuProfileSync(outDir)
process.kill(process.pid, signal)
}
process.once('SIGINT', onSignal)
process.once('SIGTERM', onSignal)
return () => {
process.off('SIGINT', onSignal)
process.off('SIGTERM', onSignal)
}
}
16 changes: 16 additions & 0 deletions packages/nuxt-cli/bin/nuxi.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env node

import inspector from 'node:inspector'
import nodeModule from 'node:module'
import process from 'node:process'
import { fileURLToPath } from 'node:url'
Expand All @@ -25,6 +26,21 @@ globalThis.__nuxt_cli__ = {
devEntry: fileURLToPath(new URL('../dist/dev/index.mjs', import.meta.url)),
}

if (
process.argv.includes('--profile')
|| process.argv.some(a => a.startsWith('--profile='))
) {
const session = new inspector.Session()
session.connect()
// eslint-disable-next-line antfu/no-top-level-await
await new Promise((resolve) => {
session.post('Profiler.enable', () => {
session.post('Profiler.start', resolve)
})
})
globalThis.__nuxt_cli__.cpuProfileSession = session
}

// eslint-disable-next-line antfu/no-top-level-await
const { runMain } = await import('../dist/index.mjs')

Expand Down
1 change: 1 addition & 0 deletions types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ declare global {
entry: string
devEntry?: string
startTime: number
cpuProfileSession?: import('node:inspector').Session
}
}

Expand Down
Loading