Environment
- bb-app 0.38.0 (host-daemon +
bb-parcel-watcher-child.mjs)
- Linux,
@parcel/watcher 2.5.6 / inotify
fs.inotify.max_user_watches=1048576
Symptom
Opening a UI surface that subscribes to environment-detail for an unmanaged
environment whose workspace is a very large directory tree makes
bb-parcel-watcher-child.mjs walk that tree, register on the order of 1e6 inotify
watches, and grow to tens of GB RSS. The daemon respawns the child (exit or ping
timeout), reapplies the same watch set, and the host OOM-kills on a loop.
On this machine, one such root was /home/uje/agentics (~1.2M directories; umbrella
git checkout containing many nested repos). While that environment was in the live
watch set the child sat at ~1,004,390 inotify watches (just under the 1,048,576 cap)
and ~30 GB RSS + ~25 GB swap. Dropping that environment-detail interest dropped the
same child to ~1,168 watches / ~60 MB.
What the code does
WatchInterestCoordinator does not watch every ready environment. It builds the
daemon watch set from current realtime interests:
environment-detail + status === "ready" + path → recursive workspace watch
thread-detail → thread-storage watch only
The daemon applies that set via replaceAuthoritativeWatchSet → watchWorkspace.
The child calls @parcel/watcher subscribe(dir, cb, opts) with opts unchanged.
Workspace-root ignore is not the git-internal list (hooks / info / logs /
modules / objects / worktrees). That list is only for the separate git-dir
watch.
Workspace-root ignore is:
- hardcoded
.git, passed as a path (so only <workspace>/.git, not nested .git)
- plus
!! dir/ entries from
git status --porcelain=v1 -z --ignored=matching --untracked-files=normal
(5s / 10MB; on failure, fall back to [".git"] only)
- no options at all if the workspace has no
.git
@parcel/watcher path ignores are path.resolve(watchRoot, value). They do not
match nested node_modules / .git / .cache. File listing already skips
node_modules and dot-directories; the watcher does not use that set.
For an umbrella parent, git status --untracked-files=normal does not recurse into
untracked nested checkouts, so those trees (and their node_modules / .git) are
fully watched.
There is no directory-count, depth, or inotify budget. A doomed watch set is
re-pushed on every child session.
Reproduction
- Register an unmanaged environment whose path is a large parent (many nested
repos / node_modules / .git trees). A non-git root is even worse (no ignore).
An umbrella git root with untracked children is enough.
- Open a UI view that subscribes to
environment-detail for that environment
(not merely create the env; not merely thread-detail).
- Watch
bb-parcel-watcher-child.mjs RSS and /proc/<pid>/fdinfo inotify watch
count climb. After kill/respawn the same set comes back.
Suggested fix
Layered, in this order:
- Default recursive ignore globs on every workspace-root subscribe, independent
of git discovery. Parcel globs are picomatch on the path relative to the watch
root and are FTS_SKIP'd during the initial crawl. Prefer directory matches, not
only children:
**/node_modules, **/.git, **/.cache
- plus the existing listing skip names as globs
(**/target, **/vendor, **/dist, **/build, **/out, **/venv,
**/__pycache__, …)
- Hard cap before subscribe: if the walk / inotify budget would be exceeded,
fail that workspace watch, emit watch-error, and do not respawn into the same
set forever.
- Optional UX: when adding an unmanaged environment, warn if the path is an
umbrella parent or fails a cheap size probe.
Environment
bb-parcel-watcher-child.mjs)@parcel/watcher2.5.6 / inotifyfs.inotify.max_user_watches=1048576Symptom
Opening a UI surface that subscribes to
environment-detailfor an unmanagedenvironment whose workspace is a very large directory tree makes
bb-parcel-watcher-child.mjswalk that tree, register on the order of 1e6 inotifywatches, and grow to tens of GB RSS. The daemon respawns the child (exit or ping
timeout), reapplies the same watch set, and the host OOM-kills on a loop.
On this machine, one such root was
/home/uje/agentics(~1.2M directories; umbrellagit checkout containing many nested repos). While that environment was in the live
watch set the child sat at ~1,004,390 inotify watches (just under the 1,048,576 cap)
and ~30 GB RSS + ~25 GB swap. Dropping that environment-detail interest dropped the
same child to ~1,168 watches / ~60 MB.
What the code does
WatchInterestCoordinatordoes not watch every ready environment. It builds thedaemon watch set from current realtime interests:
environment-detail+status === "ready"+path→ recursive workspace watchthread-detail→ thread-storage watch onlyThe daemon applies that set via
replaceAuthoritativeWatchSet→watchWorkspace.The child calls
@parcel/watchersubscribe(dir, cb, opts)withoptsunchanged.Workspace-root ignore is not the git-internal list (
hooks/info/logs/modules/objects/worktrees). That list is only for the separate git-dirwatch.
Workspace-root ignore is:
.git, passed as a path (so only<workspace>/.git, not nested.git)!! dir/entries fromgit status --porcelain=v1 -z --ignored=matching --untracked-files=normal(5s / 10MB; on failure, fall back to
[".git"]only).git@parcel/watcherpath ignores arepath.resolve(watchRoot, value). They do notmatch nested
node_modules/.git/.cache. File listing already skipsnode_modulesand dot-directories; the watcher does not use that set.For an umbrella parent,
git status --untracked-files=normaldoes not recurse intountracked nested checkouts, so those trees (and their
node_modules/.git) arefully watched.
There is no directory-count, depth, or inotify budget. A doomed watch set is
re-pushed on every child session.
Reproduction
repos /
node_modules/.gittrees). A non-git root is even worse (no ignore).An umbrella git root with untracked children is enough.
environment-detailfor that environment(not merely create the env; not merely
thread-detail).bb-parcel-watcher-child.mjsRSS and/proc/<pid>/fdinfoinotify watchcount climb. After kill/respawn the same set comes back.
Suggested fix
Layered, in this order:
of git discovery. Parcel globs are picomatch on the path relative to the watch
root and are
FTS_SKIP'd during the initial crawl. Prefer directory matches, notonly children:
**/node_modules,**/.git,**/.cache(
**/target,**/vendor,**/dist,**/build,**/out,**/venv,**/__pycache__, …)fail that workspace watch, emit
watch-error, and do not respawn into the sameset forever.
umbrella parent or fails a cheap size probe.