fix(core): reduce daemon inotify watch count by upgrading watchexec#34329
fix(core): reduce daemon inotify watch count by upgrading watchexec#34329FrozenPandaz merged 14 commits intomasterfrom
Conversation
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
✅ Deploy Preview for nx-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
View your CI Pipeline Execution ↗ for commit d0778fe
☁️ Nx Cloud last updated this comment at |
✅ Deploy Preview for nx-dev ready!
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
|
Failed to publish a PR release of this pull request, triggered by @FrozenPandaz. |
|
Failed to publish a PR release of this pull request, triggered by @FrozenPandaz. |
interprocess 2.3.0 has a compilation error on 32-bit ARM platforms due to a u32->i32 conversion issue in libc::timespec.
🐳 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-workspaceOr just copy this version and use it in your own command: 22.5.0-pr.34329.70ea9ca
To request a new release for this pull request, mention someone from the Nx team or the |
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]
…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
| 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); | ||
| } |
There was a problem hiding this comment.
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
| for ev in action.events.iter() { | ||
| trace!(?ev, "raw event"); | ||
| } |
There was a problem hiding this comment.
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
packages/nx/Cargo.toml
Outdated
| 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" |
- 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
There was a problem hiding this comment.
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"
}
}
},
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
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]>
|
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. |
Current Behavior
The daemon's file watcher uses watchexec 3.0.1 which hardcodes
RecursiveMode::Recursivewhen registering inotify watches. This means every directory gets an inotify watch — including all ofnode_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. TheWatchFiltereronly 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/cacheare skipped entirely at the watch registration level.This dramatically reduces:
How it works
WatchedPath::non_recursive()create_watch_walker()usingignore::WalkBuilder(same pattern aswalker.rs) to enumerate only non-ignored directoriesNonRecursivemode — like putting security cameras only in the rooms you care about instead of every room in the buildingon_actionhandlerWatchFiltereris unchanged — same behavior for gitignore/nxignore patternsmacOS 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
FileEventKindtags as Linux inotify or Windows ReadDirectoryChangesW.Three changes to support macOS:
Create(Folder)andModify(Metadata)) through the filter so the action handler can register themAll 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
serdeto<1.0.220because serde 1.0.220+ moved__privatetoserde_core, breakingswc_common 0.31.22watch_filterer.rs,types.rs,utils.rsrequired no changes (APIs are compatible)Related Issue(s)
Fixes #33781
Fixes nrwl/nx-console#2468