Commit Graph
1 Commits
Author SHA1 Message Date
Neil e7c5263a93 perf(git): make local Git metadata observation event-driven (no scheduler) (#16404)
* perf(git): make local Git metadata observation event-driven

Replaces the recurring per-repo metadata scan with native filesystem events on
macOS, Linux, and Windows. Polling is retained purely as a fallback.

- Narrow @parcel/watcher stream over <common>/worktrees, extended from macOS to
  Linux and Windows, with the Windows backend pinned explicitly.
- New shallow watcher mode over the allowlisted primary metadata leaves. It
  watches the containing directory rather than each file, so Git's atomic
  write-and-rename does not orphan the binding.
- Selected upstream refs stay on the existing bounded stat poll.

Verified on real hosts rather than in principle:

- Windows: `git worktree remove` and `git worktree prune` both succeed while the
  narrow stream holds the directory. The historical concern that an open handle
  would block prune does not reproduce.
- Linux: inotify costs one instance per event loop, not one per watch, so the
  watch budget is not a constraint.
- macOS/Linux/Windows: shallow events survive repeated commit, checkout,
  config, and pack-refs cycles.

Failure handling, each reproduced before being fixed:

- fs.watch binds an inode and reports nothing once that inode is replaced, with
  no error. Directory bindings are re-checked on a bounded cadence and rebound.
- A host whose notification path is dead accepts registrations and stays mute
  forever. Observed on a macOS machine whose fseventsd had grown to ~15GB and
  saturated a core. A one-shot delivery probe now fails the shallow subscribe on
  such a host so it falls back to polling instead of showing stale metadata.

This change stands alone on main and does not depend on the metadata poll
scheduler.

* test(git-watch): hold reserved inodes across root replacements

Linux returns a released reservation to the free list, so the second
replacement could land back on the first replacement's inode and look
unchanged to reconciliation. Verified on ext4: releasing yields inodes
[N, N+43, N+43] while holding yields [N, N+43, N+44]. macOS never
recycles, which is why this only failed on CI.

* refactor(git-watch): share one single-flight helper between watcher fallbacks

Both fallbacks tracked their in-flight promise with the same self-comparison
on settle, duplicated verbatim. Hoisting it removes a subtle invariant that
was being hand-maintained in two places.

* fix(git-watch): close the silent-staleness paths in primary metadata

Two independent reviews converged on the same root cause: nothing bounded
how long primary metadata could stay wrong once the shallow watcher stopped
reporting. Four distinct paths led there.

- A terminal watch error arriving while the status-ref poll was still starting
  left the repo with status-ref coverage only. handleWatcherError ran its
  teardown against nulls, then the in-flight poll installed itself, and the
  fallback guard mistook it for coverage and discarded the fallback. Primary
  metadata was then never observed again. The guard no longer treats status-ref
  polling as primary coverage, and startup re-checks watcher liveness after its
  awaits.

- Nothing re-read the six primary files while the watcher was nominally live.
  A lossy notification path, a dropped batch, or inotify queue overflow raises
  no error, so the error-driven fallback never fired and the inode rebind sweep
  does not detect loss. A 15-tick backstop re-stats them, turning permanent
  staleness into one tick. Measured cost is ~0.2 stats/s/repo against the 3/s
  the old poll cost.

- Reconciliation treated any late-observed entry create as a root replacement,
  so an ordinary  tore down a healthy stream ~30s later and
  opened a deaf window. It now also requires the root itself to be recreated.

- The shallow watcher recorded directory identity from a stat issued after
  binding, so a replacement in that gap pinned the dead inode's watcher to the
  new identity and the sweep would never rebind. Identity is now read first,
  which errs toward a harmless extra rebind.

Test helper: replacing the worktrees root frees several inodes at once, so
reserving one still let the recreated root reuse its own. It now verifies the
inode actually changed. Confirmed on ext4, where three holds were needed.
2026-08-25 01:22:07 -07:00