Skip to content

fix(core): use recursive FSEvents on macOS instead of non-recursive kqueue#34523

Merged
FrozenPandaz merged 1 commit intonrwl:masterfrom
comp615:fix/macos-watcher-event-filter
Feb 25, 2026
Merged

fix(core): use recursive FSEvents on macOS instead of non-recursive kqueue#34523
FrozenPandaz merged 1 commit intonrwl:masterfrom
comp615:fix/macos-watcher-event-filter

Conversation

@comp615
Copy link
Contributor

@comp615 comp615 commented Feb 19, 2026

Current Behavior

Since Nx 22.5.0, the daemon's native file watcher silently drops all file change events on macOS in large monorepos (~5,250+ watched directories). nx watch, nx serve, and any daemon-dependent file watching is broken.

The root cause is that #34329 switched all watched paths to WatchedPath::non_recursive(). On macOS, the notify crate uses kqueue for non-recursive watches instead of FSEvents. kqueue silently fails at scale due to vnode table pressure (kern.num_vnodes == kern.maxvnodes), causing the daemon to never detect file changes.

This is a scale-dependent bug: it works fine in small workspaces (~30 directories) but breaks silently in large ones.

Nx 22.4.5 Nx 22.5.0+
Small repo (~30 dirs) Works (FSEvents) Works (~30 kqueue watches)
Large repo (~5,250+ dirs) Works (FSEvents) Broken (kqueue silently drops all events)

Expected Behavior

The macOS file watcher should detect file creates, modifications, and deletions at any scale, matching the behavior of Nx 22.4.x.

Fix

Use platform-conditional watch modes:

On macOS, FSEvents handles recursive watching from a single root path, so directory enumeration and dynamic registration are skipped entirely. This also improves daemon startup time on macOS from ~10 minutes to <1 second in a 354-project monorepo.

What changed in watcher.rs

  1. Initial pathset: On macOS, watch only the root directory recursively via FSEvents instead of enumerating all directories for non-recursive kqueue watches.
  2. Dynamic directory registration (on_action): Wrapped in #[cfg(not(target_os = "macos"))] since FSEvents already watches the full tree.

Linux/Windows behavior is completely unchanged.

Why the event filter is fine as-is

We verified that with recursive FSEvents watches, macOS emits specific FileEventKind variants (Create(File), Modify(Data(Content)), Remove(File), Modify(Name(Any))) that the current watch_filterer.rs already handles correctly. Zero events were rejected by the catch-all. The Modify(Any) / Create(Any) variants are kqueue artifacts that are not needed with FSEvents.

Why kqueue fails silently

Apple's File System Events Programming Guide explicitly recommends FSEvents over kqueue for large hierarchies: "If you are monitoring a large hierarchy of content, you should use file system events instead." kqueue requires open(path, O_EVTONLY) per watched directory. Under vnode table pressure, the kernel recycles vnodes with kqueue watches attached without notifying the watcher. There is no error, no partial delivery, and no diagnostic signal.

Tested on

  • macOS 26.3 (Tahoe), Apple Silicon (arm64), APFS
  • 354-project pnpm monorepo (~19,865 non-ignored directories)
  • Verified: file modifications, file creates, and file deletes all detected
  • Daemon init time: ~10 min (with enumeration) -> <1s (with root-only FSEvents watch)

Related Issue(s)

Fixes #34522

@comp615 comp615 requested review from a team as code owners February 19, 2026 21:28
@comp615 comp615 requested a review from FrozenPandaz February 19, 2026 21:28
@netlify
Copy link

netlify bot commented Feb 19, 2026

Deploy Preview for nx-docs canceled.

Name Link
🔨 Latest commit 7cb2bda
🔍 Latest deploy log https://app.netlify.com/projects/nx-docs/deploys/6997809182f0070008f05410
@netlify
Copy link

netlify bot commented Feb 19, 2026

Deploy Preview for nx-dev canceled.

Name Link
🔨 Latest commit 7cb2bda
🔍 Latest deploy log https://app.netlify.com/projects/nx-dev/deploys/69978091b9555b0007733b10
@nx-cloud
Copy link
Contributor

nx-cloud bot commented Feb 19, 2026

View your CI Pipeline Execution ↗ for commit c086f96

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

☁️ Nx Cloud last updated this comment at 2026-02-20 20:59:11 UTC

Copy link
Contributor Author

@comp615 comp615 left a comment

Choose a reason for hiding this comment

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

