You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[rush] ProjectWatcher re-arm after every iteration is quadratic in watched folders and covers disabled operations聽#6110
Package: rush-lib (libraries/rush-lib, ProjectWatcher). Checked against main at 1e18b16 (2026-10-01).
Summary
On Linux (no native recursive fs.watch), ProjectWatcher closes every watcher before each iteration and re-creates them all when the graph goes idle. The re-arm has three avoidable costs:
Each fs.watch gets the session AbortSignal. Node adds one abort listener per watcher, and EventTarget.addEventListener scans the existing listeners for duplicates, so arming n watchers is O(n^2).
It walks every operation and enumerates each operation's project's tracked files. A project with several phases is enumerated once per phase.
It watches every operation in the graph, including disabled ones. With includeAllProjectsInWatchGraph, that is every project in the repo.
Repro
Linux, a large monorepo (~20,000 watched folders for the selection), rush start --to <app>, optionally with watchOptions.includeAllProjectsInWatchGraph: true. Edit a file of a library and time from the end of the iteration to "Waiting for changes" (or profile #startWatching).
(1) alone was 0.6-1.1 s of main-thread time per iteration with ~20k watchers.
(2): enumerating once per project instead of once per operation cut the re-arm from 597 ms to 328 ms.
(3): with all projects in the graph, the re-arm took ~7.0 s per iteration instead of ~1.7 s.
With all three shimmed, the re-arm still costs 0.32-0.54 s per iteration, of which fs.watch creation is ~0.15-0.28 s. Keeping watchers alive between iterations would remove that too.
Root cause (main @ 1e18b16, libraries/rush-lib/src/logic/ProjectWatcher.ts)
:327-333: fs.watch(watchedPath, { ..., signal: sessionAbortSignal }, ...), i.e. one abort listener per watcher.
:292, :295, :302-313: iterates this.#graph.operations (all operations, enabled or not) and calls getTrackedFileHashesForOperation(rushProject) once per operation.
:186 and :195: #stopWatchingAsync() before every iteration and #startWatching() on every idle, which closes and re-creates every watcher.
Suggested fix
Don't pass the session signal to each fs.watch. Keep the watchers in the existing map and close them all from one abort listener.
Dedupe by associatedProject before enumerating tracked folders, and skip operations whose record is disabled. Re-arm when the enabled set changes.
Keep the watchers alive across iterations. Diff the folder set (from the snapshot) and add or close only the folders that changed. Events that arrive during an iteration can be queued for the idle re-check that main now performs.
Activity