Repository navigation
Description
What do you want to change?
With watch = true, defer creating the filesystem observer until the first frame has reached idle, and make the readiness signature check (and the bootstrap's initial signature) run through the async git runners instead of spawnSync. On Linux, keep Git-pruned chokidar but move its directory walk off the main thread's first interaction.
Why?
Right after the first frame, useWatchedInput's mount effect builds the watch plan and starts the observer. On Linux that is chokidar walking every non-ignored directory to install per-directory watchers. It is synchronous main-thread work and lands exactly when the user starts pressing keys. Measured in a PTY on this repo (1.3k files, 212 dirs), key sent at first frame:
| Config | Key answered | Main-thread stall right after first frame |
|---|---|---|
watch = false |
+52ms | none from watch |
watch = true |
+180ms | 130ms chokidar walk, then 11ms sync git signature |
The walk scales with repo size. The #531 benchmark report (docs/watch-benchmark-final.md) recorded 858ms observer-ready wall time on its "Linux big repo" case and noted: "Concentrated setup can still affect the first interaction even when its CPU-equivalent cost is recovered within seconds." That benchmark measured time-to-first-frame, so work that runs after the first frame did not show up in its headline numbers.
History, for context:
- Chokidar on Linux instead of native recursive
fs.watchwas deliberate and measured in feat(watch): replace 250 ms watch polling with evented filesystem observation #531: Bun's native recursion registers one inotify watch per directory including ignored trees (~24k per session, exhausting the default 61,504 limit after ~3 sessions). Chokidar accepts anignoredpredicate so Git-derived pruning works. That constraint still holds; this issue does not propose switching backends. - Running the setup on the main thread at mount, and its timing relative to the first frame, was never discussed in feat(watch): replace 250 ms watch polling with evented filesystem observation #531 or since.
- The "immediate signature check after watcher readiness" is intentional (closes the scan race, pinned by
controller.test.ts). Its synchronousspawnSyncexecution was inherited from the old polling loop. perf(watch): stop watch-mode signature checks from stalling the review UI #677 later called the sync signature a bug ("it stops it freezing the terminal") and widenedwatchSignaturetostring | Promise<string>, but that PR was never merged and is now stale. refactor(ui): keep history reviews in one renderer #992 since addedrunGitTextAsyncand friends topackages/hunk-git, butwatchSignaturestill uses the sync runner. - Issue Watch mode is very slow #543 covers a related but different symptom: steady-state stalls during editing from the same sync git calls.
How? (optional)
- In
useWatchedInput, start the controller after the renderer's first idle (or a shortsetTimeoutafter mount) rather than in the mount effect. KeepinitialWatchSignaturecaptured before content load inloaders.tsso the startup race stays closed; the readiness check still runs once the observer reports ready. - Chunk chokidar's directory walk so it yields to the event loop between directories (chokidar's own
_addToNodeFsis recursive and synchronous per subtree; a wrapper can add directories incrementally, or the walk can move to a worker that reports paths back forfs.watchregistration on the main thread). Measure withbun run bench:watch-style timing before choosing. - Land the async
watchSignaturefrom perf(watch): stop watch-mode signature checks from stalling the review UI #677 on top of refactor(ui): keep history reviews in one renderer #992's async runners:string | Promise<string>with anAbortSignal, usingrunGitTextAsync,listGitUntrackedFilesAsync, andpreventOptionalLocksunchanged. This also addresses the steady-state freeze in Watch mode is very slow #543. - Keep existing invariants: 2s readiness deadline with polling fallback on
ENOSPC/EMFILE, no per-directory inotify watches on ignored trees,--no-optional-lockson every watch-time git call (hunk diff —watchcreates git locks #398), the Bun ≥ 1.3.14 guard (fix(watch): refuse deadlocking Bun runtimes #902). - Add a PTY test in
test/pty/watch.test.tsthat sends a key at first paint with watch enabled and asserts it is answered within ~100ms on a fixture with a few hundred directories.
Activity
benvinegar commentedon Sep 8, 2026
Ahhh good find
fix(watch): defer observer setup beyond the mount turn (#1063)
fix(watch): recover recreated directory subtrees (#1063)
elucid commentedon Sep 9, 2026
Landed in two PRs; closing.
watchSignature) → fix(watch): check Git signatures asynchronously #1075. Sync signature stall on the main thread 48 → 3 ms (2k dirs), 185 → 3 ms (deep tree). Extension API 25 adds Promise-returning, cancellable hooks. Also fixed Watch mode is very slow #543.readdir+stat of the widest single directory. Ignored trees still get zero inotify watches. Time-to-ready rises (~1.6× on flat 2k, ~10% on deep trees), so the 2 s readiness deadline now measures time without a completed batch rather than total time; ENOSPC/EMFILE polling fallback is unchanged.setTimeout(0)deferral moved the 300-dir first-key latency from 121/188 ms to 135/168 ms (median/max). It yields the mount turn but the traversal still lands on the first keypress; fix(watch): register directory watchers in yielding batches on Linux #1076 makes it moot.--no-optional-lockson every watch-time call, Bun ≥1.3.14 guard, no per-directory watches on ignored trees).test/pty/watch-start-integration.test.ts: key sent at first paint on a nested fixture; fails on the previous main 5/5, passes 5/5.Whole-TUI, key pressed at first frame → response visible, Linux 4 vCPU, median (n=3):
node_modulesNot addressed here: the ~370 ms watch-off floor itself (handler receives the key in ~14 ms; the rest is rendering), and two pre-existing Linux observation gaps (deleting the watched root; recreating a directory within ~1 ms of deleting it) — filed separately.