Skip to content

chore(testing): combine daemon+pre-install, cut linter.test.ts wall time 42%#35354

Draft
FrozenPandaz wants to merge 9 commits intonrwl:masterfrom
FrozenPandaz:speed-e2e-combined
Draft

chore(testing): combine daemon+pre-install, cut linter.test.ts wall time 42%#35354
FrozenPandaz wants to merge 9 commits intonrwl:masterfrom
FrozenPandaz:speed-e2e-combined

Conversation

@FrozenPandaz
Copy link
Copy Markdown
Contributor

Current Behavior

The nx run e2e-eslint:e2e-ci--src/linter.test.ts target takes ~678s on CI. A handful of other e2e files have the same shape and pay similar wall-time.

We opened two PRs targeting this on different levers:

Each PR on its own is a regression on linter.test.ts:

  • 35303 alone: every runCLI pays +2 plugin cold-loads (the new pre-installed packages are Nx plugin packages like @nx/vite, @nx/vitest, @nx/jest that get auto-discovered). With daemon off, plugin-worker boot is paid on every invocation. Net: +61s (678 → 739).
  • 35218 alone: daemon caches plugin workers across commands, but each workspace has to amortize the initial daemon startup cost. The tests create 4 workspaces × few generators each — insufficient amortization, especially in the Root projects migration block (2 generators per workspace).

The two levers are complementary: 35218 kills the overhead that 35303 introduces, 35303 gives 35218 bigger wins to amortize.

Expected Behavior

Stacking the two PRs drops linter.test.ts wall time from 678s → 393s (−42%) on the same test architecture:

Test Baseline Combined Δ
should check for linting errors 14.3s 9.8s −4.5s
should cache eslint with --cache 12.7s 7.4s −5.3s
linting output file format 7.8s 4.8s −3.0s
workspace lint rules 28.9s 20.1s −8.8s
lint plugin module boundaries 78.1s 22.2s −55.9s
fix noSelfCircularDependencies 11.0s 7.5s −3.5s
fix noRelativeOrAbsoluteImports 9.4s 6.3s −3.1s
report dependency check issues 30.1s 18.7s −11.4s
flat config generate projects 33.0s 15.3s −17.7s
React standalone migration 100.0s 67.3s −32.7s
Angular standalone migration 97.3s 65.5s −31.8s
Node standalone migration 65.5s 52.7s −12.8s
Total wall time 678s 392.7s −285s (−42%)

Biggest win is lint plugin module boundaries (−56s): that test has 4 sequential generators + many lint commands — exactly the pattern where skipped tmp installs + warm daemon stack up.

What this PR contains

This branch sits on top of both PRs and adds one more commit:

  • Merges the changes from chore(testing): pre-install ensurePackage deps in e2e newProject() pa… #35303 and fix(testing): enable Nx daemon for e2e tests #35218 so they can be measured together.
  • Adds per-phase perf-logging instrumentation to nx generate so future e2e perf investigations can see where time actually goes without having to patch node_modules:
    • generate:project-graph, generate:run-generator, generate:flush-changes, generate:post-task spans in packages/nx/src/command-line/generate/generate.ts
    • install-packages-task:<packageManager> in packages/devkit/src/tasks/install-packages-task.ts
    • format-files in packages/devkit/src/generators/format-files.ts
    • process.on('exit') drain in packages/nx/src/utils/perf-logging.ts so measures emitted just before process.exit() actually reach the observer
    • NX_PERF_LOGGING added to the e2e stripped-env allowlist so subprocess generators inherit it

Run with NX_PERF_LOGGING=true pnpm nx run e2e-eslint:e2e-ci--src/linter.test.ts to see per-generator timing breakdowns.

Related Issue(s)

Draft — depends on #35303 and #35218 landing first (or this PR can be retargeted once one of them merges).

@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 21, 2026

👷 Deploy request for nx-docs pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit 9bb6c25

@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 21, 2026

👷 Deploy request for nx-dev pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit 9bb6c25

FrozenPandaz and others added 8 commits April 21, 2026 12:32
…ckages

