Skip to content

[medium] fix(git): stop respawning git outside repos and across session cwds - #631

Merged
sirmalloc merged 1 commit into
sirmalloc:mainfrom
elhoim:perf/git-cache-keys
Oct 9, 2026
Merged

sirmalloc merged 1 commit into
sirmalloc:mainfrom
elhoim:perf/git-cache-keys

Conversation

@elhoim

@elhoim elhoim commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

BLUF

  • Priority: medium. Removes git subprocesses that run on every render and never get cached.
  • Outside a repository: rev-parse --is-inside-work-tree and the origin/upstream remote lookups used to spawn git on every render, because with no git dir there was nothing to cache against. They now return null without spawning, but only when git is certain to fail. Spawns per render (heavy config): 4 → 0.
  • Two sessions in one repo at different cwds (root and a subdirectory) wiped each other's persistent cache on every render. The cache file is now keyed by (git dir, cwd). Spawns per render with the two sessions alternating: 14 → 0 once warm.
  • CPU median per render: -13 % to -19 % on git-heavy configs. stdout is byte-identical.

Details

(a) No repository. getGitRepoMetadata returns null when no .git is found. readPersistentCacheEntry and writePersistentCacheEntry then return early, so a statusline in a non-repo cwd spawns git on every render forever. runGitArgs now skips the spawn only when all of these hold:

  • A cwd was resolved from the payload. With no cwd, git still runs in the process cwd as before.
  • GIT_DIR, GIT_WORK_TREE and GIT_COMMON_DIR are all unset, since any of them can point git elsewhere.
  • No .git entry of any kind (dir, gitfile, malformed gitfile, symlink) and no HEAD (a bare repo, or a cwd inside a git dir) exists anywhere on the physical path (realpath) from cwd up to the filesystem root. Git discovers from getcwd() after the chdir, so a logical path through a symlink could miss a repo that git would find.
  • GIT_CEILING_DIRECTORIES and filesystem boundaries only make git stop earlier, so walking all the way to the root is the conservative direction.

Every command that runGit/runGitArgs issues (rev-parse, status, diff, rev-list, ls-files, remote, symbolic-ref) needs a repository. Where the skip applies, git would exit 128 and the function would return null anyway.

(b) Cache keyed per cwd. The cache file was named after the git dir but stored one file-level cwd. A read with a different cwd missed, and the next write replaced the whole file. Hashing (gitDir, cwd) into the file name gives each session its own file. This also removes the read-modify-write race between them. Sharing entries across cwds would be wrong anyway, because some outputs depend on the cwd: ls-files --unmerged only lists conflicts under it. The existing per-repo files become one orphan per repo and are no longer read.

Measurements

Spawn counts (strace, heavy-ttl0, i.e. TTL 0 so freshness is decided by HEAD/index mtimes only):

scenario before after
non-repo cwd, every render 4 0
root / subdir sessions alternating, every render 14 0 (after first render per cwd)
single cwd, warm 0 0

bench2: round-robin fork/exec, 20 interleaved passes. CPU is user+sys including reaped children. The host was heavily loaded, so compare ratios rather than absolute values.

Run 1: git17-ttl0 (19 git widgets), load1 42-53 on 6 cores.

arm CPU median (ms) CPU p90 wall median
control node -e 0 102 158 680
before, non-repo 1403 1594 8025
after, non-repo 1378 1610 8698
before, repo root (alternating) 1642 1811 9532
after, repo root (alternating) 1422 1543 8626
before, subdir (alternating) 1711 1831 10394
after, subdir (alternating) 1389 1601 8336

Run 2: heavy-ttl0 (heavy config with a transcript, so the git share is smaller), load1 44-64.

arm CPU median (ms) CPU p90 wall median
before, non-repo 1801 2056 12590
after, non-repo 1702 1992 12182
before, root (alternating) 2102 2254 13143
after, root (alternating) 1837 2050 11759
before, subdir (alternating) 2078 2221 13905
after, subdir (alternating) 1818 2060 12185

In the non-repo case the CPU difference is within noise, because a git process that exits immediately with 128 costs little CPU. The gain there is the spawn count and latency.

Checks

  • bun run lint: the full run flagged only two unnecessary casts in the new test, now removed. After the fix, tsc --noEmit is clean and eslint is clean on the changed files.
  • bun run build: OK.
  • bun test: the new and existing git tests pass (59 in git.test.ts). Red before the fix: the two new behaviour tests fail on main. The new tests hide any stray .git/HEAD above the temp fixture, so they do not depend on the host's directory layout.
  • The full suite on this very loaded host shows the same timeout-driven failures on unmodified main as on this branch: fetchUsageData error handling, custom-command capture, and several TUI menu tests such as ColorMenu, which also fails in isolation on main. None of them touch git.
  • Same file as my sibling perf PRs on git.ts (render-scoped cache I/O), which will need a trivial rebase whichever lands first.

Overlap

🤖 Generated with Claude Code

Two cases defeat the persistent git cache and make every render spawn git
again, forever:

- Outside any repository, getGitRepoMetadata finds no git dir, so the
  persistent cache is never read or written and `rev-parse
  --is-inside-work-tree` (plus the remote lookups of the origin/upstream
  widgets) run on every render. runGitArgs now returns null without
  spawning when git is certain to fail: no GIT_DIR/GIT_WORK_TREE/
  GIT_COMMON_DIR in the environment and no `.git` entry or `HEAD` on the
  physical path from cwd up to the root. Every command runGit issues needs
  a repository, so the result is the same null git's exit 128 gave.
- The cache file was keyed by git dir but stored a single file-level cwd,
  so two sessions in one repo at different cwds (root vs a subdirectory)
  discarded each other's entries on every render. The file is now keyed by
  (git dir, cwd). Some outputs are cwd-scoped (`ls-files --unmerged`), so
  sharing entries across cwds would not be correct either. Existing
  per-repo cache files are simply no longer read.

Measured (git spawns per render, heavy config, ttl 0): non-repo cwd 4 -> 0;
root/subdir sessions alternating 14 -> 0 once warm. bench2, 20 interleaved
passes, load 42-53 on 6 cores, CPU median (user+sys incl. children):
git17 root 1642 -> 1422 ms (-13%), git17 subdir 1711 -> 1389 ms (-19%),
heavy root 2102 -> 1837 ms (-13%). stdout is byte-identical.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
whycantfindaname pushed a commit to whycantfindaname/ccstatusline that referenced this pull request Oct 1, 2026
Merged from the jason/beta4-local-fixes Trellis build (compat-repair base):
bounded stdin reads in shared hooks (sirmalloc#590), research dispatch may write the
task research dir (sirmalloc#634), scoped archive commits (sirmalloc#622, sirmalloc#630), list filter
traversal (sirmalloc#631), remove-subtask link check (sirmalloc#632), hooks.local.json ignore
(sirmalloc#633).
@sirmalloc
sirmalloc merged commit 07589a2 into sirmalloc:main Oct 9, 2026
3 checks passed
@elhoim
elhoim deleted the perf/git-cache-keys branch October 10, 2026 22:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants