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
Watch mode: keep watcher setup and the readiness signature off the first interaction #1063
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.watch was 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 an ignored predicate so Git-derived pruning works. That constraint still holds; this issue does not propose switching backends.
The "immediate signature check after watcher readiness" is intentional (closes the scan race, pinned by controller.test.ts). Its synchronous spawnSync execution 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 widened watchSignature to string | Promise<string>, but that PR was never merged and is now stale. refactor(ui): keep history reviews in one renderer #992 since added runGitTextAsync and friends to packages/hunk-git, but watchSignature still 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 short setTimeout after mount) rather than in the mount effect. Keep initialWatchSignature captured before content load in loaders.ts so 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 _addToNodeFs is recursive and synchronous per subtree; a wrapper can add directories incrementally, or the walk can move to a worker that reports paths back for fs.watch registration on the main thread). Measure with bun run bench:watch-style timing before choosing.
Add a PTY test in test/pty/watch.test.ts that sends a key at first paint with watch enabled and asserts it is answered within ~100ms on a fixture with a few hundred directories.
Step 2 (chunk the chokidar walk) → fix(watch): register directory watchers in yielding batches on Linux #1076. Directories are registered in batches of 32 depth-0 watchers with a macrotask yield between batches (public chokidar API only). Worst setup stall 260–750 ms → 10–95 ms; the residual is one indivisible 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.
Step 1 (defer start to first idle) — dropped. Measured on Linux: a 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.
Step 4 invariants preserved and verified (--no-optional-locks on every watch-time call, Bun ≥1.3.14 guard, no per-directory watches on ignored trees).
Step 5 → 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):
fixture
watch=off
main, watch=on
now, watch=on
2,000 flat dirs
362 ms
650 ms
395 ms
2,000 dirs, 100 deep
378 ms
1,292 ms
387 ms
hunk repo + ignored node_modules
377 ms
744 ms
399 ms
Not 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.
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 ofspawnSync. 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:watch = falsewatch = trueThe 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:
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.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.How? (optional)
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._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.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.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).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.