move packages that were lazily installed via ensurePackage() during
generator runs into the packages array passed to newProject(), so they
are pre-installed during workspace setup instead of mid-test. reduces
per-test install overhead across 97 e2e test files.
- Add install timing logs to installPackagesTask
- Improve error logging in e2e utils (PATH, env info on ENOENT)
- Fix stripped env to include PATH, HOME, USER, SHELL
- Use create-nx-workspace@latest instead of pinned version
- Add prep-e2e script to package.json
Reverts debug logging, env stripping, shell option, and
create-nx-workspace version changes that broke e2e type checks.
The daemon was being stripped from the environment by
getStrippedEnvironmentVariables(), causing every nx command in e2e
tests to rebuild the project graph from scratch. This added ~20s
per generator call. Hardcode NX_DAEMON=true so the daemon stays
alive between commands and caches the project graph.
Adds performance.measure() spans so NX_PERF_LOGGING=true reveals where a generator
spends its time:

- generate:project-graph, generate:run-generator, generate:flush-changes,
  generate:post-task — added to generate() in packages/nx
- install-packages-task:<pm> — wraps the pnpm/npm install exec
- format-files — wraps the prettier loop

Also drains pending PerformanceObserver records on process.exit() so measures
emitted near the tail of a generator (which normally die when process.exit skips
pending microtasks) reach the log.

Allows NX_PERF_LOGGING through the e2e stripped-env allowlist so subprocess
generators inherit it.
@nx-cloud
Copy link
Copy Markdown
Contributor

nx-cloud Bot commented Apr 21, 2026

View your CI Pipeline Execution ↗ for commit 9bb6c25

Command Status Duration Result
nx affected --targets=lint,test,build,e2e,e2e-c... ⛔ Cancelled 1h 35m 20s View ↗
nx affected -t e2e-macos-local --parallel=1 --b... ❌ Failed 43m 2s View ↗
nx run-many -t check-imports check-lock-files c... ✅ Succeeded 3s View ↗
nx-cloud record -- pnpm nx-cloud conformance:check ✅ Succeeded 16s View ↗
nx build workspace-plugin ✅ Succeeded 4m 22s View ↗
nx-cloud record -- nx sync:check ✅ Succeeded 22s View ↗
nx-cloud record -- nx format:check ✅ Succeeded 20s View ↗

☁️ Nx Cloud last updated this comment at 2026-04-22 06:14:37 UTC

Copy link
Copy Markdown
Contributor

@nx-cloud nx-cloud Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

At least one additional CI pipeline execution has run since the conclusion below was written and it may no longer be applicable.

Nx Cloud is proposing a fix for your failed CI:

We fix a race condition in DaemonClient.startInBackground() where a concurrent reset() call (triggered by the socket-close handler) could null out this._out between the two await open() calls, causing TypeError: Cannot read properties of null (reading 'fd') when spawn() was invoked. By capturing the FileHandle return values into local variables (out/err) and passing those to spawn() instead of this._out/this._err, the daemon restart is no longer affected by any concurrent reset() execution.

Warning

We could not verify this fix.

diff --git a/packages/nx/src/daemon/client/client.ts b/packages/nx/src/daemon/client/client.ts
index c915ee7a69..e4a0fd9f3c 100644
--- a/packages/nx/src/daemon/client/client.ts
+++ b/packages/nx/src/daemon/client/client.ts
@@ -1302,8 +1302,11 @@ export class DaemonClient {
       writeFileSync(DAEMON_OUTPUT_LOG_FILE, '');
     }
 
-    this._out = await open(DAEMON_OUTPUT_LOG_FILE, 'a');
-    this._err = await open(DAEMON_OUTPUT_LOG_FILE, 'a');
+    // Capture file handles in local variables to avoid a race condition where
+    // reset() may null out this._out/this._err between the async open() calls
+    // and the spawn() call, causing "Cannot read properties of null (reading 'fd')".
+    const out = (this._out = await open(DAEMON_OUTPUT_LOG_FILE, 'a'));
+    const err = (this._err = await open(DAEMON_OUTPUT_LOG_FILE, 'a'));
 
     clientLogger.log(`[Client] Starting new daemon server in background`);
 
@@ -1312,7 +1315,7 @@ export class DaemonClient {
       [join(__dirname, `../server/start.js`)],
       {
         cwd: workspaceRoot,
-        stdio: ['ignore', this._out.fd, this._err.fd],
+        stdio: ['ignore', out.fd, err.fd],
         detached: true,
         windowsHide: true,
         shell: false,

🔔 Heads up, your workspace has pending recommendations ↗ to auto-apply fixes for similar failures.

Because this branch comes from a fork, it is not possible for us to apply fixes directly, but you can apply the changes locally using the available options below.

Apply changes locally with:

npx nx-cloud apply-locally IV5P-ZJfX

Apply fix locally with your editor ↗   View interactive diff ↗



🎓 Learn more about Self-Healing CI on nx.dev

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant