Skip to content

perf(server): a checkpoint capture starts 8 git processes instead of 10 - #21315

Open
josephv123 wants to merge 1 commit into
pingdotgg:mainfrom
josephv123:fix/checkpoint-fewer-git-processes
Open

josephv123 wants to merge 1 commit into
pingdotgg:mainfrom
josephv123:fix/checkpoint-fewer-git-processes

Conversation

@josephv123

Copy link
Copy Markdown
Contributor

Problem

Every checkpoint capture starts 10 git processes one after another, and three of them are git rev-parse lookups that one process can answer. Captures run at the start and end of every turn, under a per-worktree lock, so a turn waits for each process to start and exit. On Linux each start also blocks the server's event loop for longer as the server's memory grows (libuv uses fork(); about 23 ms per start at 513 MB RSS, #21289). In #21289 a maintainer confirmed the T3 side: "a checkpoint capture on the common path runs 10 git processes in sequence".

From the trace of the profiling run in #21289 (scratch server at origin/main 1b5c2d1, 6 concurrent Claude threads with 4 short turns each; since then the capture has only gained -c flags):

  • All 98 captures ran exactly 10 git processes: rev-parse --git-common-dir, rev-parse --verify HEAD, config --bool core.sparseCheckout, rev-parse --path-format=absolute --git-path index, read-tree --reset HEAD, ls-files, add -A, write-tree, commit-tree and update-ref.
  • A capture took 325 ms (p50), and the time inside those processes added up to 311 ms (p50). Each of the three rev-parse lookups took 11–12 ms (p50), almost all of it starting and stopping the process.
  • Captures started 980 of the session's 1,325 git processes. A turn started about 27 in all; with this change it would start about 23.

Change

One git rev-parse --git-common-dir --path-format=absolute --git-path index --verify --quiet HEAD replaces the three lookups in apps/server/src/vcs/GitVcsDriver.ts. It prints the same values the separate commands did:

  • --git-common-dir comes before --path-format, so it keeps its default form: .git, ../../.git from a subdirectory, or an absolute path in a linked worktree. The capture resolves that string as before.
  • --git-path index after --path-format=absolute prints the same absolute index path as the old lookup.
  • --verify prints HEAD's oid after both paths. With --quiet, an unborn HEAD exits 1 after printing only the paths, so exit 1 still means "no commit yet".

A capture now starts 8 git processes instead of 10, or 6 instead of 7 with an unborn HEAD. Everything after the lookups is unchanged: the sparse checkout, skip-worktree, split index and nested repository handling, the durability flags, and the timeouts (the three lookups used the default 30 s, and so does the new one).

If the output isn't exactly two paths and an oid, the capture reads the paths with the old commands:

  • Git before 2.31 doesn't know --path-format and echoes it as an extra line. Its old index lookup also printed --path-format=absolute plus a relative path, which the capture can't stat, so it rebuilt the index from HEAD; it still does. HEAD's existence comes from the exit code, so Git 2.30 still starts 9 processes per capture, as before.
  • A repository path containing a newline spans lines; the old commands read it as before.
  • Any other exit (not a repository, dubious ownership) re-runs rev-parse --git-common-dir, which fails with the same error as before.

I left the core.sparseCheckout probe alone. Caching it per repository would save one more process, but Git can read that value from the repository's config or config.worktree (where git sparse-checkout writes it once extensions.worktreeConfig is on), global and system config, include.path files, includeIf "onbranch:…" sections that change with HEAD, and GIT_CONFIG_* variables. A stale value changes whether git add gets --sparse and how skip-worktree entries are read, so it needs its own design.

Scope and approval

This is a focused performance fix inside the existing capture: three read-only lookups become one, with no change to what a checkpoint contains, which refs it writes, or the flags Git writes them with. #21289 established the problem, and the maintainer's comment linked above confirms the 10-process capture.

Verification

Tests, Git 2.53.0 on Linux:

  • cd apps/server && vp test run src/vcs/GitVcsDriver.test.ts src/checkpointing/CheckpointStore.test.ts: 76 passed. The 72 existing tests are unchanged; 4 are new.
    • "checkpoint capture reads the repository with one rev-parse (HEAD=committed)" and "(HEAD=unborn)" record every git subcommand a capture starts and pin the 8- and 6-command sequences. They also check the captured file and that the checkpoint's parent is HEAD (or that it has none for an unborn HEAD). On origin/main both fail, recording 10 and 7 commands.
    • "checkpoint capture outside a repository fails with the common dir lookup error" and "checkpoint capture handles a repository path containing a newline" (skipped on Windows) cover the fallback. Both also pass on origin/main, which shows the fallback keeps today's behavior: the same error, the same 10 commands, the same checkpoint, and an untouched user index.
  • cd apps/server && vp run --no-cache typecheck: only the two existing Duplicate identifier 'Path' errors in GrokAdapterV2.test.ts, which main also has.
  • vp lint and vp fmt --check on both changed files: clean.

Older Git: I built Git 2.30.9 from the kernel.org tarball and ran both suites with it first on PATH, once on origin/main with the new tests added and once on this branch. The per-test results match exactly: 48 passed and 28 failed on each. The failures are:

  • 20 sparse checkout cases whose fixtures run sparse-checkout set --sparse-index (Git 2.32+);
  • 2 listAuthoredPaths cases that use log --diff-merges=remerge (Git 2.36+);
  • 3 tests that rely on index reuse, which Git before 2.31 never reaches;
  • the 3 new tests that pin the modern command sequence.

A capture on Git 2.30.9 runs the same 9 processes in the same order on both.

Benchmark (scratch test, not committed): CheckpointStore.captureCheckpoint through the real VcsDriverRegistry and VcsProcess layers, with git processes counted at ProcessRunner. The setup:

  • Repositories on ext4 (Ryzen 7 8840HS, Node 26.12), one file changed before each capture.
  • 3 warm-up captures, then 20 (50-file repository) or 10 (t3code checkout) measured captures per round.
  • 3 rounds alternating main and this branch.
  • The test process's RSS raised with filled buffers.

Each cell is the median of the three rounds' p50 wall time, with the range across rounds:

repository RSS main (10 processes) this branch (8 processes) difference
50 files ~180 MB 62.0 ms (60.5–62.0) 55.1 ms (54.7–58.0) −6.9 ms (−11%)
50 files ~500 MB 175.2 ms (173.4–179.5) 145.6 ms (144.8–164.3) −29.6 ms (−17%)
50 files ~1 GB 322.3 ms (318.4–330.2) 276.3 ms (270.6–312.9) −46.0 ms (−14%)
t3code, 28,271 files ~195 MB 214.5 ms (211.2–217.2) 209.9 ms (206.7–213.2) −4.6 ms (−2%)
t3code, 28,271 files ~510 MB 320.3 ms (319.9–339.3) 293.4 ms (288.2–296.9) −26.9 ms (−8%)
t3code, 28,271 files ~1 GB 474.5 ms (473.5–496.5) 408.8 ms (407.9–414.9) −65.7 ms (−14%)

The event loop's busy time per capture (performance.eventLoopUtilization()) fell by about the same amount, for example from 305 to 243 ms on the t3code checkout at ~1 GB, so the time saved is mostly the main thread blocked while starting processes. Before measuring the t3code checkout I ran git status in it so its index wasn't racily clean. Straight after a checkout, every capture re-hashes most files (about 1.3 s per capture), which hides the difference.

Not checked: macOS and Windows; Git versions other than 2.53.0 and 2.30.9; a fresh multi-thread server profile with this branch.

Written by Claude Opus 5.5 in Claude Code, orchestrated from T3 Code.

Every capture looked up the common git dir, whether HEAD resolves and the
absolute index path with three separate `git rev-parse` processes. One
`rev-parse --git-common-dir --path-format=absolute --git-path index
--verify --quiet HEAD` prints all three, so a capture now runs 8 git
processes (6 with an unborn HEAD) instead of 10 (7). Each process start
blocks the server's event loop for longer as its memory grows: at about
1 GB RSS a capture finished 46-66 ms sooner.

When the output is not the expected two paths and HEAD's oid (Git before
2.31 echoes --path-format, and a path with a newline spans lines), the
paths are read with the previous commands; a failed lookup still reports
the same error.
@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Oct 18, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at edde2b3

Macroscope's review found this PR approvable — This is a contained performance fix that reduces redundant Git processes during existing checkpoint captures while preserving the existing capture and fallback behavior. The accompanying tests cover the main and compatibility paths, and no product defaults, APIs, schemas, or sensitive areas are changed.

You can add or adjust custom eligibility rules. Learn more.

@coderabbitai

coderabbitai Bot commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Path: .coderabbit.config.ts
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a2202a15-42f2-4650-879e-da4e20894379
📥 Commits

Reviewing files that changed from the base of the PR and between eb7b35a and edde2b3.

📒 Files selected for processing: 2
  • apps/server/src/vcs/GitVcsDriver.test.ts
  • apps/server/src/vcs/GitVcsDriver.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 5 remain after this review.

📝 Walkthrough

Checkpoint capture now resolves the Git common directory, index path, and HEAD status together when possible. It uses fallback lookups when the combined result does not match the expected format. New tests cover committed and unborn repositories, errors outside a repository, and repository paths containing newlines.

Changes: Git checkpoint capture

Layer / File(s) Summary
Resolve repository details during capture
apps/server/src/vcs/GitVcsDriver.ts
Capture obtains the common directory, index path, and HEAD existence through a combined Git command when its output matches the expected format. Otherwise, it uses fallback lookups. Index reuse retains its timestamp validation and copy behavior.
Test checkpoint capture cases
apps/server/src/vcs/GitVcsDriver.test.ts
Tests record Git commands and verify capture results for committed and unborn repositories, an error outside a repository, and a repository path containing a newline.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Refactor

Suggested reviewers: im-kvijay

Merge Risk: ⚪ Minimal · up to edde2

Checkpoint capture retains fallback behavior for unexpected Git output. No actionable issue remains before merge.

Pre-merge checks: Passed 4
Check name Status Explanation
Description check Passed The description covers the required Problem, Change, Scope and approval, and Verification sections. It explains the issue, implementation, compatibility behavior, tests, benchmarks, known failures, an…
Title check Passed The title clearly identifies the performance change and the reduction from 10 to 8 Git processes during checkpoint capture. It is concise and uses a conventional commit-style prefix.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description covers the required Problem, Change, Scope and approval, and Verification sections. It explains the issue, implementation, compatibility behavior, tests, benchmarks, known failures, and untested environments.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

This branch has not been deployed

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

Labels

size:M 30-99 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant