Skip to content

perf(server): expanding a wide folder in the file tree takes ~300 ms of git check-ignore in a 20k-file repo (about 9 ms with the same output) #19269

Closed

Description

@bompus

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

  1. Create a repository with about 20,000 tracked files, plus an untracked folder wide/ with 2,000 files, a third matching a .gitignore pattern.
  2. Time printf '%s\0' wide/* | git -c core.fsmonitor=false check-ignore -z --stdin against the same command with --no-index.
  3. 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

  1. juliusmarminge commented on Oct 14, 2026

    @juliusmarminge
    Member

    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 to git check-ignore -z --stdin in 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 (mostly node_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-files prefilter, check whether a simpler short-circuit covers the common case: if the expanded directory is itself ignored and has no tracked descendants (one ls-files call), mark every child as ignored and skip check-ignore entirely. Use the prefilter plus --no-index only 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementRequested improvement or new capability.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions