fix(fs): cap file list and abort untracked scan to prevent session init timeout on large repos - #3408
charlesvien wants to merge 4 commits into
Conversation
…init OOM Large repos (100k+ files, unignored __pycache__ / venvs) caused git ls-files --others to hang for minutes and allocate hundreds of MB in the workspace-server process. The resulting GC pressure starved the concurrent session initializationResult() promise, reliably hitting the 30s timeout when adding a large project. Two-part fix in FsService.listRepoFiles: - Abort git ls-files --others after 8 s via AbortController; fall back to [] - Cap combined tracked + untracked array at 50,000 entries before building the directory tree (avoids ~200MB+ allocation for very large repos) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TpGjPYBD4pgZpsAHjozzsN
… review - Use toBe(50_000) for flat input — zero derived directories means total === cap - Add test documenting that entries total can exceed 50k when nested paths produce extra directory entries via deriveDirectories (cap is on raw files, not on the final files+dirs result) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TpGjPYBD4pgZpsAHjozzsN
|
Merging to
After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here |
|
React Doctor found no issues in the changed files. 🎉 Reviewed by React Doctor for commit |
|
Reviews (1): Last reviewed commit: "consolidate file-list cap and timeout in..." | Re-trigger Greptile |
| listFiles(baseDir, { abortSignal: controller?.signal }).catch( | ||
| (): string[] => [], | ||
| ), | ||
| listUntrackedFiles(baseDir, { abortSignal: controller?.signal }).catch( | ||
| (): string[] => [], |
There was a problem hiding this comment.
When the 8s timer fires while both git commands are still running, the shared abort signal cancels the tracked scan as well as the untracked scan. Both catches then return empty arrays, so listRepoFiles can cache and return an empty repo tree instead of the intended tracked-file fallback.
| const combined = untracked.concat(tracked); | ||
| if (maxFiles !== undefined && combined.length > maxFiles) { | ||
| combined.splice(maxFiles); | ||
| } |
There was a problem hiding this comment.
In a repo with at least 50k untracked generated files, untracked.concat(tracked) fills the cap before any tracked source file is kept. The workspace file list and query path then omit tracked files like package.json even when the tracked scan succeeded.
| const combined = untracked.concat(tracked); | |
| if (maxFiles !== undefined && combined.length > maxFiles) { | |
| combined.splice(maxFiles); | |
| } | |
| const combined = tracked.concat(untracked); | |
| if (maxFiles !== undefined && combined.length > maxFiles) { | |
| combined.splice(maxFiles); | |
| } |
|
Reviews (2): Last reviewed commit: "consolidate file-list cap and timeout in..." | Re-trigger Greptile |
| const [tracked, untracked] = await Promise.all([ | ||
| listFiles(baseDir, { abortSignal: controller?.signal }).catch( | ||
| (): string[] => [], | ||
| ), | ||
| listUntrackedFiles(baseDir, { abortSignal: controller?.signal }).catch( | ||
| (): string[] => [], | ||
| ), | ||
| ]); |
There was a problem hiding this comment.
Shared timeout aborts tracked files When
FsService passes the 8s timeout, this code sends the same abort signal to both git ls-files calls. In a large or cold repo where the tracked scan is still running when the timer fires, both commands reject and both catches return empty arrays. The no-query listRepoFiles path then caches an empty tree for 30 seconds, so the workspace can show no files instead of falling back to tracked files. The timeout should only degrade the untracked scan, or tracked files should be allowed to complete independently.
| const [tracked, untracked] = await Promise.all([ | |
| listFiles(baseDir, { abortSignal: controller?.signal }).catch( | |
| (): string[] => [], | |
| ), | |
| listUntrackedFiles(baseDir, { abortSignal: controller?.signal }).catch( | |
| (): string[] => [], | |
| ), | |
| ]); | |
| const [tracked, untracked] = await Promise.all([ | |
| listFiles(baseDir).catch((): string[] => []), | |
| listUntrackedFiles(baseDir, { abortSignal: controller?.signal }).catch( | |
| (): string[] => [], | |
| ), | |
| ]); |
| const combined = untracked.concat(tracked); | ||
| if (maxFiles !== undefined && combined.length > maxFiles) { | ||
| combined.splice(maxFiles); | ||
| } |
There was a problem hiding this comment.
Untracked files consume cap This builds the capped list with untracked files first. In a repo with 50k or more untracked/generated files, the splice keeps only those entries and drops every tracked source file even when the tracked scan succeeded.
listRepoFiles uses this capped list directly for the file tree and query filtering, so committed files such as package.json or source modules can disappear from the workspace browser and search. Tracked files should be kept first, with untracked files filling any remaining budget.
| const combined = untracked.concat(tracked); | |
| if (maxFiles !== undefined && combined.length > maxFiles) { | |
| combined.splice(maxFiles); | |
| } | |
| const combined = tracked.concat(untracked); | |
| if (maxFiles !== undefined && combined.length > maxFiles) { | |
| combined.splice(maxFiles); | |
| } |
Problem
Session initialization times out after 30s on large repos (100k+ tracked files or big untracked trees like
__pycache__orvenv).FsService.listRepoFilesscans untracked files unbounded during startup, which can allocate hundreds of MB on the workspace-server event loop and starve the init IPC promise past its deadline.Same change as #2819 by @ricardo-leiva, resubmitted from an internal branch because the Trunk merge queue cannot process fork PRs. His commits are preserved here.
Changes
listAllFilesinpackages/workspace-server/src/services/fs/service.tsnow runs the tracked and untracked scans in parallel, aborts the untracked scan after 8s (falling back to tracked files only) and caps the combined list at 50k entries before building the directory tree.How did you test this?
pnpm --filter @posthog/workspace-server test(689 tests) andpnpm --filter @posthog/git test(322 tests) pass locallyAutomatic notifications