Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(core): reduce daemon inotify watch count by upgrading watchexec#34329

Merged
FrozenPandaz merged 14 commits intomasterfrom
fix-daemon-mem
Feb 6, 2026
Merged

fix(core): reduce daemon inotify watch count by upgrading watchexec#34329
FrozenPandaz merged 14 commits intomasterfrom
fix-daemon-mem

Conversation

@FrozenPandaz
Copy link
Collaborator

@FrozenPandaz FrozenPandaz commented Feb 4, 2026

Current Behavior

The daemon's file watcher uses watchexec 3.0.1 which hardcodes RecursiveMode::Recursive when registering inotify watches. This means every directory gets an inotify watch — including all of node_modules, .git, and other ignored trees.

On a typical workspace with a large node_modules, this can consume thousands of inotify watches, eating kernel memory and CPU. The WatchFilterer only filters events after watches are already registered — the watches themselves are never prevented.

Expected Behavior

Only non-ignored directories (workspace source code) get inotify watches. Ignored directories like node_modules, .git, .nx/cache, .nx/workspace-data, and .yarn/cache are skipped entirely at the watch registration level.

This dramatically reduces:

  • inotify watch count (from thousands to hundreds)
  • Memory usage (each watch consumes kernel memory)
  • CPU overhead (fewer watches = less kernel bookkeeping)

How it works

  • Upgraded watchexec 3.0.1 → 8.0.1 which supports WatchedPath::non_recursive()
  • Added create_watch_walker() using ignore::WalkBuilder (same pattern as walker.rs) to enumerate only non-ignored directories
  • Each directory is watched with NonRecursive mode — like putting security cameras only in the rooms you care about instead of every room in the building
  • New directories created at runtime are dynamically added to the watch set via the on_action handler
  • Event-level filtering via WatchFilterer is unchanged — same behavior for gitignore/nxignore patterns

macOS Support for Dynamic Directory Registration

The initial implementation worked on Linux and Windows but failed tests on macOS because macOS FSEvents doesn't always provide the same FileEventKind tags as Linux inotify or Windows ReadDirectoryChangesW.

Three changes to support macOS:

  1. watcher.rs: On macOS, check all events for directory creation (not just events with specific FileEventKind tags) and verify via filesystem
  2. types.rs: Filter directory events from JavaScript callbacks on macOS (similar to Windows behavior)
  3. watch_filterer.rs: Allow macOS directory events (Create(Folder) and Modify(Metadata)) through the filter so the action handler can register them

All changes use #[cfg(target_os = "macos")] for compile-time conditional compilation, so Linux/Windows behavior is completely unchanged and there's zero runtime overhead.

Additional notes

  • Pinned serde to <1.0.220 because serde 1.0.220+ moved __private to serde_core, breaking swc_common 0.31.22
  • No TypeScript changes — the napi interface is identical
  • watch_filterer.rs, types.rs, utils.rs required no changes (APIs are compatible)

Related Issue(s)

Fixes #33781
Fixes nrwl/nx-console#2468

Upgrade watchexec 3.0.1 -> 8.0.1 to use WatchedPath::non_recursive().
Instead of recursively watching the workspace root (which registers
inotify watches on every directory including node_modules), enumerate
only non-ignored directories and watch each individually.

This dramatically reduces inotify watch count, memory usage, and CPU
overhead on Linux workspaces with large node_modules trees.

- Upgrade watchexec and companion crates to latest versions
- Add create_watch_walker() to enumerate non-ignored directories
- Use WatchedPath::non_recursive() for each directory
- Dynamically register watches for newly created directories
- Pin serde <1.0.220 to avoid swc_common compatibility issue
@netlify
Copy link

netlify bot commented Feb 4, 2026

Deploy Preview for nx-docs ready!

Name Link
🔨 Latest commit d0778fe
🔍 Latest deploy log https://app.netlify.com/projects/nx-docs/deploys/69865b18ad484c00081ef7cf
😎 Deploy Preview https://deploy-preview-34329--nx-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@vercel
Copy link

vercel bot commented Feb 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nx-dev Ready Ready Preview Feb 4, 2026 8:28pm

Request Review

@nx-cloud
Copy link
Contributor

nx-cloud bot commented Feb 4, 2026

View your CI Pipeline Execution ↗ for commit d0778fe

Command Status Duration Result
nx affected --targets=lint,test,test-kt,build,e... ✅ Succeeded 52m 42s View ↗
nx run-many -t check-imports check-lock-files c... ✅ Succeeded 2m 46s View ↗
nx-cloud record -- nx-cloud conformance:check ✅ Succeeded 10s View ↗
nx-cloud record -- nx format:check ✅ Succeeded 2s View ↗
nx-cloud record -- nx sync:check ✅ Succeeded <1s View ↗

☁️ Nx Cloud last updated this comment at 2026-02-06 22:18:18 UTC

@netlify
Copy link

netlify bot commented Feb 4, 2026

Deploy Preview for nx-dev ready!

Name Link
🔨 Latest commit d0778fe
🔍 Latest deploy log https://app.netlify.com/projects/nx-dev/deploys/69865b18c2d0f30008c2252f
😎 Deploy Preview https://deploy-preview-34329--nx-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Some tools like touch, rm, and VS Code generate Create(Any), Remove(Any),
and Modify(Any) events instead of the specific File variants. This adds
support for these event kinds on Linux to match Windows behavior.

Also adds tests for detecting files in newly created directories.
- Add Remove(Any) and Modify(Any) event kinds for Linux to handle
  events from tools like rm and editors that don't use specific variants
- Add trace logging for debugging watcher events
- Add test for detecting deleted files in newly created directories
nx-cloud[bot]

This comment was marked as outdated.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 5, 2026

Failed to publish a PR release of this pull request, triggered by @FrozenPandaz.
See the failed workflow run at: https://github.com/nrwl/nx/actions/runs/21725197679

@FrozenPandaz FrozenPandaz marked this pull request as ready for review February 5, 2026 19:25
@FrozenPandaz FrozenPandaz requested review from a team and vsavkin as code owners February 5, 2026 19:25
@github-actions
Copy link
Contributor

github-actions bot commented Feb 5, 2026

Failed to publish a PR release of this pull request, triggered by @FrozenPandaz.
See the failed workflow run at: https://github.com/nrwl/nx/actions/runs/21725404596

nx-cloud[bot]

This comment was marked as outdated.

interprocess 2.3.0 has a compilation error on 32-bit ARM platforms
due to a u32->i32 conversion issue in libc::timespec.
@github-actions
Copy link
Contributor

github-actions bot commented Feb 5, 2026

🐳 We have a release for that!

This PR has a release associated with it. You can try it out using this command:

npx [email protected] my-workspace

Or just copy this version and use it in your own command:

22.5.0-pr.34329.70ea9ca
Release details 📑
Published version 22.5.0-pr.34329.70ea9ca
Triggered by @FrozenPandaz
Branch fix-daemon-mem
Commit 70ea9ca
Workflow run 21726089627

To request a new release for this pull request, mention someone from the Nx team or the @nrwl/nx-pipelines-reviewers.

FrozenPandaz and others added 2 commits February 5, 2026 15:40
Extract HARDCODED_IGNORE_PATTERNS as a shared constant and make
create_walker() reusable between walker.rs and watcher.rs. This
removes ~50 lines of duplicated code.
Extract HARDCODED_IGNORE_PATTERNS as a shared constant and make
create_walker() reusable between walker.rs and watcher.rs. This
removes ~50 lines of duplicated code. [Self-Healing CI Rerun]
nx-cloud[bot]

This comment was marked as outdated.

…ction

Refactored the watcher code to:
- Extract directory registration logic into `register_new_directory_watches()`
- Use HARDCODED_IGNORE_PATTERNS from walker.rs instead of duplicating
- Add clarifying comments about ignore behavior
Comment on lines 241 to 259
let new_directories = register_new_directory_watches(
&action.events,
&ignore_globs_for_action,
&watched_path_set_for_action,
);

// Update the watchexec pathset if new directories were added
if !new_directories.is_empty() {
let path_set = watched_path_set_for_action.lock();
let new_pathset: Vec<WatchedPath> = path_set
.iter()
.map(|p| WatchedPath::non_recursive(p))
.collect();
trace!(
count = new_pathset.len(),
"updating pathset with new directories"
);
watch_exec_for_action.config.pathset(new_pathset);
}
Copy link
Member

