Files
orca/src/main/git/worktree-scan-cache-annotation-reuse.test.ts
T
Neil a7db6c336b perf(git): skip the sparse probe for worktree listings that never read it (#18050)
Three main-process call sites list a repo's worktrees to read `worktree.path`
and nothing else, but went through the annotated listing, so each one paid a
sparse-checkout probe per worktree and cached the result nobody consumed:

- `registered-worktree-roots-cache.ts` rebuilds the filesystem-auth authorized
  roots. `invalidateAuthorizedRootsCache()` fires on every worktree create and
  remove, plus repo add/clone/settings changes, so this reruns constantly.
- `filesystem-source-control-ai-targets.ts` checks whether a local repo owns a
  worktree path.
- `hosted-review.ts` verifies a worktree belongs to the repo before granting
  access.

The probe is an `fs.stat` of the per-worktree `info/sparse-checkout` plus, when
that file is non-empty, a git config read. On a WSL-hosted repo both cross 9p.
#17859 cached it and #17932 keyed that cache on the distro, which fixed a wrong
answer but also meant the distro-less callers above populate a second entry per
worktree — probed cold, revalidated on their own five-minute loop, and read by
nobody. Worktree create/remove clears the sparse cache and dirties the roots
cache together, so both variants go cold at once and the discarded half is
re-probed in full on the next auth check.

`listRepoWorktreeGraph` routes those callers to `listWorktreeGraph`, which
already existed as the annotation-free listing (#17655).

Doing only that would have cost a second `git worktree list`. The scan cache
keys in-flight scans on a `kind`, and graph and lenient were separate kinds, so
a roots rebuild overlapping a sidebar refresh would spawn its own subprocess
where the two previously coalesced. That is a real regression on macOS, Linux
and native Windows, where `getLocalProjectWorktreeGitOptions` returns `{}` and
both callers land on the identical key; on WSL they already differ by distro and
never shared.

So the annotated listing is now the graph listing plus annotation, rather than a
parallel scan of its own: `listWorktrees` awaits `listWorktreeGraph` and
annotates the rows it returns. Both soften a Git failure to `[]`, so they can
share one listing; strict keeps its own because it must be able to reject. The
two kinds ran Git twice before and now run it once, so the overlap case gets
strictly faster instead of paying for the opt-out.

An annotated scan holds two in-flight entries now (its own, plus the graph
listing it shares). Keeping its own entry matters: `detectSparseCheckoutCached`
dedupes revalidation but not the initial fill, so two concurrent badge readers
sharing only the graph scan would both probe.

Per-platform delta:
- macOS/Linux: fewer probes on the three call sites; one `git worktree list`
  instead of two when a graph and an annotated scan overlap.
- native Windows, no WSL: same, and the saved subprocess is the expensive half.
- Windows + WSL: the largest win. The discarded probes were 9p round-trips
  re-paid cold after every worktree create/remove.
- SSH/relay: none. `listRepoWorktreeGraph` returns through the same provider
  branch as `listRepoWorktrees` before reaching local Git.
- folder workspaces: none. Both return the same synthetic folder worktree.

Not in this change:
- The badge listing itself. It still probes, still annotates, and still keys on
  the distro exactly as #17932 left it.
- The remaining `listRepoWorktrees` callers. They read `isSparse`, or feed rows
  to something that does.
2026-09-01 20:19:56 -07:00

94 lines
3.4 KiB
TypeScript

// The annotated listing is the graph listing plus a sparse probe: callers that read only
// `worktree.path` must skip the probe, without costing a second `git worktree list`.
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { GitWorktreeInfo } from '../../shared/worktree/types'
const { detectSparseCheckoutMock, readWorktreeListMock, readTranslatedWorktreeGraphMock } =
vi.hoisted(() => ({
detectSparseCheckoutMock: vi.fn(),
readWorktreeListMock: vi.fn(),
readTranslatedWorktreeGraphMock: vi.fn()
}))
vi.mock('./worktree-sparse-state', () => ({
detectSparseCheckout: detectSparseCheckoutMock,
resolveGitCommonDir: vi.fn()
}))
vi.mock('./worktree-list-reader', () => ({
readCheckedOutBranchRef: vi.fn(),
readRepoCommonDirFromGit: vi.fn(),
readRepoLocation: vi.fn(),
readTranslatedWorktreeGraph: readTranslatedWorktreeGraphMock,
readWorktreeHeadOid: vi.fn(),
readWorktreeList: readWorktreeListMock
}))
import { _resetWorktreeScanCacheForTests, listWorktreeGraph, listWorktrees } from './worktree'
import { __resetSparseCheckoutStateCacheForTests } from './worktree-sparse-checkout-cache'
const REPO = '\\\\wsl.localhost\\Ubuntu\\home\\me\\repo'
const ROW: GitWorktreeInfo = {
path: 'C:\\wt\\x',
head: 'a'.repeat(40),
branch: 'refs/heads/feature',
isBare: false,
isMainWorktree: false
}
describe('graph and annotated worktree scans', () => {
beforeEach(() => {
detectSparseCheckoutMock.mockReset()
detectSparseCheckoutMock.mockResolvedValue(true)
readWorktreeListMock.mockReset()
readWorktreeListMock.mockResolvedValue([ROW])
readTranslatedWorktreeGraphMock.mockReset()
readTranslatedWorktreeGraphMock.mockResolvedValue([ROW])
_resetWorktreeScanCacheForTests()
__resetSparseCheckoutStateCacheForTests()
})
it('does not probe sparse state for a graph scan', async () => {
const rows = await listWorktreeGraph(REPO, { wslDistro: 'Ubuntu' })
expect(rows[0]?.path).toBe('C:\\wt\\x')
expect(rows[0]?.isSparse).toBeUndefined()
expect(detectSparseCheckoutMock).not.toHaveBeenCalled()
})
it('still probes sparse state for the annotated scan', async () => {
const rows = await listWorktrees(REPO, { wslDistro: 'Ubuntu' })
expect(rows[0]?.isSparse).toBe(true)
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(1)
})
it('reads the git listing once for an overlapping graph and annotated scan', async () => {
const [graphRows, annotatedRows] = await Promise.all([
listWorktreeGraph(REPO, { wslDistro: 'Ubuntu' }),
listWorktrees(REPO, { wslDistro: 'Ubuntu' })
])
expect(readTranslatedWorktreeGraphMock).toHaveBeenCalledTimes(1)
expect(graphRows[0]?.isSparse).toBeUndefined()
expect(annotatedRows[0]?.isSparse).toBe(true)
})
// Sharing the listing must not make the probe-free caller wait on the probe it opted out of.
it('resolves a graph scan while the annotated scan is still probing', async () => {
let releaseProbe!: () => void
detectSparseCheckoutMock.mockImplementation(
() =>
new Promise((resolve) => {
releaseProbe = () => resolve(true)
})
)
const annotatedScan = listWorktrees(REPO, { wslDistro: 'Ubuntu' })
const graphRows = await listWorktreeGraph(REPO, { wslDistro: 'Ubuntu' })
expect(graphRows[0]?.path).toBe('C:\\wt\\x')
releaseProbe()
expect((await annotatedScan)[0]?.isSparse).toBe(true)
})
})