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
File viewer: Open instantly by backgrounding the FS watcher subscribe
- Move the blocking FSEvents subscribe off `open_session`'s critical path onto the watcher-manager thread (`spawn_watcher_manager`). Open drops from ~118ms idle (unbounded and fat-tailed under load: 195-730ms under a synthetic FS-event flood) to ~0.3ms regardless of system load. This is a prod win too: every viewer open previously paid this latency under the 2s `viewer_open` timeout, so a busy machine or slow/network path could time out the open.
- Close the open→subscribe missed-append window with `catch_up_after_subscribe`: right after subscribing, re-stat the file and, if on-disk size exceeds what the live backend covers, drive the same `Grew` path a real event would. Comparing against live backend coverage is correct regardless of upgrade timing (mid-upgrade queues into `pending_grew`; post-upgrade tail-extends or emits).
- Fix the `search_poll_no_active_search` 8s-nextest-cap flake: the test did ~zero CPU work; 100% of its time was that one `fseventsd`-bound subscribe, whose tail crossed 8s under the saturated check suite.
- Tests that inject synthetic watcher events now call `wait_for_watcher_subscribed()`, since the watcher isn't live the instant open returns.
Copy file name to clipboardExpand all lines: apps/desktop/src-tauri/src/file_viewer/CLAUDE.md
+15-7Lines changed: 15 additions & 7 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -98,7 +98,7 @@ Classification per debounce window:
98
98
-`Replaced` when the inode changed (rename + atomic replace, log rotation).
99
99
-`MetadataOnly` when nothing observable changed.
100
100
101
-
Per-session, a manager thread (`spawn_watcher_manager_thread`) consumes events on the subscription channel:
101
+
Per-session, a manager thread (`spawn_watcher_manager`) does the FSEvents subscribe itself (see the gotcha below), then consumes events on the subscription channel:
102
102
103
103
- Always emits `viewer:file-changed:<sid>` with `{ kind: "grew", newSize }` or `{ kind: "rotated" }`.
104
104
-`Grew` with `upgrading` or `rebuilding` in flight: queues `pending_grew = Some(new_size.max(prev))` (drain-and-swap
@@ -118,12 +118,20 @@ Per-session, a manager thread (`spawn_watcher_manager_thread`) consumes events o
118
118
119
119
## Gotchas (tail mode)
120
120
121
-
**Watcher subscribe happens AFTER `SESSIONS.insert` but BEFORE the upgrade spawn.**`notify-debouncer-full::new_debouncer`
122
-
plus `debouncer.watch` need the session already in `SESSIONS` so the manager thread can look it up. They run before the
123
-
upgrade thread spawn so the watcher captures any append that lands during the upgrade window; otherwise an append
124
-
arriving between SESSIONS.insert and the watcher's first event would be observed by no one (the upgrade has already
125
-
stored its LineIndex covering only the pre-append EOF, and no later FS event ever fires for that one append). Pinned by
126
-
`tail_mode_on_extends_backend_when_watcher_reports_grew` and `test_append_during_upgrade_not_dropped`.
121
+
**The FSEvents subscribe runs on the manager thread, NOT inline in `open_session`.**`notify-debouncer-full::new_debouncer`
122
+
+`debouncer.watch` is a blocking, `fseventsd`-bound call: ~100 ms idle on macOS and seconds under load (measured: a
123
+
0.3 s test became >8 s under the full check suite, tripping the nextest cap; a synthetic FS-event flood pushed the
124
+
subscribe alone from 118 ms to 730 ms). Doing it inline made every viewer open pay that latency and risked the 2 s
125
+
`viewer_open` timeout on a busy system. So `open_session` spawns `spawn_watcher_manager`, which subscribes on its own
126
+
thread and only then runs the poll loop. Open is now sub-millisecond regardless of system load. **Don't move the
127
+
subscribe back inline.** Because the subscribe no longer precedes the upgrade thread, an append could land in the
128
+
open→subscribe window and fire no event (the watcher's size baseline is the on-disk EOF at subscribe time). That window
129
+
is closed by `catch_up_after_subscribe`: right after subscribing, it re-stats the file and, if the on-disk size exceeds
130
+
what the active backend currently covers, drives the same `Grew` path a real event would — correct whether the
131
+
ByteSeek→LineIndex upgrade has stored yet (mid-upgrade it queues into `pending_grew`; post-upgrade it tail-extends or
132
+
emits). Tests that inject synthetic watcher events must call `wait_for_watcher_subscribed()` after `open_session` first,
133
+
since the watcher isn't live the instant open returns. Pinned by `tail_mode_on_extends_backend_when_watcher_reports_grew`
134
+
and `test_append_during_upgrade_not_dropped`.
127
135
128
136
**Tail-extend race against an encoding rebuild.**`apply_tail_extend` snapshots the active backend `Arc`, drops the
129
137
SESSIONS lock, runs `extend_to_boxed` (multi-second on a multi-MB append), then re-acquires the lock. If an encoding
0 commit comments