The last chapter was about collecting evidence. This one is about using it. With a slow command, a rough baseline, and maybe one or two trace files, you can stop spraying fixes around and start narrowing the problem.
Start With the Symptom, Not the Theory
We've emphasized this consistently: the first diagnostic question is simple (even if the answer is not):
"Which command is slow, and what kind of work is that command doing?"
The command is your first clue about the layer. A tiny baseline pass already tells you a lot:
time git status
time git log -- path/to/file
time git switch main
time git fetch --dry-run
Those four commands touch four different layers: local filesystem and index work, graph and tree history work, local materialization, and network synchronization.
Separate Local Time From Remote Time
When a command crosses the network, break the delay apart before guessing at fixes.
For clone, fetch, and push, the time may include:
- client-side preparation
- server-side negotiation
- hook or policy checks
- transfer time
- local unpack or checkout
When the command crosses the network, start by timing the split:
time git fetch --dry-run
time git fetch
--dry-run does not reproduce every cost, but it is often enough to separate negotiation and server work from later local unpack and checkout behavior.
What To Run First When You Do Not Know Yet
If the problem is still vague, start with one small instrumentation pass instead of jumping straight to fixes:
mkdir -p perf
git count-objects -vH >perf/count-objects.txt
git config --show-origin --get-regexp '^(core\\.fsmonitor|core\\.untrackedCache|index\\.sparse|fetch\\.|maintenance\\.|commitGraph\\.)' >perf/config.txt || true
time git status >/dev/null
GIT_TRACE2_PERF=perf/status.perf git status >/dev/null
That already answers several practical questions:
- is the repository layout obviously unhealthy?
- are key local accelerators enabled?
- is the command slow in a repeatable way?
- does Trace2 show the time staying local rather than disappearing into the network?
If the slow command is fetch or push, swap in the network-oriented version:
time git fetch --dry-run >/dev/null
GIT_TRACE_PACKET=perf/fetch.packet GIT_TRACE2_PERF=perf/fetch.perf git fetch origin >/dev/null
That is enough to get out of guesswork and into evidence quickly.
git status Problems Usually Mean Local Filesystem and Index Cost
If the complaint is status, the likely cost layers are:
- tracked-file validation
- untracked-file discovery
- ignore matching
- index read and write cost
- working tree size
That points you toward the index, sparse-index, untracked cache, fsmonitor, split index, and worktree reduction.
Ask:
- is the working tree too large?
- is untracked discovery the real cost?
- would sparse-checkout or sparse-index reduce the local workload?
- is the repository missing index-side accelerators?
A slow status is usually not a graph problem.
The first evidence pass can stay simple:
time git status
git config --show-origin --get-regexp '^(core\\.fsmonitor|core\\.untrackedCache|index\\.sparse)'
git ls-files --debug | sed -n '1,20p'
Path-Limited History Usually Means Graph and Tree Cost
If the complaint is git log -- path, the likely cost layers are:
- commit walk cost
- path-limited history reasoning
- changed-path Bloom filters
- rename heuristics
That points you toward commit-graph, changed-path Bloom filters, and the history-traversal chapter.
Ask:
- is commit-graph present and current?
- were changed-path Bloom filters written?
- is the query path-limited, rename-heavy, or both?
A path-limited history query shows why command form matters. It is a graph walk with path reasoning and, in some cases, rename heuristics layered on top.
The corresponding probe is equally direct:
time git log -- path/to/file
git commit-graph verify
git config --show-origin --get-regexp '^commitGraph\\.'
Checkout and Branch-Switch Cost Usually Means Materialization
If the complaint is switch, checkout, or branch movement generally, the likely cost layers are:
- working-tree rewrite volume
- index rewrite volume
- sparse versus dense checkout layout
- stash churn and repeated context teardown
That points you toward worktrees, sparse-checkout, sparse-index, and the earlier command-semantics material.
Ask:
- should this really be one mutable checkout?
- would a second worktree remove the churn?
- is the checkout denser than the task requires?
Slow checkout often means too much local materialization, not bad history storage.
Start with the simplest visible command:
time git switch main
git worktree list
git sparse-checkout list
Clone and Fetch Problems Usually Mean Transport
If the complaint is clone or fetch, the likely cost layers are:
- transfer volume
- negotiation cost
- server-side pack generation
- missing bitmaps
- no bundle seeding
- no partial-clone strategy
That points you toward protocol v2, bundle URIs, bitmaps, and partial clone.
Ask:
- is the initial transfer too large?
- is negotiation too expensive?
- would bundle seeding reduce repeated bootstrap cost?
- would blobless partial clone or prefetch change the experience?
Network diagnosis needs its own layer. Many local optimizations do not help a bad clone path.
The first transport pass should usually include:
time git fetch --dry-run
git count-objects -vH
git config --show-origin --get-regexp '^(fetch\\.|remote\\..*promisor|maintenance\\.)'
Typical Fixes Map to Typical Layers
Once the subsystem is known, the fix choices are narrow.
Working-tree and index fixes:
- sparse-checkout
- sparse-index
- worktrees
- untracked cache
- fsmonitor
- split index
History and graph fixes:
- commit-graph
- changed-path Bloom filters
Storage fixes:
- maintenance
- incremental-repack or geometric repack
- MIDX
- bitmaps
Transport fixes:
- partial clone
git backfill- bundle URI
- Scalar prefetch
You can compress that mapping into a quick field guide:
- slow
git statusfirst instrumentation:time git status,git ls-files --debug, Trace2 perf likely fix area: sparse-checkout, sparse-index, fsmonitor, untracked cache - slow
git log -- pathfirst instrumentation:time git log -- path,git commit-graph verifylikely fix area: commit-graph, changed-path Bloom filters, query form - slow
git switchfirst instrumentation:time git switch,git worktree list, sparse state likely fix area: worktrees, sparse-checkout, checkout layout - slow
git fetchfirst instrumentation:time git fetch --dry-run, packet trace, Trace2 perf likely fix area: bitmaps, maintenance, bundle seeding, partial clone, prefetch - huge local disk use first instrumentation:
git count-objects -vH,git-sizerlikely fix area: maintenance, history cleanup, asset policy
Avoid Over-Fixing the Wrong Layer
A common failure mode in Git tuning is technically improving one layer while the visible problem lives somewhere else.
Examples:
- aggressive repack when the real complaint is
status - sparse-checkout when the real complaint is clone time
- bundle seeding when the real complaint is path-limited history
- commit-graph work when the real complaint is untracked-file discovery
The symptom-to-subsystem mapping matters because it protects you from paying for fixes that are real but irrelevant.
A Practical Diagnostic Sequence
When a repository feels slow, a practical sequence looks like this:
- identify the command and workflow that hurt
- decide which Git layer that command primarily touches
- inspect repository layout and current accelerators
- trace the command if the cause is still unclear
- choose one targeted change
- rerun and compare
Worked Example: Slow git status In a Large Checkout
Suppose the complaint is:
"git status takes several seconds in this checkout."
Do not start with repack or clone flags. Start with the local evidence:
time git status >/dev/null
git config --show-origin --get-regexp '^(core\\.fsmonitor|core\\.untrackedCache|index\\.sparse)' || true
git ls-files --debug | sed -n '1,20p'
GIT_TRACE2_PERF=/tmp/status.perf git status >/dev/null
Now read the result in order:
- If repeated
statusruns stay slow, the problem is probably real and local. - If fsmonitor and untracked cache are both absent, missing local accelerators are an obvious first suspect.
- If the checkout is much wider than the task needs, sparse-checkout and sparse-index become the first real candidates.
- If Trace2 shows most of the time inside local index and working-tree regions, stop looking at packfiles and transport.
That leads to much narrower fixes:
- enable fsmonitor if the platform and repo support it
- enable untracked cache if directory scanning is the real bill
- reduce checkout width with sparse-checkout
- consider sparse-index if the repository is large enough for index layout itself to matter
The order matters more than the menu of features: which local layer is slow, and what does the evidence say about it?
Worked Example: Slow git fetch
Now suppose the complaint is:
"Fetch is slow every time, even when the change set is small."
The first pass is different:
time git fetch --dry-run >/dev/null
git count-objects -vH
git config --show-origin --get-regexp '^(fetch\\.|remote\\..*promisor|maintenance\\.)' || true
GIT_TRACE_PACKET=/tmp/fetch.packet GIT_TRACE2_PERF=/tmp/fetch.perf git fetch origin >/dev/null
Now the likely readings are:
- If packet tracing shows a long negotiation or a lot of round trips, transport and reachability are the first place to look.
- If Trace2 shows most local time after transfer, unpack or checkout may be part of the cost.
- If maintenance and bitmap-related preparation are weak on the server side, the client may be paying for poor repository layout upstream.
- If the repository is large and repeatedly fetched, prefetch, bundle seeding, or partial-clone policy may change the user experience more than local checkout tuning.
That usually narrows the next step to one of:
- server-side maintenance and bitmap freshness
- prefetch strategy
- partial clone
- bundle seeding for first-clone pain
- separating transfer cost from later local materialization cost
If traces point to stale server bitmaps, pack layout, or hook cost, stop piling on client config and fix the server-side layer instead.