Skip to content

fix(core): speed up location boot and VCS routes on Windows - #60569

Open
neriousy wants to merge 1 commit into
devfrom
fewer-git-rounds
Open

neriousy wants to merge 1 commit into
devfrom
fewer-git-rounds

Conversation

@neriousy

Copy link
Copy Markdown
Member

Issue for this PR

No issue: found while profiling the server on Windows.

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

On Windows each git process costs ~35-40 ms, and opening a Location ran several of them back to back while the agent, model, provider and integration lists waited. Plugin activation awaits every state's notify, and two of them do git work: the VCS branch refresh (2 sequential rounds for a clone, 3 without a remote) and the location watcher's repository probe, a rev-parse that project resolution had already answered. That was ~175 ms of the ~200 ms activation flush on reopen.

  • plugin/vcs/git.ts: the default branch reads the remote list together with origin's HEAD, and reads local branches with init.defaultBranch in one more round only when no remote HEAD answers. vcs.status no longer checks HEAD first (diffing an unborn HEAD lists nothing). vcs.diff fetches the batched patch in the same round as the change list, since chunks name their own files, and branch mode reads HEAD and the default branch together.
  • git.ts: repo.discover is cached by the .git metadata fingerprint, as Project.resolve already is, so the location watcher and snapshots reuse project discovery.
  • util/which.ts: the Windows lookup stats once per PATHEXT extension instead of once per case and does not throw on each miss: 14.6 → 6.2 ms for a missing command, which the Azure plugin looks up on every Location boot. A test checks it against which with awkward PATH and PATHEXT values.
  • boot-work.test.ts: a step spawns 8 git processes instead of 9, because snapshot discovery is reused.
  • packages/server/script/benchmark-server.ts (new): boots opencode serve from any checkout with isolated state and replays a client opening a location: open, each endpoint warm, reopen, and /api/info latency while review diffs run, with git spawns per phase. --compare prints before/after.

Windows 11, Ryzen 7 7800X3D, Git for Windows 2.48.1. Median of 8 server launches on a generated 3,000-file repo shaped like a clone, with two edits and a new file:

dev this PR change
reopen a location (boot + client sync) 274.8 ms 176.5 ms −36%
first location open 699.5 ms 576.4 ms −18%
agent/model/provider lists on first open 518 ms 410 ms −21%
vcs.status 132.1 ms 91.0 ms −31%
vcs.diff working (review panel) 221.5 ms 180.6 ms −18%
vcs.diff branch (review panel) 385.3 ms 267.2 ms −31%
git spawns per reopen 4 3
/api/info p99 while diffs run 1.0 ms 1.0 ms

On effect-smol (2,163 files, clean tree): reopen 245.9 → 156.8 ms, vcs.status 88.5 → 50.4 ms, branch diff 247.8 → 178.7 ms. The working diff is flat there (93.6 → 94.5 ms): on a clean tree the batched patch is a wasted parallel process. Starting four git processes at once also makes the worst single event-loop stall during a diff ~4 ms longer; p99 is unchanged.

Not included: starting mingw64\bin\git.exe instead of the cmd\git.exe launcher saves another ~10 ms per git call, but the synchronous part of CreateProcess grows with the image (~3 ms for the 45 KiB launcher, ~8.4 ms for the 4 MiB git.exe) and lands on the server thread. With it, /api/info p99 during diffs went from 1.0 to 9.1 ms.

How did you verify your code works?

  • core, via bun run script/test.ts: boot-work, vcs, git, snapshot, project, worktree, vcs-init, provider-azure, location-watcher-policy, which, shell, formatter and ripgrep (217 pass). server: vcs, worktree, session-diff and process tests.
  • bun run check, and bun run typecheck:tsc in core and server.
  • From packages/server, against a dev worktree: bun run script/benchmark-server.ts --source <dev worktree> --runs 8 --json before.json, then bun run script/benchmark-server.ts --runs 8 --compare before.json. Also with --directory on effect-smol.

Visual

