Repository navigation
fix(server): file search no longer freezes the server while fff's watcher reads a large file - #36273
Conversation
…cher reads a large file fff's watcher holds its picker lock while it reads each new file to the end, so in a non-git root one large or slow file blocked every search, and closing an index, on the event loop for as long as that read took. The fff-node patch adds fileSearchAsync, directorySearchAsync, mixedSearchAsync and grepAsync, which run the native search on a libuv pool thread. Searches on one finder run one at a time, so a held lock occupies at most one pool thread per finder. destroy() now frees the handle on a pool thread too. WorkspaceSearchIndex uses the async calls. Fixes #11306 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fff's isScanning takes the same picker lock the watcher holds while it reads a new file, so the symlink scan and the timed-out initial scan check could still block the event loop before a search went off-thread. Both now read scan progress through getScanProgressAsync. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Other scenariosAll 64 scenario measurements match All 64 scenario measurements
Baseline: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
ApprovabilityVerdict: Approved at Macroscope's review found this PR approvable — Workspace searches and finder cleanup move off the JavaScript thread to avoid freezing the server during watcher lock contention. Searches are serialized per finder, and cleanup waits for pending native calls; no product-default or diagnostic-suppression changes are introduced. You can add or adjust custom eligibility rules. Learn more. |
|
No actionable comments were generated in the recent review. Walkthrough
Merge Risk:
|
Review item confirmed and fixed: fff's isScanning takes the picker lock, so WorkspaceSearchIndex's two remaining sync calls could still block the event loop while the watcher reads a large file. Those calls were scanSymlinkEntries' indexComplete check, which runs on a first or stale symlink scan before a name search, and hasIncompleteInitialScan after an initial scan timeout. Both now go through one isScanning Effect that awaits finder.getScanProgressAsync(), the existing off-thread progress read added by the fff-node patch in #35460 (runInNewThread, shared by concurrent callers per finder). A failed or rejected read counts as still scanning: search marks the result truncated and keeps the symlink scan result only briefly. hasIncompleteInitialScan returns an Effect, so list and search now yield it. The fff patch is unchanged from ff77366. Not re-measured against a large file in this round; the fix relies on getScanProgressAsync already being off-thread.
Fixes #11306
Reproduction on main
Commit 9b5d997 (origin/main). (1) Mechanism probe: @ff-labs/fff-node 0.11.0 from apps/server, FileFinder.create on a 50-file non-git folder with the same options WorkspaceSearchIndex uses; renamed a 6 GB plain-text file whose page cache had been dropped (posix_fadvise DONTNEED) into the folder; the next synchronous finder.mixedSearch("") blocked the Node event loop for 10,794 ms. (2) Real app:
T3CODE_DEV_SEED=0 vp run dev(fresh database), web client driven by headless Chromium (Playwright). Added a non-git project with 50 small files viat3 project add, created a thread in it with the Replay (dev) provider, opened that thread, and typed@srcin the composer (projects.searchEntries -> WorkspaceSearchIndex.search -> mixedSearch). Then moved the uncached 6 GB text file into the project root and typed one more character every ~750 ms, while a loop fetched /api/projects from the server every 200 ms. Result: a single WorkspaceSearchIndex.runSearch span took 9,470 ms. Unrelated RPCs (orchestration.searchThreads, server.refreshProviders, server.reportClientActivity) were queued behind it for up to 9.5 s. The HTTP probe took 9,365 ms (baseline under 210 ms). The composer popup sat on 'Searching workspace files…'. The same steps with a git repository as the project did not stall (searches 5-40 ms, probes under 210 ms), so the freeze shows on non-git roots such as a home directory, as reported.Evidence
Fresh databases, Linux web dev servers, headless Chromium, 1440×900, dark theme, and Replay (dev) on both sides. In matching non-git projects with 50 small files, type
@src, rename an uncached 6 GiB text file into the root, and continue typing every approximately 750 ms while probing/api/projectsevery 200 ms.The fix isolates the slow search from the server event loop. Recordings include an explicitly labeled external measurement display; host information is redacted.
Browser driver · Synthetic seed and setup · Measurements and span durations
Not shown:
Tests
vp test run src/workspace/ src/orchestration-v2/RunFinalizationService.test.ts (apps/server): 6 files, 86 tests passed. vp test run src/workspace/WorkspaceSearchIndex.test.ts, with fakeSymlinkFinder's isScanning now throwing: 10 passed with the fix; with WorkspaceSearchIndex.ts reverted to ff77366, 8 failed. vp run check:changed: unused patches, format, lint, knip 1/2 and 2/2, graphql, typecheck (41 packages), tests (3 files) all passed.
Not in this change
Who did the work
Run by the t3code factory in T3 Code.