Commit d5cd6a1
fix(core): use recursive FSEvents on macOS instead of non-recursive kqueue (#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>1 parent 700c98f commit d5cd6a1
1 file changed
Lines changed: 49 additions & 24 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
201 | 201 | | |
202 | 202 | | |
203 | 203 | | |
204 | | - | |
205 | | - | |
206 | | - | |
207 | | - | |
208 | | - | |
209 | | - | |
210 | | - | |
211 | | - | |
212 | | - | |
213 | | - | |
214 | | - | |
215 | | - | |
216 | | - | |
217 | | - | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
218 | 224 | | |
219 | 225 | | |
220 | 226 | | |
| |||
284 | 290 | | |
285 | 291 | | |
286 | 292 | | |
287 | | - | |
288 | | - | |
289 | | - | |
290 | | - | |
291 | | - | |
292 | | - | |
293 | | - | |
294 | | - | |
295 | | - | |
296 | | - | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
297 | 322 | | |
298 | 323 | | |
299 | 324 | | |
| |||
0 commit comments