-
fix(core): use recursive FSEvents on macOS instead of non-recursive kqueue (#34523)
发布于
2026-02-25 14:45:31 +00:00 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, thenotifycrate 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- Initial pathset: On macOS, watch only the root directory
recursively via FSEvents instead of enumerating all directories for
non-recursive kqueue watches. - 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
FileEventKindvariants (Create(File),Modify(Data(Content)),
Remove(File),Modify(Name(Any))) that the current
watch_filterer.rsalready handles correctly. Zero events were rejected
by the catch-all. TheModify(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 requiresopen(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
下载附件
- macOS: Single recursive watch on the workspace root (uses FSEvents