Skip to content

perf(workspace-fs): watch only directories on Linux - #8325

Open
maleadt wants to merge 2 commits into
ParadevAI:mainfrom
maleadt:tb/linux-dir-watch
Open

maleadt wants to merge 2 commits into
ParadevAI:mainfrom
maleadt:tb/linux-dir-watch

Conversation

@maleadt

@maleadt maleadt commented Oct 8, 2026 •

Copy link
Copy Markdown

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 lint and bun run typecheck pass (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.
  • NativeWatchRequest gains an optional AbortSignal so a cancelled attach stops crawling instead of finishing a crawl nobody will use.

Written for commit 1c67e08. Summary will update on new commits.

View guided diff

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.

On Linux the host-service watched each worktree with chokidar 4, which
opens an fs.watch on every file and every directory and crawls the tree
on the host event loop with two lstat calls per entry. For an LLVM
worktree (17k directories, 186k files) that is 203k inotify watches, a
~9 s attach with event-loop stalls near 0.5 s, and ~725 MB of heap. While
it runs, terminals and every other request to the host wait.

An inotify watch on a directory already reports writes, attribute
changes, creates, deletes and renames of its children, so the new
backend watches directories only, as @parcel/watcher does. It watches a
directory before listing it, and reconciles every reported path against
lstat and the recorded children through one queue, which also tells
create from update. The same LLVM worktree attaches in ~3 s with 17k
watches, stalls of a few milliseconds and ~50 MB of heap.

A write made through a hard link outside the tree is no longer reported,
since inotify only reports a directory's children by the path used. The
request gains an abort signal so that a cancelled attach stops crawling.

Fixes ParadevAI#8324
@coderabbitai

coderabbitai Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 04378bf4-f126-4418-ae9e-268327975b47
📥 Commits

Reviewing files that changed from the base of the PR and between 4070552 and 1c67e08.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • packages/workspace-fs/src/watch-backend/directory-backend.test.ts
  • packages/workspace-fs/src/watch-backend/directory-backend.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The workspace filesystem replaces its Linux Chokidar backend with a directory-only backend. The backend reconciles filesystem events and supports aborting an attachment in progress. Other platforms continue to use the Parcel backend.

Changes

Linux directory-only watching

Layer / File(s) Summary
Backend contract and recursive attachment
packages/workspace-fs/src/watch-backend/types.ts, packages/workspace-fs/src/watch.ts, packages/workspace-fs/src/watch-backend/directory-backend.ts, packages/workspace-fs/src/watch-backend/directory-backend.test.ts
The native watch request accepts an optional abort signal, which the watcher manager passes to the backend. The directory backend watches each directory before listing it, recursively attaches to child directories, and rejects attachment when it is aborted or fails. Tests cover watch allocation and abort cleanup.
Queued event reconciliation
packages/workspace-fs/src/watch-backend/directory-backend.ts, packages/workspace-fs/src/watch-backend/directory-backend.test.ts
The backend queues paths from watcher events and reconciles them against filesystem state to emit create, update, and delete events. Tests cover directory moves and replacement, file and directory transitions, symlinks, and same-name paths.
Linux backend selection and migration
packages/workspace-fs/package.json, packages/workspace-fs/src/watch-backend/chokidar-backend.ts, packages/workspace-fs/src/watch-backend/index.ts, packages/workspace-fs/src/watch-backend/directory-backend-ignore.test.ts, packages/workspace-fs/src/watch-backend/directory-crawl.bench.ts, packages/workspace-fs/src/watch-backend/watch-backend.test.ts, packages/workspace-fs/src/watch-pathtypes-growth.test.ts
Linux selects directoryWatchBackend; other platforms continue to select parcelWatchBackend. The Chokidar backend and dependency are removed. Tests and the benchmark now reference the directory backend, and a test comment describes its asynchronous lstat behavior.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~30 minutes

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant FsWatcherManager
  participant directoryWatchBackend
  participant DirectoryWatch
  participant fs.watch
  participant lstat
  FsWatcherManager->>directoryWatchBackend: subscribe with abort signal
  directoryWatchBackend->>DirectoryWatch: create and attach
  DirectoryWatch->>fs.watch: watch directory before listing
  fs.watch-->>DirectoryWatch: report directory event
  DirectoryWatch->>lstat: classify queued path
  lstat-->>DirectoryWatch: return current path state
  DirectoryWatch-->>FsWatcherManager: deliver create, update, or delete event
Loading

Merge Risk: ⚪ Minimal · up to 1c67e

No merge-blocking issue was established; the watcher retains a recovery path when the watched root is replaced.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 9 functions across 9 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description includes complete What & why, testing details, checklist, behavior changes, performance results, known limitations, and issue context. All required checklist items are marked complete.
Title check ✅ Passed The title uses conventional commit format and clearly identifies the main change: replacing Linux worktree file watches with directory-only watches for performance.
Linked Issues check ✅ Passed The changes satisfy the coding requirements in [#8324]. directoryWatchBackend watches directories before scanning, reconciles queued paths, emits descendant deletes, detects directory identity chang…
Out of Scope Changes check ✅ Passed The changes remain within [#8324]. Backend replacement, Linux selection, abort propagation, benchmarks, and tests support directory-only workspace watching. The PR does not modify `git-directory-worke…
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

[High risk] Replaces file watcher backend on Linux with new implementation.

Bound the initial event drain before merging so busy workspaces can finish attaching.

Findings

  1. P1 Busy workspaces never finish attaching ▶
Fix with agent prompt
### Issue 1
packages/workspace-fs/src/watch-backend/directory-backend.ts:65-66
`attach()` waits for the live event queue to become empty before returning. If a build or another steady stream of writes adds events faster than the serial `lstat` calls clear them, the crawl can finish while `subscribe()` still waits indefinitely. Other watch requests for that root wait behind it.

Limit the initial drain to a snapshot or bounded pass, then finish queued work in the background.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Replaces Linux's chokidar backend with directoryWatchBackend, using one watch per directory and a shared queue to check changed paths.

  • Adds cancellation during attach and tests for moves, replacements, symlinks, and watch counts.
  • Removes the direct chokidar dependency; other platforms still use Parcel.
  • The initial drain needs a bound so steady writes cannot prevent the subscription from becoming ready.
  • Acknowledged behavior change: maleadt states that writes through hard links outside the tree are intentionally no longer reported.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Subscribe on Linux] --> B[Watch each directory before listing it]
  B --> C[Crawl included child directories]
  D[File events] --> E[Queue changed paths]
  C --> F[Drain queued paths]
  E --> F
  F --> G[Check each path and emit changes]
  G --> H{Queue empty?}
  H -- No --> F
  H -- Yes --> I[Return subscription]
  I --> J[Drain later events in background]
  K[Abort during attach] --> L[Close watches and reject attach]
Loading

Reviews (1) · Last reviewed commit: "perf(workspace-fs): watch only directori..." · Reviewed by Greptile

attach() drained the event queue until it was empty. A process that
keeps writing to the tree refills the queue as fast as it is drained,
so subscribe() never resolved: with a script creating files during
attach, 2 of 3 attaches were still pending after 5 s under Node. Settle
only what the crawl queued and drain later changes after attaching;
the same script now attaches in about 90 ms.
@maleadt
maleadt force-pushed the tb/linux-dir-watch branch from 211428a to 1c67e08 Compare October 8, 2026 13:42
@maleadt

maleadt commented Oct 15, 2026

Copy link
Copy Markdown
Author

Bump. @AviPeltz @Kitenite Without something like this, superset is almost unusable on larger repositories (Linux, LLVM, etc). #7878 and #7879 are probably also worthwhile improvements here.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf(workspace-fs): watch only directories on Linux instead of every file

1 participant