Choose a reason for hiding this comment

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

Can we combine these 2? It feels like register should either actually do the registration, or it should reurn the new ones and then we handle it below. Right now it feels like its split into two

Comment on lines 271 to 273
for ev in action.events.iter() {
trace!(?ev, "raw event");
}
Copy link
Member

Choose a reason for hiding this comment

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

Can we inline into either par_iter or a tap somewhere in there? Feels weird to iter to trace and the iter again to do the real work... The events are realistically not going to be some massive collection so it probably doesn't really matter but feels odd

xxhash-rust = { version = '0.8.5', features = ['xxh3', 'xxh64'] }
vt100-ctt = { git = "https://github.com/JamesHenry/vt100-rust", rev = "b15dc3b0f7db94167a9c584f1d403899c0cc871d" }
serde = "1.0.219"
serde = ">=1.0.219, <1.0.220"
Copy link
Member

Choose a reason for hiding this comment

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

This looks weird?

- Simplify serde version pin to =1.0.219
- Make extract_new_directories a pure function (no mutex side effect)
- Inline raw event trace loop into par_iter
- Remove Promise(done) antipattern from watcher tests
- Use WatchEvent[] type instead of any[]
- Remove stale console.log from test setup
- Simplify wait() helper
Copy link
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've increased the Node.js heap limit to 4GB for the test task to fix the out-of-memory error. The refactored watcher tests now accumulate events in memory arrays and run for longer durations, requiring more heap space than the default 2GB limit. This change ensures the test suite can complete successfully without running out of memory.

Tip

We verified this fix by re-running nx:test.

diff --git a/nx.json b/nx.json
index 1aebbbff8a..e4c413a3ee 100644
--- a/nx.json
+++ b/nx.json
@@ -127,7 +127,7 @@
       "options": {
         "args": ["--passWithNoTests", "--detectOpenHandles", "--forceExit"],
         "env": {
-          "NODE_OPTIONS": "--experimental-vm-modules"
+          "NODE_OPTIONS": "--experimental-vm-modules --max-old-space-size=4096"
         }
       }
     },

Apply fix via Nx Cloud  Reject fix via Nx Cloud


Or Apply changes locally with:

npx nx-cloud apply-locally gb5E-Bm6s

Apply fix locally with your editor ↗   View interactive diff ↗


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

- Remove redundant Vec<WatchedPath> from enumerate_watch_paths, return only HashSet<PathBuf>
- Inline is_ignored_path one-liner
- Refactor extract_new_directories to use iterator chain
- Use extend() instead of manual loop with clone
- Pass iterators directly to pathset() instead of collecting into Vec
FrozenPandaz and others added 4 commits February 6, 2026 16:20
Windows reports CreateKind::Any (not CreateKind::Folder) for directory
creation via ReadDirectoryChangesW. This adds Windows support for
dynamically registering watches on newly created directories, maps
CreateKind::Any to EventType::create for correct event types, and
filters directory events from the JS callback in transform_event.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
macOS FSEvents doesn't always provide FileEventKind tags like Linux/Windows,
requiring platform-specific handling for dynamic directory registration:

- watcher.rs: On macOS, check all events and verify directories via filesystem
  (Linux/Windows filter early based on FileEventKind tags)
- types.rs: Filter directory events from JS callback on macOS (similar to Windows)
- watch_filterer.rs: Allow macOS directory events (Create(Folder) and Modify(Metadata))
  through the filter so the action handler can register them

This enables the watcher to detect files created in newly created directories on macOS.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@FrozenPandaz FrozenPandaz merged commit 0aef1ef into master Feb 6, 2026
24 checks passed
@FrozenPandaz FrozenPandaz deleted the fix-daemon-mem branch February 6, 2026 22:21
@github-actions
Copy link
Contributor

This pull request has already been merged/closed. If you experience issues related to these changes, please open a new issue referencing this pull request.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 12, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Daemon file watcher slow to start causing stale cache and project graph in large repos using pnpm workspace High memory/file-system-watcher usage

2 participants