diff --git a/src/main/git/git-upstream-status-read-owner.test.ts b/src/main/git/git-upstream-status-read-owner.test.ts new file mode 100644 index 00000000000..4eecfd035fe --- /dev/null +++ b/src/main/git/git-upstream-status-read-owner.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, it, vi } from 'vitest' +import type { GitPushTarget, GitUpstreamStatus } from '../../shared/types' +import { + GitUpstreamStatusReadOwner, + type GitUpstreamStatusExecutionIdentity +} from './git-upstream-status-read-owner' + +const upstreamStatus: GitUpstreamStatus = { + hasUpstream: true, + upstreamName: 'origin/main', + ahead: 1, + behind: 0 +} + +function deferredPromise(): { + promise: Promise + resolve: (value: T) => void + reject: (error: unknown) => void +} { + let resolve!: (value: T) => void + let reject!: (error: unknown) => void + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve + reject = innerReject + }) + return { promise, resolve, reject } +} + +describe('GitUpstreamStatusReadOwner', () => { + it('shares one live read across ten identical callers', async () => { + const owner = new GitUpstreamStatusReadOwner() + const pending = deferredPromise() + const load = vi.fn(() => pending.promise) + + const reads = Array.from({ length: 10 }, () => + owner.read({ kind: 'native' }, '/repo', undefined, load) + ) + await Promise.resolve() + + expect(load).toHaveBeenCalledTimes(1) + pending.resolve(upstreamStatus) + await expect(Promise.all(reads)).resolves.toEqual( + Array.from({ length: 10 }, () => upstreamStatus) + ) + }) + + it('isolates paths, execution hosts, and every explicit target field', async () => { + const owner = new GitUpstreamStatusReadOwner() + const load = vi.fn().mockResolvedValue(upstreamStatus) + const baseTarget: GitPushTarget = { + remoteName: 'fork', + branchName: 'feature' + } + const cases: [GitUpstreamStatusExecutionIdentity, string, GitPushTarget | undefined][] = [ + [{ kind: 'native' }, '/repo-a', undefined], + [{ kind: 'native' }, '/repo-b', undefined], + [{ kind: 'wsl', distro: 'Ubuntu' }, '/repo-a', undefined], + [{ kind: 'wsl', distro: 'Debian' }, '/repo-a', undefined], + [{ kind: 'native' }, '/repo-a', baseTarget], + [{ kind: 'native' }, '/repo-a', { ...baseTarget, remoteName: 'origin' }], + [{ kind: 'native' }, '/repo-a', { ...baseTarget, branchName: 'other' }], + [ + { kind: 'native' }, + '/repo-a', + { ...baseTarget, remoteUrl: 'https://github.com/example/fork.git' } + ], + [{ kind: 'native' }, '/repo-a', { ...baseTarget, remoteCreated: false }], + [{ kind: 'native' }, '/repo-a', { ...baseTarget, remoteCreated: true }], + [{ kind: 'ssh-provider' }, '/repo-a', baseTarget] + ] + + await Promise.all( + cases.map(([identity, worktreePath, target]) => + owner.read(identity, worktreePath, target, load) + ) + ) + + expect(load).toHaveBeenCalledTimes(cases.length) + }) + + it('runs fresh work after success and rejection', async () => { + const owner = new GitUpstreamStatusReadOwner() + const failure = new Error('upstream failed') + const load = vi + .fn<() => Promise>() + .mockResolvedValueOnce(upstreamStatus) + .mockRejectedValueOnce(failure) + .mockResolvedValueOnce(upstreamStatus) + + await expect(owner.read({ kind: 'native' }, '/repo', undefined, load)).resolves.toBe( + upstreamStatus + ) + await expect(owner.read({ kind: 'native' }, '/repo', undefined, load)).rejects.toBe(failure) + await expect(owner.read({ kind: 'native' }, '/repo', undefined, load)).resolves.toBe( + upstreamStatus + ) + expect(load).toHaveBeenCalledTimes(3) + }) + + it('fences before, during, and after mutation generations', async () => { + const owner = new GitUpstreamStatusReadOwner() + const pendingReads = Array.from({ length: 3 }, () => deferredPromise()) + const load = vi.fn(() => pendingReads[load.mock.calls.length - 1].promise) + + const before = owner.read({ kind: 'native' }, '/repo', undefined, load) + await Promise.resolve() + owner.invalidate() + const during = owner.read({ kind: 'native' }, '/repo', undefined, load) + await Promise.resolve() + owner.invalidate() + const after = owner.read({ kind: 'native' }, '/repo', undefined, load) + await Promise.resolve() + + pendingReads.forEach((pending) => pending.resolve(upstreamStatus)) + await expect(Promise.all([before, during, after])).resolves.toEqual( + Array.from({ length: 3 }, () => upstreamStatus) + ) + expect(load).toHaveBeenCalledTimes(3) + }) +}) diff --git a/src/main/git/git-upstream-status-read-owner.ts b/src/main/git/git-upstream-status-read-owner.ts new file mode 100644 index 00000000000..be297f90e8f --- /dev/null +++ b/src/main/git/git-upstream-status-read-owner.ts @@ -0,0 +1,39 @@ +import type { GitPushTarget, GitUpstreamStatus } from '../../shared/types' +import { InFlightPromiseDedupe, stableInFlightKey } from '../../shared/in-flight-promise-dedupe' + +export type GitUpstreamStatusExecutionIdentity = + | { kind: 'native' } + | { kind: 'wsl'; distro: string } + | { kind: 'ssh-provider' } + +export class GitUpstreamStatusReadOwner { + private readonly inFlightReads = new InFlightPromiseDedupe() + + read( + executionIdentity: GitUpstreamStatusExecutionIdentity, + worktreePath: string, + pushTarget: GitPushTarget | undefined, + load: () => Promise + ): Promise { + const key = stableInFlightKey([ + executionIdentity, + worktreePath, + pushTarget + ? [ + 'explicit-target', + pushTarget.remoteName, + pushTarget.branchName, + pushTarget.remoteUrl ?? null, + pushTarget.remoteCreated ?? null + ] + : ['configured-upstream'] + ]) + return this.inFlightReads.run(key, load) + } + + invalidate(): void { + this.inFlightReads.clear() + } +} + +export const nativeAndWslGitUpstreamStatusReadOwner = new GitUpstreamStatusReadOwner() diff --git a/src/main/git/status.ts b/src/main/git/status.ts index 120c84869d2..3bd825c8622 100644 --- a/src/main/git/status.ts +++ b/src/main/git/status.ts @@ -54,6 +54,7 @@ import type { GitRuntimeOptions } from './git-runtime-options' import { gitOptionsForWorktree } from './git-runtime-options' import { GitStatusReadLeaseOwner } from './git-status-read-lease-owner' import { parseGitRevListFirstParentOid } from '../../shared/git-rev-list-output' +import { nativeAndWslGitUpstreamStatusReadOwner } from './git-upstream-status-read-owner' import { beginGitStatusLineStatsCacheWrite, clearGitStatusLineStatsCache, @@ -99,6 +100,7 @@ const statusReadLeaseOwner = new GitStatusReadLeaseOwner() export function invalidateGitReadCaches(): void { gitDiffReadDedupe.clear() statusReadLeaseOwner.invalidate() + nativeAndWslGitUpstreamStatusReadOwner.invalidate() clearGitStatusLineStatsCache() clearSubmodulePathsCache() resolvedUpstreamNameCache.clear() diff --git a/src/main/git/upstream.test.ts b/src/main/git/upstream.test.ts index 61d288bb160..d0288e4fd12 100644 --- a/src/main/git/upstream.test.ts +++ b/src/main/git/upstream.test.ts @@ -1,4 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' +import type * as NodeFs from 'node:fs' +import path from 'node:path' const { gitExecFileAsyncMock } = vi.hoisted(() => ({ gitExecFileAsyncMock: vi.fn() @@ -8,7 +10,8 @@ vi.mock('./runner', () => ({ gitExecFileAsync: gitExecFileAsyncMock })) -import { getUpstreamStatus } from './upstream' +import { getUpstreamStatus, invalidateGitUpstreamStatusReads } from './upstream' +import { runWithGitReadCacheInvalidation } from './status' const missingTrackingRefError = new Error( "fatal: ambiguous argument 'HEAD@{u}': unknown revision or path not in the working tree.\n" + @@ -19,6 +22,157 @@ const missingTrackingRefError = new Error( describe('getUpstreamStatus', () => { beforeEach(() => { gitExecFileAsyncMock.mockReset() + invalidateGitUpstreamStatusReads() + }) + + it('benchmarks concurrent upstream Git command pressure', async () => { + const benchPath = process.env.ORCA_GIT_UPSTREAM_COALESCING_BENCH_JSON + if (!benchPath) { + return + } + gitExecFileAsyncMock.mockImplementation((args: string[]) => { + if (args[0] === 'symbolic-ref') { + return Promise.resolve({ stdout: 'main\n' }) + } + if (args[0] === 'rev-parse') { + return Promise.resolve({ stdout: 'origin/main\n' }) + } + if (args[0] === 'rev-list') { + return Promise.resolve({ stdout: '2\t3\n' }) + } + if (args[0] === 'log') { + return Promise.resolve({ stdout: '+ abc123 remote work\n' }) + } + throw new Error(`unexpected git args: ${args.join(' ')}`) + }) + + await Promise.all(Array.from({ length: 10 }, () => getUpstreamStatus('/repo'))) + + const commands = gitExecFileAsyncMock.mock.calls.map(([args, options]) => ({ args, options })) + const commandCounts = Object.fromEntries( + ['symbolic-ref', 'rev-parse', 'rev-list', 'log'].map((command) => [ + command, + commands.filter(({ args }) => args[0] === command).length + ]) + ) + const { mkdirSync, writeFileSync } = await vi.importActual('node:fs') + mkdirSync(path.dirname(benchPath), { recursive: true }) + writeFileSync( + benchPath, + JSON.stringify({ + scenario: 'local-git-upstream-concurrent-burst', + concurrentCalls: 10, + physicalGitCalls: commands.length, + commandCounts, + commandChain: ['symbolic-ref', 'rev-parse', 'rev-list', 'log'].map((command) => + commands.find(({ args }) => args[0] === command) + ) + }) + ) + }) + + it('isolates physical reads by worktree, native or WSL host, and every target field', async () => { + gitExecFileAsyncMock.mockImplementation((args: string[]) => { + if (args[0] === 'symbolic-ref') { + return Promise.resolve({ stdout: 'main\n' }) + } + if (args[0] === 'check-ref-format') { + return Promise.resolve({ stdout: '' }) + } + if (args[0] === 'rev-parse' && args.includes('HEAD@{u}')) { + return Promise.resolve({ stdout: 'origin/main\n' }) + } + if (args[0] === 'rev-parse' && args.includes('--verify')) { + return Promise.resolve({ stdout: 'abc123\n' }) + } + if (args[0] === 'rev-list') { + return Promise.resolve({ stdout: '0\t0\n' }) + } + throw new Error(`unexpected git args: ${args.join(' ')}`) + }) + const baseTarget = { remoteName: 'fork', branchName: 'feature' } + + await Promise.all([ + getUpstreamStatus('/repo-a'), + getUpstreamStatus('/repo-b'), + getUpstreamStatus('/repo-a', undefined, { wslDistro: 'Ubuntu' }), + getUpstreamStatus('/repo-a', undefined, { wslDistro: 'Debian' }), + getUpstreamStatus('/repo-a', baseTarget), + getUpstreamStatus('/repo-a', { ...baseTarget, remoteName: 'origin' }), + getUpstreamStatus('/repo-a', { ...baseTarget, branchName: 'other' }), + getUpstreamStatus('/repo-a', { + ...baseTarget, + remoteUrl: 'https://github.com/example/fork.git' + }), + getUpstreamStatus('/repo-a', { ...baseTarget, remoteCreated: false }), + getUpstreamStatus('/repo-a', { ...baseTarget, remoteCreated: true }) + ]) + + expect( + gitExecFileAsyncMock.mock.calls.filter(([args]) => args[0] === 'symbolic-ref') + ).toHaveLength(4) + expect( + gitExecFileAsyncMock.mock.calls.filter(([args]) => args[0] === 'check-ref-format') + ).toHaveLength(6) + }) + + it('runs fresh physical work after a normalized rejection', async () => { + gitExecFileAsyncMock + .mockResolvedValueOnce({ stdout: 'main\n' }) + .mockResolvedValueOnce({ stdout: 'origin/main\n' }) + .mockRejectedValueOnce(new Error('fatal: authentication failed')) + .mockResolvedValueOnce({ stdout: 'main\n' }) + .mockResolvedValueOnce({ stdout: 'origin/main\n' }) + .mockResolvedValueOnce({ stdout: '0\t0\n' }) + + await expect(getUpstreamStatus('/repo')).rejects.toThrow('fatal: authentication failed') + await expect(getUpstreamStatus('/repo')).resolves.toMatchObject({ + hasUpstream: true, + upstreamName: 'origin/main' + }) + expect(gitExecFileAsyncMock).toHaveBeenCalledTimes(6) + }) + + it('uses the common pre/post mutation fence for physical upstream reads', async () => { + const pendingReads: { + promise: Promise<{ stdout: string }> + resolve: (value: { stdout: string }) => void + }[] = [] + gitExecFileAsyncMock.mockImplementation((args: string[]) => { + if (args[0] === 'symbolic-ref') { + let resolve!: (value: { stdout: string }) => void + const promise = new Promise<{ stdout: string }>((innerResolve) => { + resolve = innerResolve + }) + pendingReads.push({ promise, resolve }) + return promise + } + if (args[0] === 'rev-parse') { + return Promise.resolve({ stdout: 'origin/main\n' }) + } + if (args[0] === 'rev-list') { + return Promise.resolve({ stdout: '0\t0\n' }) + } + throw new Error(`unexpected git args: ${args.join(' ')}`) + }) + let finishMutation!: () => void + const mutationGate = new Promise((resolve) => { + finishMutation = resolve + }) + + const before = getUpstreamStatus('/repo') + await vi.waitFor(() => expect(pendingReads).toHaveLength(1)) + const mutation = runWithGitReadCacheInvalidation(() => mutationGate) + const during = getUpstreamStatus('/repo') + await vi.waitFor(() => expect(pendingReads).toHaveLength(2)) + finishMutation() + await mutation + const after = getUpstreamStatus('/repo') + await vi.waitFor(() => expect(pendingReads).toHaveLength(3)) + + pendingReads.forEach(({ resolve }) => resolve({ stdout: 'main\n' })) + await Promise.all([before, during, after]) + expect(pendingReads).toHaveLength(3) }) it('returns upstream and ahead/behind counts when tracking is configured', async () => { diff --git a/src/main/git/upstream.ts b/src/main/git/upstream.ts index d36b5f0a53d..9c4100bf507 100644 --- a/src/main/git/upstream.ts +++ b/src/main/git/upstream.ts @@ -5,11 +5,16 @@ import { getEffectiveGitUpstreamStatus } from '../../shared/git-effective-upstre import { getPublishTargetStatus } from '../../shared/git-publish-target-status' import { gitExecFileAsync } from './runner' import { validateGitPushTarget } from './push-target-validation' +import { nativeAndWslGitUpstreamStatusReadOwner } from './git-upstream-status-read-owner' type GitExecOptions = { wslDistro?: string } +export function invalidateGitUpstreamStatusReads(): void { + nativeAndWslGitUpstreamStatusReadOwner.invalidate() +} + function gitExecOptions( cwd: string, options: GitExecOptions = {} @@ -35,7 +40,7 @@ async function getBehindCommitsArePatchEquivalent( } } -export async function getUpstreamStatus( +async function readUpstreamStatus( worktreePath: string, pushTarget?: GitPushTarget, options: GitExecOptions = {} @@ -71,3 +76,19 @@ export async function getUpstreamStatus( throw new Error(normalizeGitErrorMessage(error, 'upstream')) } } + +export function getUpstreamStatus( + worktreePath: string, + pushTarget?: GitPushTarget, + options: GitExecOptions = {} +): Promise { + const executionIdentity = options.wslDistro + ? ({ kind: 'wsl', distro: options.wslDistro } as const) + : ({ kind: 'native' } as const) + return nativeAndWslGitUpstreamStatusReadOwner.read( + executionIdentity, + worktreePath, + pushTarget, + () => readUpstreamStatus(worktreePath, pushTarget, options) + ) +} diff --git a/src/main/providers/ssh-git-provider.test.ts b/src/main/providers/ssh-git-provider.test.ts index dd3f8590247..dde91d98843 100644 --- a/src/main/providers/ssh-git-provider.test.ts +++ b/src/main/providers/ssh-git-provider.test.ts @@ -1002,6 +1002,125 @@ describe('SshGitProvider', () => { expect(result).toEqual(upstreamResult) }) + it('benchmarks concurrent upstream-status RPC pressure', async () => { + const benchPath = process.env.ORCA_SSH_GIT_UPSTREAM_COALESCING_BENCH_JSON + if (!benchPath) { + return + } + mux.request.mockResolvedValue({ + hasUpstream: true, + upstreamName: 'origin/main', + ahead: 1, + behind: 0 + }) + + await Promise.all( + Array.from({ length: 10 }, () => provider.getUpstreamStatus('/home/user/repo')) + ) + + const upstreamRequests = mux.request.mock.calls.filter( + ([method]) => method === 'git.upstreamStatus' + ) + const { mkdirSync, writeFileSync } = await vi.importActual('node:fs') + mkdirSync(path.dirname(benchPath), { recursive: true }) + writeFileSync( + benchPath, + JSON.stringify({ + scenario: 'ssh-git-upstream-concurrent-burst', + concurrentCalls: 10, + upstreamStatusRequests: upstreamRequests.length, + method: 'git.upstreamStatus', + payload: { worktreePath: '/home/user/repo' } + }) + ) + }) + + it('shares one upstream-status RPC across ten identical callers', async () => { + const pending = deferredPromise<{ + hasUpstream: true + upstreamName: string + ahead: number + behind: number + }>() + mux.request.mockReturnValue(pending.promise) + + const reads = Array.from({ length: 10 }, () => provider.getUpstreamStatus('/home/user/repo')) + await waitForRequestCount(mux.request, 1) + + expect(mux.request).toHaveBeenCalledTimes(1) + pending.resolve({ hasUpstream: true, upstreamName: 'origin/main', ahead: 1, behind: 0 }) + await expect(Promise.all(reads)).resolves.toHaveLength(10) + }) + + it('isolates upstream-status RPCs by worktree and every target field', async () => { + mux.request.mockResolvedValue({ + hasUpstream: true, + upstreamName: 'fork/feature', + ahead: 0, + behind: 0 + }) + const baseTarget = { remoteName: 'fork', branchName: 'feature' } + + await Promise.all([ + provider.getUpstreamStatus('/repo-a'), + provider.getUpstreamStatus('/repo-b'), + provider.getUpstreamStatus('/repo-a', baseTarget), + provider.getUpstreamStatus('/repo-a', { ...baseTarget, remoteName: 'origin' }), + provider.getUpstreamStatus('/repo-a', { ...baseTarget, branchName: 'other' }), + provider.getUpstreamStatus('/repo-a', { + ...baseTarget, + remoteUrl: 'https://github.com/example/fork.git' + }), + provider.getUpstreamStatus('/repo-a', { ...baseTarget, remoteCreated: false }), + provider.getUpstreamStatus('/repo-a', { ...baseTarget, remoteCreated: true }) + ]) + + expect(mux.request).toHaveBeenCalledTimes(8) + }) + + it('keeps upstream-status reads isolated per provider instance', async () => { + const otherProvider = new SshGitProvider('conn-1', mux as never) + mux.request.mockResolvedValue({ + hasUpstream: true, + upstreamName: 'origin/main', + ahead: 0, + behind: 0 + }) + + await Promise.all([ + provider.getUpstreamStatus('/home/user/repo'), + otherProvider.getUpstreamStatus('/home/user/repo') + ]) + + expect(mux.request).toHaveBeenCalledTimes(2) + }) + + it('runs a fresh upstream-status RPC after result and error settlement', async () => { + const failure = new Error('upstream RPC failed') + mux.request + .mockResolvedValueOnce({ + hasUpstream: true, + upstreamName: 'origin/main', + ahead: 0, + behind: 0 + }) + .mockRejectedValueOnce(failure) + .mockResolvedValueOnce({ + hasUpstream: false, + ahead: 0, + behind: 0 + }) + + await expect(provider.getUpstreamStatus('/home/user/repo')).resolves.toMatchObject({ + hasUpstream: true + }) + await expect(provider.getUpstreamStatus('/home/user/repo')).rejects.toBe(failure) + await expect(provider.getUpstreamStatus('/home/user/repo')).resolves.toMatchObject({ + hasUpstream: false + }) + expect(mux.request).toHaveBeenCalledTimes(3) + }) + it('getUpstreamStatus forwards an explicit push target', async () => { const upstreamResult = { hasUpstream: true, upstreamName: 'fork/feature', ahead: 0, behind: 1 } mux.request.mockResolvedValue(upstreamResult) @@ -1432,6 +1551,42 @@ describe('SshGitProvider', () => { expect(mux.request.mock.calls.filter(([method]) => method === 'git.status')).toHaveLength(3) }) + it('fences upstream-status reads before, during, and after an SSH mutation', async () => { + const upstreamRequests = Array.from({ length: 3 }, () => + deferredPromise<{ hasUpstream: false; ahead: 0; behind: 0 }>() + ) + const mutation = deferredPromise() + let upstreamRequestIndex = 0 + mux.request.mockImplementation((method) => { + if (method === 'git.upstreamStatus') { + return upstreamRequests[upstreamRequestIndex++]?.promise + } + if (method === 'git.stage') { + return mutation.promise + } + return Promise.resolve(undefined) + }) + + const beforeMutation = provider.getUpstreamStatus('/home/user/repo') + await waitForRequestCount(mux.request, 1) + const mutating = provider.stageFile('/home/user/repo', 'src/file.ts') + await waitForRequestCount(mux.request, 2) + const duringMutation = provider.getUpstreamStatus('/home/user/repo') + await waitForRequestCount(mux.request, 3) + mutation.resolve(undefined) + await mutating + const afterMutation = provider.getUpstreamStatus('/home/user/repo') + await waitForRequestCount(mux.request, 4) + + upstreamRequests.forEach((pending) => + pending.resolve({ hasUpstream: false, ahead: 0, behind: 0 }) + ) + await Promise.all([beforeMutation, duringMutation, afterMutation]) + expect( + mux.request.mock.calls.filter(([method]) => method === 'git.upstreamStatus') + ).toHaveLength(3) + }) + it.each([ [ 'clone', diff --git a/src/main/providers/ssh-git-provider.ts b/src/main/providers/ssh-git-provider.ts index 7c6e968d7cd..51499f8e8d0 100644 --- a/src/main/providers/ssh-git-provider.ts +++ b/src/main/providers/ssh-git-provider.ts @@ -33,6 +33,7 @@ import { import { InFlightPromiseDedupe, stableInFlightKey } from '../../shared/in-flight-promise-dedupe' import { gitExecMutatesRepository } from '../../shared/git-exec-mutation' import { GitStatusReadLeaseOwner } from '../git/git-status-read-lease-owner' +import { GitUpstreamStatusReadOwner } from '../git/git-upstream-status-read-owner' type NonInteractiveExecQueueEntry = { started: boolean @@ -85,6 +86,7 @@ function filterUntrackedPorcelainStatus(stdout: string | undefined): string | un export class SshGitProvider implements IGitProvider { private readonly gitDiffReadDedupe = new InFlightPromiseDedupe() private readonly statusReadLeaseOwner = new GitStatusReadLeaseOwner() + private readonly upstreamStatusReadOwner = new GitUpstreamStatusReadOwner() private connectionId: string private mux: SshChannelMultiplexer @@ -102,6 +104,7 @@ export class SshGitProvider implements IGitProvider { private invalidateGitReads(): void { this.gitDiffReadDedupe.clear() this.statusReadLeaseOwner.invalidate() + this.upstreamStatusReadOwner.invalidate() } private loggedWorktreeIsCleanFallback = false @@ -507,10 +510,16 @@ export class SshGitProvider implements IGitProvider { worktreePath: string, pushTarget?: GitPushTarget ): Promise { - return (await this.mux.request('git.upstreamStatus', { + return this.upstreamStatusReadOwner.read( + { kind: 'ssh-provider' }, worktreePath, - ...(pushTarget ? { pushTarget } : {}) - })) as GitUpstreamStatus + pushTarget, + async () => + (await this.mux.request('git.upstreamStatus', { + worktreePath, + ...(pushTarget ? { pushTarget } : {}) + })) as GitUpstreamStatus + ) } async pushBranch( diff --git a/tools/benchmarks/results/phase2-git-upstream-inflight-dedupe-2026-07-30.md b/tools/benchmarks/results/phase2-git-upstream-inflight-dedupe-2026-07-30.md new file mode 100644 index 00000000000..759b416aa53 --- /dev/null +++ b/tools/benchmarks/results/phase2-git-upstream-inflight-dedupe-2026-07-30.md @@ -0,0 +1,145 @@ +# Phase 2 Git upstream-status in-flight sharing — 2026-07-30 + +## Decision + +Retain in-flight-only upstream-status sharing at both physical owners. Ten concurrent identical +local callers now share one four-command Git chain, and ten concurrent identical SSH callers share +one `git.upstreamStatus` RPC. No settled result, rejected result, Git command, RPC method, payload, +or max-lines policy changed. + +Checkpoint: `6fee674c48a6dc5742156f70e1fa1363a760f6c6`. + +## Ownership and identity + +`GitUpstreamStatusReadOwner` is the concrete bounded joinability owner. The native/WSL path uses +one module-owned instance, keyed by: + +- native versus the exact WSL distro; +- worktree path; +- configured-upstream versus explicit-target mode; and +- explicit target `remoteName`, `branchName`, `remoteUrl`, and `remoteCreated`, preserving absent + versus explicit `false`. + +Each `SshGitProvider` owns a separate instance, keyed by worktree path and the same complete target +identity. Provider replacement or reconnect therefore cannot reuse another connection +incarnation's work. The shared primitive removes successful and failed entries after settlement +and bounds joinability to 128 entries and 30 seconds; it does not retain a status result. + +The existing local `runWithGitReadCacheInvalidation` and SSH `runWithGitReadInvalidation` wrappers +invalidate upstream joinability before and after every existing Git mutation. Already-issued +promises remain valid, while reads admitted before, during, and after a mutation use separate +generations. + +## A/B physical-call measurement + +The opt-in deterministic benchmarks call the actual local and SSH provider entry points with ten +concurrent identical callers. Counts are taken at the mocked physical `gitExecFileAsync` and mux +request boundaries. + +| Arm | Local physical Git calls | SSH `git.upstreamStatus` RPCs | +| -------------------- | -----------------------: | ----------------------------: | +| A — checkpoint | 40 | 10 | +| B — retained sharing | 4 | 1 | +| Change | -36 | -9 | + +The local command histogram changes from ten of each command to one of each. The command vectors +and execution options are otherwise byte-for-byte equivalent: + +```json +[ + { + "args": ["symbolic-ref", "--quiet", "--short", "HEAD"], + "options": { "cwd": "/repo" } + }, + { + "args": ["rev-parse", "--abbrev-ref", "HEAD@{u}"], + "options": { "cwd": "/repo" } + }, + { + "args": ["rev-list", "--left-right", "--count", "HEAD...origin/main"], + "options": { "cwd": "/repo" } + }, + { + "args": ["log", "--oneline", "--cherry-mark", "--right-only", "HEAD...origin/main", "--"], + "options": { "cwd": "/repo" } + } +] +``` + +The SSH request remains exactly: + +```json +{ + "method": "git.upstreamStatus", + "payload": { "worktreePath": "/home/user/repo" } +} +``` + +When an explicit target exists, the existing `pushTarget` property is still present with the +unchanged object; when absent, the property remains omitted. Existing configured-upstream and +explicit-target tests continue to cover validation, remote tracking probes, URL/created metadata, +ahead/behind counts, patch equivalence, normalized errors, and no-upstream semantics. + +Baseline JSON was archived at `/tmp/git-upstream-inflight-a.json` and +`/tmp/ssh-git-upstream-inflight-a.json`; retained JSON is at +`/tmp/git-upstream-inflight-b.json` and `/tmp/ssh-git-upstream-inflight-b.json`. + +## Production A/B artifacts + +Fresh production builds were archived outside the worktree: + +- A: `/tmp/orca-phase2-git-upstream-a.vQcXwZ/out` +- B: `/tmp/orca-phase2-git-upstream-b-final.OVLh7P/out` + +| Artifact | A raw / gzip | B raw / gzip | Change | A SHA-256 | B SHA-256 | +| ---------------------- | ----------------- | ----------------- | ----------- | ------------------------------------------------------------------ | ------------------------------------------------------------------ | +| Electron main entry | 778,457 / 174,582 | 779,267 / 174,769 | +810 / +187 | `c5992b64d59edfb9719ea2e19b2872f923253686615e53e356aeac6d3139e632` | `64526204d377f8ca634de5940d77b3f2eff1421bd7a8e7346b0ffcc189be21da` | +| Electron preload entry | 130,798 / 20,642 | 130,798 / 20,642 | 0 / 0 | `c388a39cdca9609760e286d95b87ad1e53793720450e507f830ff1f6c5bd259f` | `c388a39cdca9609760e286d95b87ad1e53793720450e507f830ff1f6c5bd259f` | + +The Electron-main raw budget remains 825,109 bytes; B retains 45,842 bytes of headroom. Main, +preload, and renderer file counts remain 184, 1, and 787 respectively. Preload and all renderer +artifacts are byte-identical; the renderer manifest remains byte-identical with SHA-256 +`92ad617f8c8aee6d4c4a23622f524266cbe9c93b3fe65544d1acb833dc3f0d67`. + +Renderer entry measurements are unchanged: + +| Entry | Raw bytes | Gzip bytes | JS | CSS | +| --------------- | --------: | ---------: | --: | --: | +| Renderer index | 8,416,540 | 1,877,823 | 292 | 2 | +| Renderer popout | 4,507,253 | 984,615 | 77 | 2 | +| Renderer web | 4,360,652 | 928,355 | 33 | 1 | + +Complete A and B manifest validation found 778 records, three HTML entries, 6,247 static edges, +213 dynamic edges, and 860 emitted artifact references, with zero missing or escaping paths, zero +cross-entry imports, and zero cycles. + +An initial non-retained wiring arm imported the full upstream implementation into the eager status +module and cost +6,686 main-entry raw bytes. Moving the common fence onto the named owner singleton +reduced the retained cost to +810 bytes without changing invalidation behavior. + +## Validation + +- Baseline provider/upstream suite: 112 passed. +- Focused owner/upstream/provider suite: 124 passed. +- Final owner/upstream/status/provider suite: 215 passed. +- Broad dedupe, upstream, remote, native/WSL status, SSH provider/dispatch, filesystem mutation, + and runtime-routing suite: 465 passed, 1 skipped. +- `pnpm run typecheck:node`: passed. +- Targeted `oxlint --deny-warnings`: passed. +- Targeted `oxfmt --check`: passed. +- `pnpm run check:max-lines-ratchet`: passed with 354 grandfathered suppressions and no new bypass. +- Fresh A and B `pnpm run build:electron-vite`: passed. +- `pnpm run check:electron-bundle-budgets`: passed against fresh B. +- Complete renderer manifest resolution and entry-cycle validation: passed for A and B. +- `git diff --check`: passed. + +## Limitations and residual risk + +- Physical call counts use deterministic mocked Git and mux boundaries. They prove admission, + exact commands, options, method, and payload, not live process startup or SSH latency. +- After the bounded primitive's 30-second joinability window, a still-hung read may be joined by no + new callers and a fresh call may start. Existing callers remain attached to their original work. +- This tranche does not add caller cancellation to the upstream-status API and does not broaden + into a repository snapshot service. +- No packaged live smoke was run across macOS, Linux, Windows, WSL, or SSH hosts. Existing routing, + folder-workspace, relay, and Git 2.25-compatible command suites cover the unchanged boundaries.