perf(workspace-fs): watch only directories on Linux
What & why
On Linux the host-service watches each worktree with chokidar 4, which opens an fs.watch (one inotify watch) on every file and every directory, and crawls the tree on the host event loop with two lstat calls per entry. On a large repository that blocks the host: with several LLVM worktrees (186k files each) open, the vitals log showed FSEventWrap swinging between ~100 and 420k, event-loop busyShare at 1.0 and delays up to 4.4 s. During those stalls, opening a terminal hangs, and so does every other request to that host. Each such worktree also uses ~200k of the user's fs.inotify.max_user_watches.
A directory's inotify watch already reports writes, attribute changes, creates, deletes and renames of its children, by name, so the per-file watches only duplicate it. This PR replaces the chokidar backend with one that watches directories only, as @parcel/watcher (and so VS Code) does on Linux, and as chokidar's unreleased v6 does. It watches a directory before listing it. Callbacks only queue the path they name, and one queue reconciles each path against lstat and the children recorded per directory. That is also how a create is told apart from an update. A replaced directory is detected by its device and inode, and an event about a watched directory itself reopens its watch. NativeWatchRequest gains an optional abort signal, so a cancelled attach stops crawling instead of finishing a crawl nobody will use.
The one behaviour change is that a write made through a hard link outside the tree is no longer reported, because inotify reports a directory's children by the path that was used. Runtime watch errors such as ENOSPC still go to the same log-only onUnexpectedError as before. Moving the crawl into a worker, and using this backend for git-directory-worker.ts (whose recursive fs.watch on Linux also watches every file), are possible follow-ups. This is complementary to #7878 and #7879, which reduce how often a worktree is re-crawled; this PR reduces what each crawl costs.
Fixes #8324
How I tested it
LLVM worktree (17k directories, 186k files), Node 22.13 on a 32-core Linux machine under load, two runs each:
| chokidar | this PR | |
|---|---|---|
| attach | 8.7–9.9 s | 2.8–3.1 s |
| max event-loop stall during attach | 480 ms | 2–3 ms |
| inotify watches | 203,384 | 16,927 |
| heap after attach | ~725 MB | ~50 MB |
max stall moving llvm/test (72.5k entries) out of the tree | 0.8–1.1 s | 126–139 ms |
Both report the same 72,532 events for that move, out of the tree and back in. A harness ran both backends side by side through 21 operations: create, append, atomic save, chmod, mkdir -p followed by an immediate write, file and directory renames, rm -rf, file↔directory replacement, symlinks, a 200-file burst, and a directory moved in from outside. After coalesceWatchEvents, the events matched except in the two hard-link cases.
The shared backend suite now also runs the two inotify race tests that parcel skips. New tests cover:
- watch count;
- a directory moved away or moved within the tree;
- a directory replaced at the same path;
- a file replaced by a directory and back;
- symlinks;
- a child named like its directory;
- an attach aborted mid-crawl.
These tests passed 100/100 runs on Linux. The rest of the workspace-fs suite and the host-service watcher tests pass on Linux, except search.adversarial3.test.ts, which fails the same way on main. Bun's fs.watch drops a second event for the same name within a few milliseconds (Node does not), so two tests pause briefly before recreating an entry.
Checklist
- PR title follows conventional commits (
type(scope): subject) -
bun run lintandbun run typecheckpass (CI fails on lint warnings too) - "Allow edits from maintainers" is checked on fork PRs
Summary by cubic
Replaces the chokidar backend for Linux worktree watching with a directory-only backend using one inotify watch per directory. On large worktrees this cuts attach time from ~9 s to ~3 s, drops inotify watches from ~200k to ~17k per worktree, reduces event-loop stalls from hundreds of milliseconds to a few, and prevents host hangs with multiple large worktrees open (#8324). Attach also no longer hangs when the tree keeps changing: it settles what the crawl queued and drains later events after attaching.
Behavior changes
- Writes made through a hard link outside the tree are no longer reported, because inotify reports a directory's children by the path that was used.
NativeWatchRequestgains an optionalAbortSignalso a cancelled attach stops crawling instead of finishing a crawl nobody will use.
Summary by CodeRabbit
- New Features
- Linux file watching now tracks additions, updates, and deletions through directory-based monitoring.
- Watch setup can be cancelled while it is still in progress.
- Bug Fixes
- Improved tracking when directories or files are moved or replaced.
- Improved handling of symlinks, ignored entries, and filesystem changes during large directory scans.
- Watch coverage is cleaned up when setup is cancelled or encounters certain filesystem errors.