Repository navigation
Description
What happened
Listing a folder's children (file tree expand, WorkspaceEntries.list with directoryPath) labels ignored entries by piping every child path to git check-ignore -z --stdin. Git consults the index for each path, so the cost grows with the number of children times the size of the tracked index. In a repository with 20,000 tracked files, a folder with 2,000 children (a typical top-level node_modules) costs about 300 ms in that one git process. Small folders are fine: 100 children is about 13 ms.
Diagnosis
apps/server/src/workspace/WorkspaceEntries.ts:330 (upstream main 4d73e1d) runs, in sequential 1,000-path chunks:
git -c core.fsmonitor=false check-ignore -z --stdin
The same input with --no-index skips the index lookups and is about 35 times faster, but it is not a drop-in: it labels force-tracked ignored files, and directories that contain tracked descendants, as ignored. The current command does not.
A cheaper form that keeps the current output: list the tracked children of the folder once (git --literal-pathspecs ls-files --cached -z -- <folder>/), drop those names (and the directory ancestors of tracked descendants), then run check-ignore --no-index on the rest.
Measured with the real commands on throwaway repositories (git 2.55.0, Linux ext4, nice -n 10, median of 7 runs, 2,000-child folder, one third of children match *.cache):
| Repository | Children | Current check-ignore |
Prefilter + --no-index |
|---|---|---|---|
| 20,000 tracked files | 2,000 | 303 ms (294-346) | 9 ms |
For scale, ordinary folders of tracked files in the same repository took 13 ms for 100 children and 33 ms for 200 children with the current command (plain --no-index on those: about 3 ms; the prefilter would not be needed there).
A separate run by another researcher with 20,000 children in a 20,000-file repository measured 2,336 ms against 57 ms (20 git calls down to 4). Output sets matched in all cases, including a force-added ignored file and a directory with a force-added descendant, where plain --no-index differs (667 against 666 paths in my check).
These are asynchronous subprocesses, so this is added latency on the listing request, not an event-loop stall. Complete request and client latency, sparse indexes, and Windows and macOS git behavior are unmeasured.
Steps to reproduce
- Create a repository with about 20,000 tracked files, plus an untracked folder
wide/with 2,000 files, a third matching a.gitignorepattern. - Time
printf '%s\0' wide/* | git -c core.fsmonitor=false check-ignore -z --stdinagainst the same command with--no-index. - Expect about 300 ms against about 8 ms, with identical output when no child is tracked.
Version
upstream main 4d73e1d (the code is unchanged at d765d03)
Environment
Linux 7.2.9 (WSL2), git 2.55.0, Node v26.11.1
Evidence
Scripts and receipts: `bench-ignore.py`, `ignore-correctness.json` and `measurements-ignore.json` from the investigation. I can attach them on request.Related issues
No existing issue or PR found (searched check-ignore, WorkspaceEntries ignore, check-ignore slow, open and closed). Merged #11527 introduced on-demand folder listing; closed #1024 and merged #1873 used chunked --no-index filtering on an older indexing path, which does not address tracked entries.
Fix applied or workaround
None applied. I can open a pull request for this once triage confirms the direction. The branch is not written yet; the change keeps the output identical (tracked-children prefilter, then --no-index on the rest) and adds a test covering a force-tracked ignored file and a directory with tracked descendants.
Filed by
No response
Activity
juliusmarminge commentedon Oct 14, 2026
Note
Grok responding on behalf of Julius.
Confirmed on main, but this is a perf enhancement rather than a bug:
WorkspaceEntries.list(around L324–340) pipes every child togit check-ignore -z --stdinin sequential 1,000-path chunks. The output is correct, and the cost is about 300 ms of async latency, only when expanding very wide folders (mostlynode_modules-scale). Ordinary folders take 13–33 ms by your numbers.A PR is welcome if it stays small and keeps the output identical. Before adding a
ls-filesprefilter, check whether a simpler short-circuit covers the common case: if the expanded directory is itself ignored and has no tracked descendants (onels-filescall), mark every child as ignored and skipcheck-ignoreentirely. Use the prefilter plus--no-indexonly when it measurably helps beyond that. Include the test you described for force-tracked ignored files and for directories with tracked descendants. Benchmark scripts aren't needed in the PR.