<svg xmlns="http://www.w3.org/2000/svg" width="240" height="72" viewBox="0 0 240 72" fill="none">
  <style>
    text{font:500 9px system-ui,-apple-system,"Segoe UI",sans-serif;fill:currentColor;opacity:.55}
    .k{fill:currentColor;opacity:.55}
    .ok{stroke:currentColor;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;opacity:.8}
    .cur{fill:currentColor;opacity:0}
    @media (prefers-reduced-motion:no-preference){
      .r1,.a{animation:r1 3.6s linear infinite}
      .r2{animation:r2 3.6s linear infinite}
      .r3{animation:r3 3.6s linear infinite}
      .oka{animation:oka 3.6s linear infinite}
      .okb{animation:okb 3.6s linear infinite}
      .cur{animation:cur 3.6s linear infinite}
    }
    @keyframes r1{0%{opacity:.12}1%,28%{opacity:.95}30%,100%{opacity:.55}}
    @keyframes r2{0%,28%{opacity:.12}30%,57%{opacity:.95}59%,100%{opacity:.55}}
    @keyframes r3{0%,57%{opacity:.12}59%,86%{opacity:.95}88%,100%{opacity:.55}}
    @keyframes oka{0%,28%{opacity:0}31%,100%{opacity:.8}}
    @keyframes okb{0%,86%{opacity:0}89%,100%{opacity:.8}}
    @keyframes cur{0%{opacity:.4;transform:translateX(0)}88%{opacity:.4;transform:translateX(156px)}92%,100%{opacity:0;transform:translateX(156px)}}
  </style>
  <text x="6" y="23">before</text>
  <text x="6" y="54">after</text>
  <rect class="k r1" x="46" y="13" width="50" height="13" rx="3"/>
  <rect class="k r2" x="99" y="13" width="50" height="13" rx="3"/>
  <rect class="k r3" x="152" y="13" width="50" height="13" rx="3"/>
  <path class="ok okb" d="M209 19.5l3 3 6-6.5"/>
  <rect class="k a" x="46" y="43" width="50" height="3.4" rx="1.7"/>
  <rect class="k a" x="46" y="47.8" width="50" height="3.4" rx="1.7"/>
  <rect class="k a" x="46" y="52.6" width="50" height="3.4" rx="1.7"/>
  <path class="ok oka" d="M103 50l3 3 6-6.5"/>
  <rect class="cur" x="45" y="8" width="1.5" height="54" rx=".75"/>
</svg>

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Plugin activation awaits the VCS branch refresh and the location watcher's
repository probe, so every Location boot waited on three to four sequential
git rounds before the agent, model, provider and integration lists answered.
Each round costs ~40ms on Windows.

- Read the remote list with origin's HEAD in one round, and local branches
  with init.defaultBranch in one more only when no remote HEAD names the
  default branch.
- Cache repository discovery by the .git metadata fingerprint, as
  Project.resolve already does.
- vcs.status no longer checks HEAD before listing; vcs.diff fetches the
  batched patch with the change list and reads HEAD with the default branch.
- Look up commands on Windows with one stat per PATHEXT extension instead of
  one per case, without throwing on every miss.

Adds packages/server/script/benchmark-server.ts, which boots
`opencode serve` from any checkout and replays a client opening a location.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks sound to me. On Linux, the server benchmark from this PR (base checkout as --source, then --compare) shows git spawns per reopen dropping from 12 to 9 and first open from 6 to 5, and the branch and working diffs are faster. The core boot-work, which, vcs, git, project and snapshot tests pass, and tsc is clean. I couldn't run the Windows-only which lookup or its test, so the Windows timings are taken from the description.

On the code: skipping the HEAD check in vcs.status is safe, because failed git commands come back as empty output, so an unborn HEAD gives an empty stats list. The new discover cache can't store a failure, since the probe never errors and an empty result is removed from the cache. The rewritten which follows the which package's search step by step.

Public API is unchanged; the benchmark is a dev-only script. No blocking concerns.

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant