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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

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 8f5cf9c

opened this

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 from record.ts: same 15-minute window, same limits on samples and keys. To allow that, createPerfRecord now takes an optional check for which keys it keeps; it still defaults to route patterns. A key is git <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 as git other, so no argument, path or ref can end up in the record.
  • Where the time is taken. In src/git/env.ts, runGitRaw and runGitCapture start the clock before waiting for a spawn permit (gitGate.run), so time spent queued counts. That queue is what the 7 s rev-parse points to. In src/git/local.ts, runLocal and the untracked-diff spawn are timed too, because the server also calls local.ts (from actions/runs.ts, actions/sealed.ts and core/carried.ts). The cost per spawn is one performance.now() pair, one Date.now() and a set lookup.
  • Budget and judgement. budgets.ts adds GIT_BUDGET (250 ms). watch.ts adds evaluateGit, 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). startPerfWatcher takes the git record as an optional last argument, so the existing duty tests run unchanged. main.ts passes in the process's git record from perf/live.ts.
  • Docs. docs/features/performance-watcher.md describes 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/runGitCapture record an entry with no secret path or ref in it;
  • a slow rev-parse past 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

Closes #8158

commented

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 from record.ts: same 15-minute window, same limits on samples and keys. To allow that, createPerfRecord now takes an optional check for which keys it keeps; it still defaults to route patterns. A key is git <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 as git other, so no argument, path or ref can end up in the record.
  • Where the time is taken. In src/git/env.ts, runGitRaw and runGitCapture start the clock before waiting for a spawn permit (gitGate.run), so time spent queued counts. That queue is what the 7 s rev-parse points to. In src/git/local.ts, runLocal and the untracked-diff spawn are timed too, because the server also calls local.ts (from actions/runs.ts, actions/sealed.ts and core/carried.ts). The cost per spawn is one performance.now() pair, one Date.now() and a set lookup.
  • Budget and judgement. budgets.ts adds GIT_BUDGET (250 ms). watch.ts adds evaluateGit, 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). startPerfWatcher takes the git record as an optional last argument, so the existing duty tests run unchanged. main.ts passes in the process's git record from perf/live.ts.
  • Docs. docs/features/performance-watcher.md describes 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/runGitCapture record an entry with no secret path or ref in it;
  • a slow rev-parse past 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

Changes requested — before the latest push

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.

  1. 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.
  1. 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.
  1. 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.

commented

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 at GIT_BUDGET, 250 ms. evaluateGit in src/perf/watch.ts uses 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() in src/git/local.ts: the untracked (--no-index) case now records its time in a finally, as runLocal does, so a stream read that throws is still recorded.
  • Docs: the header comment in src/perf/duty.ts is wrapped, and docs/features/performance-watcher.md names 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

commented

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.ts keeps the time of each process, including any wait for a permit, so a queue of git processes shows up as itself. Times are recorded in runGitRaw, runGitCapture, runLocal and the untracked case of diff(), always in a finally. The record key is git <subcommand>, taken from a fixed list, so no argument, path or ref is ever kept; anything else is recorded as git other.
  • Budgets: in src/perf/budgets.ts, gitBudgetFor gives 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. evaluateGit in src/perf/watch.ts uses the subcommand's own budget, and src/perf/duty.ts judges the git record alongside the route record.
  • Tests: test/perf-git.test.ts shows 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

Approved

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.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs