Skip to content

v2.55 regression: cache_tree_fully_valid() makes branch-changing checkout ~40s on a large full index (even for same-commit switches) #974

Description

@tyrielv

Summary

Since v2.55, a branch-changing git checkout on a large full index has become dramatically slower. Almost the entire cost is in cache_tree_fully_valid(). The slowdown is independent of how many paths change: switching between two branches that point at the same commit (zero working-tree updates, nr_unpack_entries=0) takes ~40s. The same operation on v2.54 completes in <1s.

Environment

  • Microsoft Git v2.55.0.vfs.0.3 (also reproduces on v2.55.0.vfs.0.6), Windows x64
  • Large repository — index has ~2.4M entries (~180 MB full index)
  • Reproduced in two independent setups (see below)

Steps to reproduce

  1. In a repo with a very large index, create two branches at the same commit:
    git branch branchA
    git branch branchB
    
  2. Alternate checking them out:
    git checkout branchA
    git checkout branchB
    
    Each switch moves HEAD to a different ref but changes no files (identical trees).

Expected

Sub-second — nothing is written to the working tree.

Actual

~40–44s per switch on v2.55. The same steps on v2.54 finish in <1s (verified by direct A/B on the same repository).

Evidence — GIT_TRACE2_PERF, exactly one checkout (wall-clock ~44s)

Confirmed a single git process (one cmd_name = checkout, no child git processes):

Region t_rel (s)
cache_tree … fully_valid (call 1, inside unpack_trees) ~20
cache_tree … fully_valid (call 2, during index write) ~19
unpack_trees … check_updates ~0.0001
unpack_trees/nr_unpack_entries 0
cache-tree.c:262  region_enter  cache_tree  ....label:fully_valid
cache-tree.c:264  region_leave  19.919597   cache_tree  ....label:fully_valid   (inside unpack_trees)
unpack-trees.c:2147 data  unpack_trees/nr_unpack_entries:0
cache-tree.c:262  region_enter  cache_tree  label:fully_valid
cache-tree.c:264  region_leave  18.359747   cache_tree  label:fully_valid       (during index write)

In our repro cache_tree_fully_valid() is invoked twice per checkout (~20s + ~19s ≈ ~40s of the ~44s total); check_updates — the phase parallel checkout speeds up — is ~0.

Two independent reproductions

  1. GVFS/VFS-for-Git enlistment (virtual filesystem, ~183 MB index): ~39–44s per switch.

  2. Plain scalar clone of the same repo (no virtual filesystem, ~179 MB index): ~44s per switch, same trace shape (2× fully_valid, nr_unpack_entries=0).

    Important: a default scalar clone uses a sparse index, and with the sparse index the checkout is fast — the regression does not appear. It only reproduces after disabling the sparse index and expanding to a full index:

    git config index.sparse false
    git read-tree HEAD     # materialize the full ~2.4M-entry index
    

    This localizes the cost to full-index cache-tree validation, and shows it is not specific to GVFS.

What we ruled out

  • Parallel checkout / core.fscache: checkout.workers=0 (parallel) vs =1 (sequential) are identical (~38–44s each). check_updates is ~0, so parallelizing working-tree writes changes nothing.
  • Amount of change: nr_unpack_entries=0 — the cost is a fixed per-checkout tax, not proportional to changed paths, and is worst for same-commit switches.
  • GVFS virtual filesystem: also reproduces on a plain scalar clone (full index).
  • Not present in v2.54: verified by direct A/B on the same repository (<1s).
  • Sparse index avoids it: only the full index is affected.

Notes

  • Also present in v2.55.0.vfs.0.6.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions