Skip to content

Recursive workspace watch can OOM the host for a large environment root #1779

Description

@uje-m

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 replaceAuthoritativeWatchSetwatchWorkspace.
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:

  1. hardcoded .git, passed as a path (so only <workspace>/.git, not nested .git)
  2. plus !! dir/ entries from
    git status --porcelain=v1 -z --ignored=matching --untracked-files=normal
    (5s / 10MB; on failure, fall back to [".git"] only)
  3. 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

  1. 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.
  2. Open a UI view that subscribes to environment-detail for that environment
    (not merely create the env; not merely thread-detail).
  3. 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:

  1. 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__, …)
  2. 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.
  3. Optional UX: when adding an unmanaged environment, warn if the path is an
    umbrella parent or fails a cheap size probe.

Metadata

Metadata

Assignees

Labels

confirmed-reproBug reproduced independently; see linked reporthostHost daemon, process lifecycle, memory, event loopperfworkspacesWorktrees, environments, git, shells

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions