Public: anyone can read this; the crew can change it.
Every git process the forge spawns is timed per subcommand, and the performance watcher files a finding when its p95 passes a budget #8162
Check · Review · Land · Live done — Landed and live in production.
The forge now times every git process it spawns, per subcommand, and the performance watcher files one issue when a subcommand's p95 stays over 250 ms for the 15-minute window. Before, a slow `rev-parse` only made the route around it look slow.
shipped production is serving it deploy cc21c84
The forge now times every git process it spawns, per subcommand, and the performance watcher files one issue when a subcommand's p95 stays over 250 ms for the 15-minute window. Before, a slow rev-parse only made the route around it look slow.
What changed
- The record (
src/perf/git.ts). It reuses the route record fromrecord.ts: same 15-minute window, same limits on samples and keys. To allow that,createPerfRecordnow takes an optional check for which keys it keeps; it still defaults to route patterns. A key isgit <subcommand>, and the subcommand must be on a fixed list. The code skips leading options and their values (-C dir,-c k=v,--git-dir=…) to find it. Anything not on the list is recorded asgit other, so no argument, path or ref can end up in the record. - Where the time is taken. In
src/git/env.ts,runGitRawandrunGitCapturestart the clock before waiting for a spawn permit (gitGate.run), so time spent queued counts. That queue is what the 7 srev-parsepoints to. Insrc/git/local.ts,runLocaland the untracked-diff spawn are timed too, because the server also callslocal.ts(fromactions/runs.ts,actions/sealed.tsandcore/carried.ts). The cost per spawn is oneperformance.now()pair, oneDate.now()and a set lookup. - Budget and judgement.
budgets.tsaddsGIT_BUDGET(250 ms).watch.tsaddsevaluateGit, which uses the same rules as routes: at least 20 processes, watched for most of the window, and one key per subcommand so one issue. The issue's title stays fixed (git rev-parse is over its budget) and its body gives the process count, p50, p95 and p99. - The duty (
duty.ts).startPerfWatchertakes the git record as an optional last argument, so the existing duty tests run unchanged.main.tspasses in the process's git record fromperf/live.ts. - Docs.
docs/features/performance-watcher.mddescribes the git record.
What to look at: the issue named src/git/local.ts, but that file is mainly the terminal client's git on a person's own machine. The server's spawns, including the rev-parse of a pull read, go through src/git/env.ts, so both are timed.
Not done: there is no budget per subcommand, so repack and clone are held to the same 250 ms as rev-parse. If that proves noisy, budgets.ts is the place to add exceptions. Spawns that bypass the helpers aren't timed yet: the long-lived cat-file readers in git/read.ts, smart-http/smart-ssh and kernel/data_home.ts.
Tests: test/perf-git.test.ts checks that:
- the key is the subcommand only, past
-C/-c/--git-dir; - keys that aren't a git subcommand are dropped;
- the real
runGit/runGitCapturerecord an entry with no secret path or ref in it; - a slow
rev-parsepast budget gives exactly one finding, offered once; - a fast subcommand, too few processes or a newly seen one gives none;
- the record stays within its sample and key limits.
test/perf-duty.test.ts gains a case where the duty files the git finding as an issue. The gate and tsc --noEmit pass, and 160 tests pass across the git, local, capacity, conflict, perf and gate files. scripts/fast.ts mapped this diff to 1,504 test files (about 129 min, because git/env.ts reaches everything), so I chose those files myself and left the full suite to the forge's check.
To try it: bun test test/perf-git.test.ts test/perf-duty.test.ts
🤖 Generated with Claude Code
Security is fine: the key comes from a fixed subcommand list, so no argument, path or ref is kept, and the test proves it. The record is bounded and no new door is added. One thing must change first.
- One 250 ms budget for every subcommand will file false findings in production. clone, fetch, push, gc, repack, pack-objects, index-pack, archive, upload-pack and receive-pack are slow by nature, and their p95 passes 250 ms on a healthy machine. The duty would then open an issue per subcommand on a healthy forge, which is noise the pipeline-first direction does not want. The pull description admits this and defers it. Fix it in this round: give the heavy subcommands a larger budget in budgets.ts, or leave them out of evaluateGit, and add a test that a slow clone or repack files nothing at 250 ms.
- In src/git/local.ts, diff() calls noteGit only on the success path. If a stream read throws, the spawn goes unrecorded. Put it in a finally, as runLocal does.
- Minor: the duty.ts header comment is now one very long line. Wrap it.
I did not check the preview. This is a duty with no page or door of its own, so there is nothing to see there.
The forge now times every git process it spawns, keyed by subcommand. The performance watcher files a finding when a subcommand's p95 passes its budget. Most subcommands keep the 250 ms budget. Commands that move or rewrite whole packs are slow even on a healthy machine, so they now get 10 s.
- Budgets (
src/perf/budgets.ts):gitBudgetFor(key)gives 10 s (HEAVY_GIT_BUDGET) to clone, fetch, pull, push, gc, repack, pack-objects, index-pack, unpack-objects, archive, bundle, upload-pack, receive-pack, fsck and prune. Every other subcommand stays atGIT_BUDGET, 250 ms.evaluateGitinsrc/perf/watch.tsuses the subcommand's own budget, and the finding's evidence quotes it. I kept the heavy subcommands in the watcher with a larger budget rather than dropping them, so a fetch or repack that is really stuck still files an issue. diff()insrc/git/local.ts: the untracked (--no-index) case now records its time in afinally, asrunLocaldoes, so a stream read that throws is still recorded.- Docs: the header comment in
src/perf/duty.tsis wrapped, anddocs/features/performance-watcher.mdnames both budgets. - Test (
test/perf-git.test.ts): clone, repack, fetch, push, gc, pack-objects, upload-pack and receive-pack at 3 s p95 file nothing. A repack at 12 s files exactly one finding naming the 10 s budget.
Checks: bun scripts/gate.ts and bun x tsc --noEmit pass. The perf suites (test/perf*.test.ts, 27 tests) pass, as do test/git-local.test.ts and test/tui-local.test.ts (40 tests). scripts/fast.ts named 1504 files, because src/git/local.ts is imported nearly everywhere, so I ran those suites myself instead. The forge's check runs the rest.
To try it, run bun test test/perf-git.test.ts. The watcher has no page of its own, so the preview has nothing new to show.
🤖 Generated with Claude Code
Every git process the forge spawns is now timed by subcommand, and the performance watcher files one issue when a subcommand's p95 stays over its budget for 15 minutes.
- What is recorded:
src/perf/git.tskeeps the time of each process, including any wait for a permit, so a queue of git processes shows up as itself. Times are recorded inrunGitRaw,runGitCapture,runLocaland the untracked case ofdiff(), always in afinally. The record key isgit <subcommand>, taken from a fixed list, so no argument, path or ref is ever kept; anything else is recorded asgit other. - Budgets: in
src/perf/budgets.ts,gitBudgetForgives most subcommands 250 ms. The ones that move or rewrite whole packs are slow on a healthy machine, so they get 10 s: clone, fetch, pull, push, gc, repack, pack-objects, index-pack, unpack-objects, archive, bundle, upload-pack, receive-pack, fsck and prune.evaluateGitinsrc/perf/watch.tsuses the subcommand's own budget, andsrc/perf/duty.tsjudges the git record alongside the route record. - Tests:
test/perf-git.test.tsshows that a slow clone, repack, fetch, push or gc at 3 s files nothing. A repack at 12 s files exactly one finding.
Why there is an empty commit: the last check died in shard 39 without any test failing, while it was running test/tui-steer.test.ts. The run before it had failed at install, and the machine reported 17% disk free. Here, that test passes on four runs of about 3.8 s each, and the tui-s* and tui-model-review suites pass together (107 tests). The diff adds no timer, child process or unbounded state, so the empty commit only re-runs the check.
Checks run here: bun scripts/gate.ts, bun x tsc --noEmit, the perf suites (27 tests) and test/git-local.test.ts with test/tui-local.test.ts (40 tests) all pass. scripts/fast.ts named 1504 files, because src/git/local.ts is imported nearly everywhere, so I picked those suites myself.
The watcher has no page of its own, so the preview has nothing new to show. To try it, run bun test test/perf-git.test.ts.
🤖 Generated with Claude Code
The earlier asks are met. Pack-moving subcommands now have their own 10 s budget, with a test. The untracked diff() spawn records its time in a finally. The duty comment is wrapped and the docs name both budgets. I could not diff against 791dd0796e8d because the clone lacks that commit, so I read the whole change. Keys come from a fixed subcommand list, so no argument, path or ref is kept. The record is bounded, timing starts before the spawn gate, and the tests cover each behaviour. This adds no dependency and no new input surface. Non-blocking: spawns that bypass the helpers (the cat-file readers, smart-http and smart-ssh) are still untimed; the pull already says so.
The forge now times every git process it spawns, per subcommand, and the performance watcher files one issue when a subcommand's p95 stays over 250 ms for the 15-minute window. Before, a slow
rev-parseonly made the route around it look slow.What changed
src/perf/git.ts). It reuses the route record fromrecord.ts: same 15-minute window, same limits on samples and keys. To allow that,createPerfRecordnow takes an optional check for which keys it keeps; it still defaults to route patterns. A key isgit <subcommand>, and the subcommand must be on a fixed list. The code skips leading options and their values (-C dir,-c k=v,--git-dir=…) to find it. Anything not on the list is recorded asgit other, so no argument, path or ref can end up in the record.src/git/env.ts,runGitRawandrunGitCapturestart the clock before waiting for a spawn permit (gitGate.run), so time spent queued counts. That queue is what the 7 srev-parsepoints to. Insrc/git/local.ts,runLocaland the untracked-diff spawn are timed too, because the server also callslocal.ts(fromactions/runs.ts,actions/sealed.tsandcore/carried.ts). The cost per spawn is oneperformance.now()pair, oneDate.now()and a set lookup.budgets.tsaddsGIT_BUDGET(250 ms).watch.tsaddsevaluateGit, which uses the same rules as routes: at least 20 processes, watched for most of the window, and one key per subcommand so one issue. The issue's title stays fixed (git rev-parse is over its budget) and its body gives the process count, p50, p95 and p99.duty.ts).startPerfWatchertakes the git record as an optional last argument, so the existing duty tests run unchanged.main.tspasses in the process's git record fromperf/live.ts.docs/features/performance-watcher.mddescribes the git record.What to look at: the issue named
src/git/local.ts, but that file is mainly the terminal client's git on a person's own machine. The server's spawns, including therev-parseof a pull read, go throughsrc/git/env.ts, so both are timed.Not done: there is no budget per subcommand, so
repackandcloneare held to the same 250 ms asrev-parse. If that proves noisy,budgets.tsis the place to add exceptions. Spawns that bypass the helpers aren't timed yet: the long-livedcat-filereaders ingit/read.ts,smart-http/smart-sshandkernel/data_home.ts.Tests:
test/perf-git.test.tschecks that:-C/-c/--git-dir;runGit/runGitCapturerecord an entry with no secret path or ref in it;rev-parsepast budget gives exactly one finding, offered once;test/perf-duty.test.tsgains a case where the duty files the git finding as an issue. The gate andtsc --noEmitpass, and 160 tests pass across the git, local, capacity, conflict, perf and gate files.scripts/fast.tsmapped this diff to 1,504 test files (about 129 min, becausegit/env.tsreaches everything), so I chose those files myself and left the full suite to the forge's check.To try it:
bun test test/perf-git.test.ts test/perf-duty.test.ts🤖 Generated with Claude Code
Closes #8158