diff --git a/benchmarks/goals.json b/benchmarks/goals.json index 8f7c0feb9c90b1..a58fdf0deb1baa 100644 --- a/benchmarks/goals.json +++ b/benchmarks/goals.json @@ -9,9 +9,9 @@ "max": 0.3 }, "copy-warm": { - "max": 1.0 + "max": 0.5 }, "build-warm": { - "max": 5.0 + "max": 0.75 } } diff --git a/benchmarks/package.json b/benchmarks/package.json index e64899ecf54c73..4f9cf24b9750b8 100644 --- a/benchmarks/package.json +++ b/benchmarks/package.json @@ -7,11 +7,11 @@ }, "scripts": { "run": "tsx run-benchmarks.ts", - "bench:version": "NX_NO_CLOUD=true hyperfine --show-output --setup 'pnpm exec nx reset' --warmup 1 --min-runs 10 --export-json results-version.json 'pnpm exec nx --version'", - "bench:show-projects": "NX_NO_CLOUD=true hyperfine --show-output --setup 'pnpm exec nx reset' --warmup 1 --min-runs 5 --export-json results-show-projects.json 'pnpm exec nx show projects --tui=false'", - "bench:cat-warm": "NX_NO_CLOUD=true hyperfine --show-output --setup 'pnpm exec nx reset' --warmup 1 --min-runs 5 --export-json results-cat-warm.json 'pnpm exec nx run-many -t cat --tui=false'", - "bench:copy-warm": "NX_NO_CLOUD=true hyperfine --show-output --setup 'pnpm exec nx reset' --warmup 1 --min-runs 5 --export-json results-copy-warm.json 'pnpm exec nx run-many -t copy --tui=false'", - "bench:build-warm": "NX_NO_CLOUD=true hyperfine --setup 'pnpm exec nx reset' --warmup 1 --min-runs 5 --export-json results-build-warm.json 'pnpm exec nx run-many -t build --tui=true --tui-auto-exit'" + "bench:version": "NX_NO_CLOUD=true hyperfine --show-output --setup 'node ../packages/nx/dist/bin/nx.js reset' --warmup 1 --min-runs 10 --export-json results-version.json 'node ../packages/nx/dist/bin/nx.js --version'", + "bench:show-projects": "NX_NO_CLOUD=true hyperfine --show-output --setup 'node ../packages/nx/dist/bin/nx.js reset' --warmup 1 --min-runs 5 --export-json results-show-projects.json 'node ../packages/nx/dist/bin/nx.js show projects --tui=false'", + "bench:cat-warm": "NX_NO_CLOUD=true hyperfine --show-output --setup 'node ../packages/nx/dist/bin/nx.js reset' --warmup 1 --min-runs 5 --export-json results-cat-warm.json 'node ../packages/nx/dist/bin/nx.js run-many -t cat --tui=false'", + "bench:copy-warm": "NX_NO_CLOUD=true hyperfine --show-output --setup 'node ../packages/nx/dist/bin/nx.js reset' --warmup 1 --min-runs 5 --export-json results-copy-warm.json 'node ../packages/nx/dist/bin/nx.js run-many -t copy --tui=false'", + "bench:build-warm": "NX_NO_CLOUD=true hyperfine --setup 'node ../packages/nx/dist/bin/nx.js reset' --warmup 1 --min-runs 5 --export-json results-build-warm.json 'node ../packages/nx/dist/bin/nx.js run-many -t build --tui=true --tui-auto-exit'" }, "nx": { "targets": { diff --git a/packages/nx/bin/nx.ts b/packages/nx/bin/nx.ts index 67059e9cc67007..a551a84df2c0f1 100644 --- a/packages/nx/bin/nx.ts +++ b/packages/nx/bin/nx.ts @@ -13,8 +13,6 @@ import { WorkspaceTypeAndRoot, } from '../src/utils/find-workspace-root'; import * as pc from 'picocolors'; -import { loadRootEnvFiles } from '../src/utils/dotenv'; -import { initLocal } from './init-local'; import { output } from '../src/utils/output'; import { getNxInstallationPath, @@ -26,13 +24,11 @@ import { execSync } from 'child_process'; import { createRequire } from 'module'; import { extname, join } from 'path'; import { existsSync } from 'fs'; -import { assertSupportedPlatform } from '../src/native/assert-supported-platform'; import { performance } from 'perf_hooks'; -import { setupWorkspaceContext } from '../src/utils/workspace-context'; -import { daemonClient } from '../src/daemon/client/client'; -import { removeDbConnections } from '../src/utils/db-connection'; -import { ensureAnalyticsPreferenceSet } from '../src/utils/analytics-prompt'; -import { flushAnalytics, startAnalytics } from '../src/analytics'; +// Register the performance observer as early as possible so any +// `performance.mark` / `measure` anywhere downstream is captured. The module +// is side-effect only and its heavy deps (analytics, daemon logger) are +// lazy-loaded inside the observer callback, so the import itself is cheap. import '../src/utils/perf-logging'; const isTsExt = extname(__filename).endsWith('.ts'); @@ -45,12 +41,18 @@ async function main() { process.argv[2] !== '--help' && process.argv[2] !== 'reset' ) { + const { assertSupportedPlatform } = await import( + '../src/native/assert-supported-platform.js' + ); assertSupportedPlatform(); } const workspace = findWorkspaceRoot(process.cwd()); - if (workspace) { + // --version doesn't need any env / daemon / analytics state — skip dotenv + // loading (and the heavy modules it would pull in). + if (workspace && process.argv[2] !== '--version') { + const { loadRootEnvFiles } = await import('../src/utils/dotenv.js'); performance.mark('loading dotenv files:start'); loadRootEnvFiles(workspace.dir); performance.mark('loading dotenv files:end'); @@ -70,7 +72,7 @@ async function main() { (process.argv[2] === 'graph' && !workspace) ) { process.env.NX_DAEMON = 'false'; - require('nx/src/command-line/nx-commands').commandsObject.argv; + (await import('nx/src/command-line/nx-commands')).commandsObject.argv; } else { // polyfill rxjs observable to avoid issues with multiple version of Observable installed in node_modules // https://twitter.com/BenLesh/status/1192478226385428483?s=20 @@ -107,18 +109,27 @@ async function main() { // this file is already in the local workspace if (isNxCloudCommand(process.argv[2])) { + const { daemonClient } = await import('../src/daemon/client/client.js'); if (!daemonClient.enabled() && workspace !== null) { + const { setupWorkspaceContext } = await import( + '../src/utils/workspace-context.js' + ); setupWorkspaceContext(workspace.dir); } await initAnalytics(); // nx-cloud commands can run without local Nx installation process.env.NX_DAEMON = 'false'; - require('nx/src/command-line/nx-commands').commandsObject.argv; + (await import('nx/src/command-line/nx-commands')).commandsObject.argv; } else if (isLocalInstall) { + const { daemonClient } = await import('../src/daemon/client/client.js'); if (!daemonClient.enabled() && workspace !== null) { + const { setupWorkspaceContext } = await import( + '../src/utils/workspace-context.js' + ); setupWorkspaceContext(workspace.dir); } await initAnalytics(); + const { initLocal } = await import('./init-local.js'); await initLocal(workspace); } else if (localNx) { // Nx is being run from globally installed CLI - hand off to the local @@ -127,9 +138,9 @@ async function main() { warnIfUsingOutdatedGlobalInstall(GLOBAL_NX_VERSION, LOCAL_NX_VERSION); if (localNx.includes('.nx')) { const nxWrapperPath = localNx.replace(/\.nx.*/, '.nx/') + 'nxw.js'; - require(nxWrapperPath); + await import(nxWrapperPath); } else { - require(localNx); + await import(localNx); } } } @@ -220,11 +231,17 @@ function isNxCloudCommand(command: string): boolean { return nxCloudCommands.includes(command); } +let analyticsStarted = false; async function initAnalytics() { + const { ensureAnalyticsPreferenceSet } = await import( + '../src/utils/analytics-prompt.js' + ); + const { startAnalytics } = await import('../src/analytics/index.js'); try { await ensureAnalyticsPreferenceSet(); } catch {} await startAnalytics(); + analyticsStarted = true; } function handleMissingLocalInstallation(detectedWorkspaceRoot: string | null) { @@ -339,12 +356,13 @@ const getLatestVersionOfNx = ((fn: () => string) => { return () => cache || (cache = fn()); })(_getLatestVersionOfNx); -process.on('exit', () => { - removeDbConnections(); -}); - -main().catch((error) => { +main().catch(async (error) => { console.error(error); - flushAnalytics(); + if (analyticsStarted) { + // analyticsStarted implies '../src/analytics' is already in the module + // cache, so this resolves from cache without any disk work. + const { flushAnalytics } = await import('../src/analytics/index.js'); + flushAnalytics(); + } process.exit(1); }); diff --git a/packages/nx/src/daemon/server/shutdown-utils.ts b/packages/nx/src/daemon/server/shutdown-utils.ts index ff25a0cbe89219..e8d1691d95fbf2 100644 --- a/packages/nx/src/daemon/server/shutdown-utils.ts +++ b/packages/nx/src/daemon/server/shutdown-utils.ts @@ -8,7 +8,6 @@ import { DaemonProjectGraphError, ProjectGraphError, } from '../../project-graph/error-types'; -import { removeDbConnections } from '../../utils/db-connection'; import { cleanupPlugins } from '../../project-graph/plugins/get-plugins'; import { MESSAGE_END_SEQ } from '../../utils/consume-messages-from-socket'; import { cleanupLatestNx } from './latest-nx'; @@ -144,8 +143,6 @@ async function performShutdown( deleteDaemonJsonProcessCache(); cleanupPlugins(); - removeDbConnections(); - // Clean up shared latest Nx installation cleanupLatestNx(); diff --git a/packages/nx/src/utils/db-connection.ts b/packages/nx/src/utils/db-connection.ts index baceb729a69e66..e389e4034dde62 100644 --- a/packages/nx/src/utils/db-connection.ts +++ b/packages/nx/src/utils/db-connection.ts @@ -63,13 +63,15 @@ export function getLocalDbConnection( return connection; } -export function removeDbConnections() { +function removeDbConnections() { for (const connection of dbConnectionMap.values()) { closeDbConnection(connection); } dbConnectionMap.clear(); } +process.on('exit', removeDbConnections); + function getEntryOrSet( map: Map, key: TKey, diff --git a/packages/nx/src/utils/perf-logging.ts b/packages/nx/src/utils/perf-logging.ts index 90924843682e0b..34c48f48bafee4 100644 --- a/packages/nx/src/utils/perf-logging.ts +++ b/packages/nx/src/utils/perf-logging.ts @@ -1,10 +1,6 @@ import { PerformanceObserver } from 'perf_hooks'; -import { customDimensions, reportEvent } from '../analytics'; -import type { EventParameters } from '../analytics'; import type { TrackedDetail } from './perf-hooks'; -import { isOnDaemon } from '../daemon/is-on-daemon'; -import { serverLogger } from '../daemon/logger'; function isTrackedDetail(detail: unknown): detail is TrackedDetail { return ( @@ -14,41 +10,43 @@ function isTrackedDetail(detail: unknown): detail is TrackedDetail { ); } -const dimensionValues = customDimensions - ? new Set(Object.values(customDimensions)) - : null; - -let initialized = false; - -if (!initialized) { - initialized = true; - - const obs = new PerformanceObserver((list) => { - for (const entry of list.getEntries()) { - const { detail } = entry; - - if (process.env.NX_PERF_LOGGING === 'true') { - const message = `Time taken for '${entry.name}' ${entry.duration}ms`; - if (isOnDaemon()) { - serverLogger.log(message); - } else { - console.log(message); - } - } - - if (isTrackedDetail(detail) && dimensionValues) { - const { track, ...rest } = detail; - const eventParameters: EventParameters = { - [customDimensions.duration]: entry.duration, - }; - for (const [key, value] of Object.entries(rest)) { - if (dimensionValues.has(key)) { - eventParameters[key] = value; - } - } - reportEvent(entry.name, eventParameters); - } +new PerformanceObserver((list) => { + const entries = list.getEntries(); + const logEnabled = process.env.NX_PERF_LOGGING === 'true'; + const tracked = entries.filter((e) => isTrackedDetail(e.detail)); + + // Short-circuit before loading analytics / daemon logger (~60ms of native + // binding + module init) when there's nothing to do. + if (!logEnabled && tracked.length === 0) return; + + if (logEnabled) { + const { isOnDaemon } = + require('../daemon/is-on-daemon') as typeof import('../daemon/is-on-daemon'); + const { serverLogger } = + require('../daemon/logger') as typeof import('../daemon/logger'); + const log = isOnDaemon() + ? (msg: string) => serverLogger.log(msg) + : console.log; + for (const entry of entries) { + log(`Time taken for '${entry.name}' ${entry.duration}ms`); } - }); - obs.observe({ entryTypes: ['measure'] }); -} + } + + if (tracked.length === 0) return; + + const { customDimensions, reportEvent } = + require('../analytics') as typeof import('../analytics'); + if (!customDimensions) return; + const dimensionValues = new Set(Object.values(customDimensions)); + + for (const entry of tracked) { + const { track, ...rest } = entry.detail as TrackedDetail; + const params: import('../analytics').EventParameters = { + [customDimensions.duration]: entry.duration, + }; + for (const [key, value] of Object.entries(rest)) { + if (dimensionValues.has(key)) params[key] = value; + } + reportEvent(entry.name, params); + } +}).observe({ entryTypes: ['measure'] });