Note: I feel like there might be a better refactor / solution here (e.g. pull them all up since they are common? Should FileEventKind::Modify(ModifyKind::Data(_)) => continue, even exist now?)...but I didn't have a full understanding and this is a narrow change. Feel free to build upon!

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 has identified a possible root cause for your failed CI:

This CI failure appears to be related to the environment or external dependencies rather than your code changes.

No code changes were suggested for this issue.

You can trigger a rerun by pushing an empty commit:

git commit --allow-empty -m "chore: trigger rerun" git push

Nx Cloud View detailed reasoning on Nx Cloud ↗


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

@comp615 comp615 marked this pull request as draft February 19, 2026 22:17
…queue PR nrwl#34329 switched all watched paths to non-recursive to reduce inotify watch count on Linux. On macOS, the notify crate uses kqueue for non-recursive watches instead of FSEvents. kqueue silently fails at scale (~5,250+ directories) due to vnode table pressure, causing the daemon to never detect file changes in large monorepos. This fix uses platform-conditional watch modes: - macOS: single recursive watch on the workspace root (uses FSEvents) - Linux/Windows: non-recursive per-directory watches (preserves nrwl#33781 fix) On macOS, FSEvents handles recursive watching natively from a single root path, so directory enumeration and dynamic registration are skipped entirely. This also improves daemon startup time on macOS (from ~10 minutes to <1 second in a 354-project monorepo). Fixes nrwl#34522 Amp-Thread-ID: https://ampcode.com/threads/T-019c7804-5473-73ae-861f-d5dc1572a1ce Co-authored-by: Amp <amp@ampcode.com> Amp-Thread-ID: https://ampcode.com/threads/T-019c7804-5473-73ae-861f-d5dc1572a1ce Co-authored-by: Amp <amp@ampcode.com>
@comp615 comp615 force-pushed the fix/macos-watcher-event-filter branch from 7cb2bda to c086f96 Compare February 20, 2026 05:24
@comp615 comp615 changed the title fix(core): expand macOS FileEventKind allowlist in daemon watcher fix(core): use recursive FSEvents on macOS instead of non-recursive kqueue Feb 20, 2026
@comp615 comp615 marked this pull request as ready for review February 20, 2026 05:31
Copy link
Collaborator

@FrozenPandaz FrozenPandaz left a comment

Choose a reason for hiding this comment

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

Thank you! It makes sense to stick with kqueue recursive watcher for macos.

I'm going to follow up with some unit tests to prevent this from happening in the future.

Thank you for your contribution! 👍

@FrozenPandaz FrozenPandaz merged commit d5cd6a1 into nrwl:master Feb 25, 2026
27 checks passed
FrozenPandaz added a commit that referenced this pull request Feb 25, 2026
Adds comprehensive test coverage for PR #34523 (fix for #34522). Rust unit tests (watcher.rs): - test_macos_uses_single_recursive_watch: Verifies macOS uses 1 recursive watch vs 11,022 non-recursive watches, preventing kqueue vnode exhaustion - test_extract_new_directories_filters_ignored: Tests ignore filtering - Helper function determine_watch_strategy() for testing watch setup logic TypeScript integration test (watcher.spec.ts): - should detect file changes in large directory structures: Creates 10,000+ directories and verifies file events are actually delivered. This would fail with 22.5.0 on macOS due to silent kqueue failure. These tests verify the fix prevents kqueue vnode table exhaustion on macOS in large monorepos (5,000+ directories) by using FSEvents instead.
FrozenPandaz added a commit that referenced this pull request Feb 25, 2026
## Current Behavior PR #34523 fixed the macOS file watcher issue (#34522) but did not include comprehensive test coverage. ## Expected Behavior Tests should verify that the fix works correctly and catch any future regressions. ## Related Issue(s) Adds test coverage for #34523 and #34522 --- ## Changes **TypeScript integration test** (`packages/nx/src/native/tests/watcher.spec.ts`): Added **"should detect file changes in large directory structures"** - a comprehensive integration test that: 1. Creates 10,000+ directories simulating a monorepo-scale workspace 2. Starts a real `Watcher` instance 3. Creates and modifies files deep in the directory tree 4. Verifies that file change events are actually delivered This test validates the actual behavior users care about - that file watching works reliably in large repos - rather than testing implementation details. It would catch any regression where events fail to be delivered at scale. ## Testing TypeScript integration test validates the actual bug fix - that file events are delivered reliably in large directory structures with 10,000+ directories.
FrozenPandaz pushed a commit that referenced this pull request Feb 26, 2026
…queue (#34523) ## Current Behavior Since Nx 22.5.0, the daemon's native file watcher silently drops all file change events on macOS in large monorepos (~5,250+ watched directories). `nx watch`, `nx serve`, and any daemon-dependent file watching is broken. The root cause is that #34329 switched all watched paths to `WatchedPath::non_recursive()`. On macOS, the `notify` crate uses **kqueue** for non-recursive watches instead of **FSEvents**. kqueue silently fails at scale due to vnode table pressure (`kern.num_vnodes == kern.maxvnodes`), causing the daemon to never detect file changes. This is a **scale-dependent** bug: it works fine in small workspaces (~30 directories) but breaks silently in large ones. | | **Nx 22.4.5** | **Nx 22.5.0+** | |---|---|---| | **Small repo (~30 dirs)** | Works (FSEvents) | Works (~30 kqueue watches) | | **Large repo (~5,250+ dirs)** | Works (FSEvents) | **Broken** (kqueue silently drops all events) | ## Expected Behavior The macOS file watcher should detect file creates, modifications, and deletions at any scale, matching the behavior of Nx 22.4.x. ## Fix Use platform-conditional watch modes: - **macOS:** Single recursive watch on the workspace root (uses FSEvents natively) - **Linux/Windows:** Non-recursive per-directory watches (preserves the #33781 inotify fix) On macOS, FSEvents handles recursive watching from a single root path, so directory enumeration and dynamic registration are skipped entirely. This also improves daemon startup time on macOS from ~10 minutes to <1 second in a 354-project monorepo. ### What changed in `watcher.rs` 1. **Initial pathset:** On macOS, watch only the root directory recursively via FSEvents instead of enumerating all directories for non-recursive kqueue watches. 2. **Dynamic directory registration (`on_action`):** Wrapped in `#[cfg(not(target_os = "macos"))]` since FSEvents already watches the full tree. Linux/Windows behavior is completely unchanged. ### Why the event filter is fine as-is We verified that with recursive FSEvents watches, macOS emits specific `FileEventKind` variants (`Create(File)`, `Modify(Data(Content))`, `Remove(File)`, `Modify(Name(Any))`) that the current `watch_filterer.rs` already handles correctly. Zero events were rejected by the catch-all. The `Modify(Any)` / `Create(Any)` variants are kqueue artifacts that are not needed with FSEvents. ### Why kqueue fails silently Apple's [File System Events Programming Guide](https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/FSEvents_ProgGuide/KernelQueues/KernelQueues.html) explicitly recommends FSEvents over kqueue for large hierarchies: *"If you are monitoring a large hierarchy of content, you should use file system events instead."* kqueue requires `open(path, O_EVTONLY)` per watched directory. Under vnode table pressure, the kernel recycles vnodes with kqueue watches attached without notifying the watcher. There is no error, no partial delivery, and no diagnostic signal. ## Tested on - macOS 26.3 (Tahoe), Apple Silicon (arm64), APFS - 354-project pnpm monorepo (~19,865 non-ignored directories) - Verified: file modifications, file creates, and file deletes all detected - Daemon init time: ~10 min (with enumeration) -> <1s (with root-only FSEvents watch) ## Related Issue(s) Fixes #34522 Co-authored-by: Amp <amp@ampcode.com> (cherry picked from commit d5cd6a1)
FrozenPandaz added a commit that referenced this pull request Feb 26, 2026
## Current Behavior PR #34523 fixed the macOS file watcher issue (#34522) but did not include comprehensive test coverage. ## Expected Behavior Tests should verify that the fix works correctly and catch any future regressions. ## Related Issue(s) Adds test coverage for #34523 and #34522 --- ## Changes **TypeScript integration test** (`packages/nx/src/native/tests/watcher.spec.ts`): Added **"should detect file changes in large directory structures"** - a comprehensive integration test that: 1. Creates 10,000+ directories simulating a monorepo-scale workspace 2. Starts a real `Watcher` instance 3. Creates and modifies files deep in the directory tree 4. Verifies that file change events are actually delivered This test validates the actual behavior users care about - that file watching works reliably in large repos - rather than testing implementation details. It would catch any regression where events fail to be delivered at scale. ## Testing TypeScript integration test validates the actual bug fix - that file events are delivered reliably in large directory structures with 10,000+ directories. (cherry picked from commit 5d64f72)
FrozenPandaz pushed a commit that referenced this pull request Feb 26, 2026
…queue (#34523) ## Current Behavior Since Nx 22.5.0, the daemon's native file watcher silently drops all file change events on macOS in large monorepos (~5,250+ watched directories). `nx watch`, `nx serve`, and any daemon-dependent file watching is broken. The root cause is that #34329 switched all watched paths to `WatchedPath::non_recursive()`. On macOS, the `notify` crate uses **kqueue** for non-recursive watches instead of **FSEvents**. kqueue silently fails at scale due to vnode table pressure (`kern.num_vnodes == kern.maxvnodes`), causing the daemon to never detect file changes. This is a **scale-dependent** bug: it works fine in small workspaces (~30 directories) but breaks silently in large ones. | | **Nx 22.4.5** | **Nx 22.5.0+** | |---|---|---| | **Small repo (~30 dirs)** | Works (FSEvents) | Works (~30 kqueue watches) | | **Large repo (~5,250+ dirs)** | Works (FSEvents) | **Broken** (kqueue silently drops all events) | ## Expected Behavior The macOS file watcher should detect file creates, modifications, and deletions at any scale, matching the behavior of Nx 22.4.x. ## Fix Use platform-conditional watch modes: - **macOS:** Single recursive watch on the workspace root (uses FSEvents natively) - **Linux/Windows:** Non-recursive per-directory watches (preserves the #33781 inotify fix) On macOS, FSEvents handles recursive watching from a single root path, so directory enumeration and dynamic registration are skipped entirely. This also improves daemon startup time on macOS from ~10 minutes to <1 second in a 354-project monorepo. ### What changed in `watcher.rs` 1. **Initial pathset:** On macOS, watch only the root directory recursively via FSEvents instead of enumerating all directories for non-recursive kqueue watches. 2. **Dynamic directory registration (`on_action`):** Wrapped in `#[cfg(not(target_os = "macos"))]` since FSEvents already watches the full tree. Linux/Windows behavior is completely unchanged. ### Why the event filter is fine as-is We verified that with recursive FSEvents watches, macOS emits specific `FileEventKind` variants (`Create(File)`, `Modify(Data(Content))`, `Remove(File)`, `Modify(Name(Any))`) that the current `watch_filterer.rs` already handles correctly. Zero events were rejected by the catch-all. The `Modify(Any)` / `Create(Any)` variants are kqueue artifacts that are not needed with FSEvents. ### Why kqueue fails silently Apple's [File System Events Programming Guide](https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/FSEvents_ProgGuide/KernelQueues/KernelQueues.html) explicitly recommends FSEvents over kqueue for large hierarchies: *"If you are monitoring a large hierarchy of content, you should use file system events instead."* kqueue requires `open(path, O_EVTONLY)` per watched directory. Under vnode table pressure, the kernel recycles vnodes with kqueue watches attached without notifying the watcher. There is no error, no partial delivery, and no diagnostic signal. ## Tested on - macOS 26.3 (Tahoe), Apple Silicon (arm64), APFS - 354-project pnpm monorepo (~19,865 non-ignored directories) - Verified: file modifications, file creates, and file deletes all detected - Daemon init time: ~10 min (with enumeration) -> <1s (with root-only FSEvents watch) ## Related Issue(s) Fixes #34522 Co-authored-by: Amp <amp@ampcode.com> (cherry picked from commit d5cd6a1)
FrozenPandaz added a commit that referenced this pull request Feb 26, 2026
## Current Behavior PR #34523 fixed the macOS file watcher issue (#34522) but did not include comprehensive test coverage. ## Expected Behavior Tests should verify that the fix works correctly and catch any future regressions. ## Related Issue(s) Adds test coverage for #34523 and #34522 --- ## Changes **TypeScript integration test** (`packages/nx/src/native/tests/watcher.spec.ts`): Added **"should detect file changes in large directory structures"** - a comprehensive integration test that: 1. Creates 10,000+ directories simulating a monorepo-scale workspace 2. Starts a real `Watcher` instance 3. Creates and modifies files deep in the directory tree 4. Verifies that file change events are actually delivered This test validates the actual behavior users care about - that file watching works reliably in large repos - rather than testing implementation details. It would catch any regression where events fail to be delivered at scale. ## Testing TypeScript integration test validates the actual bug fix - that file events are delivered reliably in large directory structures with 10,000+ directories. (cherry picked from commit 5d64f72)
@github-actions
Copy link
Contributor

github-actions bot commented Mar 3, 2026

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 Mar 3, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

2 participants