diff --git a/packages/nuxi/bin/nuxi.mjs b/packages/nuxi/bin/nuxi.mjs index 63504b583..49df74bd9 100755 --- a/packages/nuxi/bin/nuxi.mjs +++ b/packages/nuxi/bin/nuxi.mjs @@ -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' @@ -23,6 +24,22 @@ globalThis.__nuxt_cli__ = { startTime: Date.now(), entry: fileURLToPath(import.meta.url), devEntry: fileURLToPath(new URL('../dist/dev/index.mjs', import.meta.url)), + cpuProfileSession: undefined, +} + +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 diff --git a/packages/nuxi/src/commands/_shared.ts b/packages/nuxi/src/commands/_shared.ts index 7d10a1c0d..8606274aa 100644 --- a/packages/nuxi/src/commands/_shared.ts +++ b/packages/nuxi/src/commands/_shared.ts @@ -40,6 +40,15 @@ export const extendsArgs = { }, } as const satisfies Record +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 + export const legacyRootDirArgs = { // cwd falls back to rootDir's default (indirect default) cwd: { diff --git a/packages/nuxi/src/commands/build.ts b/packages/nuxi/src/commands/build.ts index fb9b6ec61..3ad1b172b 100644 --- a/packages/nuxi/src/commands/build.ts +++ b/packages/nuxi/src/commands/build.ts @@ -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 { startCpuProfile, stopCpuProfile } from '../utils/profile' +import { cwdArgs, dotEnvArgs, envNameArgs, extendsArgs, legacyRootDirArgs, logLevelArgs, profileArgs } from './_shared' export default defineCommand({ meta: { @@ -31,6 +32,7 @@ export default defineCommand({ ...dotEnvArgs, ...envNameArgs, ...extendsArgs, + ...profileArgs, ...legacyRootDirArgs, }, async run(ctx) { @@ -38,67 +40,90 @@ export default defineCommand({ const cwd = resolve(ctx.args.cwd || ctx.args.rootDir) - intro(colors.cyan('Building Nuxt for production...')) + const profileArg = ctx.args.profile + const perfValue = profileArg === 'verbose' ? true : profileArg ? 'quiet' : undefined + if (profileArg) { + await startCpuProfile() + } + + try { + intro(colors.cyan('Building Nuxt for production...')) - const kit = await loadKit(cwd) + const kit = await loadKit(cwd) - await showVersions(cwd, kit, ctx.args.dotenv) - const nuxt = await kit.loadNuxt({ - cwd, - dotenv: { + await showVersions(cwd, kit, ctx.args.dotenv) + + const nuxt = await kit.loadNuxt({ cwd, - fileName: ctx.args.dotenv, - }, - envName: ctx.args.envName, // c12 will fall back to NODE_ENV - overrides: { - logLevel: ctx.args.logLevel as 'silent' | 'info' | 'verbose', - // TODO: remove in 3.8 - _generate: ctx.args.prerender, - nitro: { - static: ctx.args.prerender, - preset: ctx.args.preset || process.env.NITRO_PRESET || process.env.SERVER_PRESET, + dotenv: { + cwd, + fileName: ctx.args.dotenv, + }, + envName: ctx.args.envName, // c12 will fall back to NODE_ENV + overrides: { + logLevel: ctx.args.logLevel as 'silent' | 'info' | 'verbose', + // TODO: remove in 3.8 + _generate: ctx.args.prerender, + nitro: { + static: ctx.args.prerender, + preset: ctx.args.preset || process.env.NITRO_PRESET || process.env.SERVER_PRESET, + }, + ...(ctx.args.extends && { extends: ctx.args.extends }), + ...ctx.data?.overrides, + ...((perfValue || ctx.data?.overrides?.debug) && { + debug: { + ...ctx.data?.overrides?.debug, + ...(perfValue && { perf: perfValue }), + }, + }), }, - ...(ctx.args.extends && { extends: ctx.args.extends }), - ...ctx.data?.overrides, - }, - }) + }) - let nitro: ReturnType | undefined - // In Bridge, if Nitro is not enabled, useNitro will throw an error - try { - // Use ? for backward compatibility for Nuxt <= RC.10 - nitro = kit.useNitro?.() - if (nitro) { - logger.info(`Nitro preset: ${colors.cyan(nitro.options.preset)}`) + let nitro: ReturnType | undefined + // In Bridge, if Nitro is not enabled, useNitro will throw an error + try { + // Use ? for backward compatibility for Nuxt <= RC.10 + nitro = kit.useNitro?.() + if (nitro) { + logger.info(`Nitro preset: ${colors.cyan(nitro.options.preset)}`) + } + } + catch { + // } - } - catch { - // - } - await clearBuildDir(nuxt.options.buildDir) + await clearBuildDir(nuxt.options.buildDir) - await kit.writeTypes(nuxt) + await kit.writeTypes(nuxt) - nuxt.hook('build:error', (err) => { - logger.error(`Nuxt build error: ${err}`) - process.exit(1) - }) + nuxt.hook('build:error', (err) => { + logger.error(`Nuxt build error: ${err}`) + if (profileArg) { + stopCpuProfile(cwd) + } + process.exit(1) + }) - await kit.buildNuxt(nuxt) + await kit.buildNuxt(nuxt) - if (ctx.args.prerender) { - if (!nuxt.options.ssr) { - logger.warn(`HTML content not prerendered because ${colors.cyan('ssr: false')} was set.`) - logger.info(`You can read more in ${colors.cyan('https://nuxt.com/docs/getting-started/deployment#static-hosting')}.`) + if (ctx.args.prerender) { + if (!nuxt.options.ssr) { + logger.warn(`HTML content not prerendered because ${colors.cyan('ssr: false')} was set.`) + logger.info(`You can read more in ${colors.cyan('https://nuxt.com/docs/getting-started/deployment#static-hosting')}.`) + } + // TODO: revisit later if/when nuxt build --prerender will output hybrid + const dir = nitro?.options.output.publicDir + const publicDir = dir ? relative(process.cwd(), dir) : '.output/public' + outro(`✨ You can now deploy ${colors.cyan(publicDir)} to any static hosting!`) + } + else { + outro('✨ Build complete!') } - // TODO: revisit later if/when nuxt build --prerender will output hybrid - const dir = nitro?.options.output.publicDir - const publicDir = dir ? relative(process.cwd(), dir) : '.output/public' - outro(`✨ You can now deploy ${colors.cyan(publicDir)} to any static hosting!`) } - else { - outro('✨ Build complete!') + finally { + if (profileArg) { + await stopCpuProfile(cwd) + } } }, }) diff --git a/packages/nuxi/src/commands/dev.ts b/packages/nuxi/src/commands/dev.ts index 9101f7da9..c8fe5db54 100644 --- a/packages/nuxi/src/commands/dev.ts +++ b/packages/nuxi/src/commands/dev.ts @@ -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()) @@ -62,6 +62,7 @@ const command = defineCommand({ }, clipboard: { ...listhenArgs.clipboard, default: false }, }, + ...profileArgs, sslCert: { type: 'string', description: '(DEPRECATED) Use `--https.cert` instead.', diff --git a/packages/nuxi/src/commands/generate.ts b/packages/nuxi/src/commands/generate.ts index 6e2b9015f..2ee64d2c4 100644 --- a/packages/nuxi/src/commands/generate.ts +++ b/packages/nuxi/src/commands/generate.ts @@ -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({ @@ -18,6 +18,7 @@ export default defineCommand({ ...dotEnvArgs, ...envNameArgs, ...extendsArgs, + ...profileArgs, ...legacyRootDirArgs, }, async run(ctx) { diff --git a/packages/nuxi/src/dev/index.ts b/packages/nuxi/src/dev/index.ts index 8a8f2f748..dbeb02279 100644 --- a/packages/nuxi/src/dev/index.ts +++ b/packages/nuxi/src/dev/index.ts @@ -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 { startCpuProfile, stopCpuProfile } from '../utils/profile.ts' import { NuxtDevServer } from './utils' const start = Date.now() @@ -55,11 +56,22 @@ interface InitializeReturn { export async function initialize(devContext: NuxtDevContext, ctx: InitializeOptions = {}): Promise { 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() + } + 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, @@ -107,6 +119,10 @@ export async function initialize(devContext: NuxtDevContext, ctx: InitializeOpti console.debug(`Dev server (internal) initialized in ${Date.now() - start}ms`) } + if (profileArg) { + process.once('exit', () => stopCpuProfile(devContext.cwd)) + } + return { listener: devServer.listener, close: async () => { diff --git a/packages/nuxi/src/dev/utils.ts b/packages/nuxi/src/dev/utils.ts index 6c262eff9..9ad83e93f 100644 --- a/packages/nuxi/src/dev/utils.ts +++ b/packages/nuxi/src/dev/utils.ts @@ -48,6 +48,7 @@ export interface NuxtDevContext { dotenv?: string envName?: string extends?: string + profile?: string | boolean } } diff --git a/packages/nuxi/src/utils/profile.ts b/packages/nuxi/src/utils/profile.ts new file mode 100644 index 000000000..d73159fe6 --- /dev/null +++ b/packages/nuxi/src/utils/profile.ts @@ -0,0 +1,75 @@ +import type { Session } from 'node:inspector' +import { mkdirSync, writeFileSync } from 'node:fs' +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 { + const cli = globalThis.__nuxt_cli__ + if (cli?.cpuProfileSession) { + session = cli.cpuProfileSession + delete cli.cpuProfileSession + return + } + const inspector = await import('node:inspector') + session = new inspector.Session() + session.connect() + try { + await new Promise((res, rej) => { + session!.post('Profiler.enable', (err) => { + if (err) { + return rej(err) + } + session!.post('Profiler.start', (err) => { + if (err) { + return rej(err) + } + res() + }) + }) + }) + } + catch (err) { + session.disconnect() + session = undefined + throw err + } +} + +export async function stopCpuProfile(outDir: string): Promise { + if (!session) { + return + } + const s = session + session = undefined + const outPath = join(outDir, `profile-${profileCount++}.cpuprofile`) + try { + await new Promise((resolve, reject) => { + s.post('Profiler.stop', (err, params) => { + if (err) { + return reject(err) + } + + if (!params?.profile) { + return resolve(params) + } + + 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 {} + + resolve(params) + }) + }) + } + finally { + s.disconnect() + } +} diff --git a/packages/nuxt-cli/bin/nuxi.mjs b/packages/nuxt-cli/bin/nuxi.mjs index 63504b583..7721e99cd 100755 --- a/packages/nuxt-cli/bin/nuxi.mjs +++ b/packages/nuxt-cli/bin/nuxi.mjs @@ -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' @@ -25,6 +26,36 @@ 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() + + try { + // eslint-disable-next-line antfu/no-top-level-await + await new Promise((resolve, reject) => { + session.post('Profiler.enable', (err) => { + if (err) { + return reject(err) + } + session.post('Profiler.start', (err) => { + if (err) { + return reject(err) + } + resolve() + }) + }) + }) + globalThis.__nuxt_cli__.cpuProfileSession = session + } + catch (err) { + session.disconnect() + throw err + } +} + // eslint-disable-next-line antfu/no-top-level-await const { runMain } = await import('../dist/index.mjs') diff --git a/types.d.ts b/types.d.ts index 2893c86a3..b88e543e1 100644 --- a/types.d.ts +++ b/types.d.ts @@ -1,3 +1,5 @@ +import type { Session } from 'node:inspector' + declare global { // eslint-disable-next-line vars-on-top var __nuxt_cli__: @@ -6,6 +8,7 @@ declare global { entry: string devEntry?: string startTime: number + cpuProfileSession?: Session } }