Cache worktree rows for faster workspace selection (#38479)

* Cache projected worktree rows for faster workspace selection

* Fixed workspace-catalog.ts:171 to key by stable repo/host identity instead of repo object references. Existing revision and scan invalidation remain intact.
This commit is contained in:
Jinjing
2026-11-05 01:22:49 -08:00
committed by GitHub
parent 2257ece2b9
commit e8a78b9250
5 changed files with 123 additions and 42 deletions
@@ -1,4 +1,7 @@
import { joinResolvedWorktreeScans } from './workspaces/resolved-worktree-scan-join'
import {
joinResolvedWorktreeScans,
projectResolvedWorktreeScans
} from './workspaces/resolved-worktree-scan-join'
import { getRepoHostIdentityForParts } from '../../shared/repo-host-identity'
import { WorkspaceGitIdentity } from './sidebar-inputs/workspace-git-identity'
import { describe, expect, it, vi } from 'vitest'
@@ -455,7 +458,7 @@ it('stamps the scan/sidebar join while cached catalog projection stays pure acro
const scans = new Map([
[key, { ok: true as const, worktrees: [gitWorktree('/repo/work')], gitRun: {} }]
])
joinResolvedWorktreeScans(deps.store, () => deps, scans, true)
projectResolvedWorktreeScans(deps.store, () => deps, scans)
expect(writes).not.toHaveBeenCalled()
const first = joinResolvedWorktreeScans(deps.store, () => deps, scans)
expect(writes).toHaveBeenCalledOnce()
@@ -471,6 +474,6 @@ it('stamps the scan/sidebar join while cached catalog projection stays pure acro
identity.observeScan(rows(detached), detached.scannedRepoKeys)
expect(identity.title(detached.worktrees[0]!)).toBe(first.worktrees[0]!.displayName)
expect(writes).toHaveBeenCalledOnce()
joinResolvedWorktreeScans(deps.store, () => deps, scans, true)
projectResolvedWorktreeScans(deps.store, () => deps, scans)
expect(writes).toHaveBeenCalledOnce()
})
@@ -1,4 +1,7 @@
import type { ResolvedWorktree } from './runtime-worktree-path-identity'
import type { RuntimeStore } from './runtime-store-contract'
import type { ResolvedWorktreeRowsJoin } from './workspaces/resolved-worktree-scan-join'
import { WORKTREE_METADATA_DOMAINS } from '../persistence/loading-store/worktree-identity-metadata'
export type ResolvedWorktreeSnapshot = {
worktrees: ResolvedWorktree[]
@@ -6,6 +9,8 @@ export type ResolvedWorktreeSnapshot = {
}
type ResolvedCache = ResolvedWorktreeSnapshot & { expiresAt: number; inventoryRevision: number }
type CachedScanProjection = { inputs: readonly unknown[]; rows: ResolvedWorktreeRowsJoin }
type StoreWrites = { store: RuntimeStore; revision: number }
type ResolvedInFlight = {
generation: number
inventoryRevision: number
@@ -15,6 +20,8 @@ export class RuntimeResolvedWorktreeCache {
private resolved: ResolvedCache | null = null
private resolvedInFlight: ResolvedInFlight | null = null
private resolvedGeneration = 0
private projection: CachedScanProjection | null = null
private storeWrites: StoreWrites | null = null
get generation(): number {
return this.resolvedGeneration
@@ -64,8 +71,54 @@ export class RuntimeResolvedWorktreeCache {
}
}
/** Rows projected from cached scans, reused until a store write, an invalidation, or a changed scan entry; stores that cannot report writes recompute every read. */
projectCachedScans(
store: RuntimeStore | null,
scanEntries: readonly unknown[],
project: () => ResolvedWorktreeRowsJoin
): ResolvedWorktreeRowsJoin {
const revision = this.storeWriteRevision(store)
if (revision === null) {
return project()
}
// Captured before projecting: a metadata migration the read itself saves must miss next time.
const inputs = [revision, ...scanEntries]
const held = this.projection
if (
held &&
held.inputs.length === inputs.length &&
held.inputs.every((input, index) => input === inputs[index])
) {
return held.rows
}
const generation = this.resolvedGeneration
const rows = project()
if (generation === this.resolvedGeneration) {
this.projection = { inputs, rows }
}
return rows
}
private storeWriteRevision(store: RuntimeStore | null): number | null {
if (!store?.onStateWritten) {
return null
}
if (this.storeWrites?.store !== store) {
const writes: StoreWrites = { store, revision: 0 }
this.storeWrites = writes
// Rows read metadata plus repos, lineage and folder workspaces, whose saves name no domain.
store.onStateWritten((domains) => {
if (!domains || WORKTREE_METADATA_DOMAINS.some((domain) => domains.includes(domain))) {
writes.revision += 1
}
})
}
return this.storeWrites.revision
}
invalidateResolved(): void {
this.resolvedGeneration += 1
this.resolved = null
this.projection = null
}
}
@@ -41,14 +41,14 @@ import type { AgentModelCatalogReadParams } from '../../native-chat/agent-model-
import type { RuntimeWorktreeScanCache, RuntimeWorktreeScanInFlight } from '../orca-runtime-core'
import type { RuntimeWorktreeScanResult } from '../repo-worktree-resolution-scan'
import type { ResolvedWorktree } from '../runtime-worktree-path-identity'
import type { ResolvedWorktreeScanJoin } from '../workspaces/resolved-worktree-scan-join'
import type { ResolvedWorktreeRowsJoin } from '../workspaces/resolved-worktree-scan-join'
import type { OrcaRuntimeWithWorktreePsDependencies } from '../workspaces/workspace-process-listing-dependencies'
type ScopeFixture = {
store: RuntimeStore
structuredAgentModelCatalog: OrcaRuntimeWithWorktreePsDependencies['structuredAgentModelCatalog']
resolvedWorktrees: RuntimeResolvedWorktreeCache
projectCachedWorktrees: () => ResolvedWorktreeScanJoin
projectCachedWorktrees: () => ResolvedWorktreeRowsJoin
worktreeScanCache: Map<string, RuntimeWorktreeScanCache>
worktreeScanGenerations: Map<string, number>
worktreeScanInFlight: Map<string, RuntimeWorktreeScanInFlight>
@@ -131,6 +131,8 @@ function rig(agent = 'opencode') {
getWorktreeMeta: (id: string) => metadata[id],
getAllWorktreeLineage: () => ({}),
getAllWorkspaceLineage: () => ({}),
// Production stores report writes; this fixture's in-place edits are followed by invalidation.
onStateWritten: () => () => {},
removeWorktreeLineage: vi.fn(),
setWorktreeMeta: writes
}
@@ -29,7 +29,7 @@ export type ResolvedWorktreeRepoScans = Map<
RepoWorktreeRowsScan & { gitRun: object | null }
>
export type ResolvedWorktreeScanJoin = ResolvedWorktreeSnapshot & {
export type ResolvedWorktreeRowsJoin = Pick<ResolvedWorktreeSnapshot, 'worktrees'> & {
/** Git repos the scans predate, which list no rows until scanned. */
unscannedRepoKeys: ReadonlySet<HostScopedRepoKey>
/** Rows a scan that reached a verdict listed, by their git run; stalled scans remain degraded. */
@@ -38,6 +38,8 @@ export type ResolvedWorktreeScanJoin = ResolvedWorktreeSnapshot & {
scannedRepoKeys: ReadonlySet<HostScopedRepoKey>
}
export type ResolvedWorktreeScanJoin = ResolvedWorktreeSnapshot & ResolvedWorktreeRowsJoin
function resolvedWorktreeRepoScanKey(repo: Repo): HostScopedRepoKey {
return getRepoHostIdentityForParts(repo.id, getRepoExecutionHostId(repo))
}
@@ -82,40 +84,50 @@ export async function scanResolvedWorktreeRepos(
export function joinResolvedWorktreeScans(
store: RuntimeStore | null,
rowDeps: () => RepoWorktreeRowDeps,
scans: ResolvedWorktreeRepoScans,
projectOnly = false
scans: ResolvedWorktreeRepoScans
): ResolvedWorktreeScanJoin {
const repos = store?.getRepos() ?? []
const platformByRepoId = store
? platformsByRepoId(repos, resolveLocalProjectRuntimesForRepos(store, repos))
: new Map<string, NodeJS.Platform>()
return { ...joinRows(store, rowDeps, scans, joinRepoWorktreeRows), platformByRepoId }
}
/** The same rows from memory only: no identity stamping and no launch platforms. */
export function projectResolvedWorktreeScans(
store: RuntimeStore | null,
rowDeps: () => RepoWorktreeRowDeps,
scans: ResolvedWorktreeRepoScans
): ResolvedWorktreeRowsJoin {
return joinRows(store, rowDeps, scans, projectRepoWorktreeRows)
}
function joinRows(
store: RuntimeStore | null,
rowDeps: () => RepoWorktreeRowDeps,
scans: ResolvedWorktreeRepoScans,
rowsForRepo: typeof joinRepoWorktreeRows
): ResolvedWorktreeRowsJoin {
if (!store) {
return {
worktrees: [],
platformByRepoId: new Map(),
unscannedRepoKeys: new Set(),
gitRunById: new Map(),
scannedRepoKeys: new Set()
}
}
const metaById = store.getAllWorktreeMeta() ?? {}
const repos = store.getRepos()
const platformByRepoId = platformsByRepoId(
repos,
resolveLocalProjectRuntimesForRepos(store, repos)
)
const deps = rowDeps()
const unscannedRepoKeys = new Set<HostScopedRepoKey>()
const gitRunById = new Map<HostScopedWorktreeKey, object | null>()
const scannedRepoKeys = new Set<HostScopedRepoKey>()
const perRepoWorktrees = repos.map((repo) => {
const perRepoWorktrees = store.getRepos().map((repo) => {
const key = resolvedWorktreeRepoScanKey(repo)
const scan = scans.get(key) ?? null
if (!scan && !isFolderRepo(repo)) {
unscannedRepoKeys.add(key)
}
const rows = (projectOnly ? projectRepoWorktreeRows : joinRepoWorktreeRows)(
deps,
repo,
scan,
metaById
)
const rows = rowsForRepo(deps, repo, scan, metaById)
if (scan?.ok) {
scannedRepoKeys.add(key)
for (const row of rows) {
@@ -128,5 +140,5 @@ export function joinResolvedWorktreeScans(
perRepoWorktrees.flat(),
store.getAllWorktreeLineage?.() ?? {}
)
return { worktrees, platformByRepoId, unscannedRepoKeys, gitRunById, scannedRepoKeys }
return { worktrees, unscannedRepoKeys, gitRunById, scannedRepoKeys }
}
@@ -15,8 +15,10 @@ import {
import {
joinResolvedWorktreeScans,
platformsByRepoId,
projectResolvedWorktreeScans,
scanResolvedWorktreeRepos,
type ResolvedWorktreeRepoScans,
type ResolvedWorktreeRowsJoin,
type ResolvedWorktreeScanJoin
} from './resolved-worktree-scan-join'
import { projectResolvedWorktreeLineage } from '../../../shared/resolved-worktree-lineage'
@@ -33,7 +35,7 @@ import { withUnregisteredRemovalCheckouts } from '../../workspaces/removal/workt
import { withoutInFlightWorktreeCreates } from '../../workspaces/creation/worktree-create-in-flight'
import { preserveFolderUpgradeWorktreePath } from '../../workspaces/catalog/folder-upgrade-worktree-path'
import type { RuntimeWorktreeScanCache } from '../orca-runtime-core'
import { getRepoHostIdentityForParts } from '../../../shared/repo-host-identity'
import { getRepoHostIdentity } from '../../../shared/repo-host-identity'
import {
resolveWorktreeScanCacheTtlMs,
resolveWorktreeScanRuntime
@@ -158,26 +160,35 @@ export abstract class RuntimeWorkspaceCatalog extends OrcaRuntimeWithReadKittyFi
)
}
protected projectCachedWorktrees(): ResolvedWorktreeScanJoin {
const scans: ResolvedWorktreeRepoScans = new Map()
for (const repo of this.store?.getRepos() ?? []) {
const cached = this.worktreeScanCache.get(`${repo.id}\0${getRepoExecutionHostId(repo)}`)
if (cached) {
scans.set(getRepoHostIdentityForParts(repo.id, getRepoExecutionHostId(repo)), {
...(cached.listing ?? cached.result),
gitRun: cached
})
}
}
return joinResolvedWorktreeScans(
/** The fleet from cached scans for synchronous selector matching, reprojected only when its inputs change. */
protected projectCachedWorktrees(): ResolvedWorktreeRowsJoin {
const cachedScans = (this.store?.getRepos() ?? []).map((repo) => {
const entry = this.worktreeScanCache.get(`${repo.id}\0${getRepoExecutionHostId(repo)}`)
return { repo, entry, scan: entry && (entry.listing ?? entry.result) }
})
return this.resolvedWorktrees.projectCachedScans(
this.store,
() => ({
...this.repoWorktreeRowDeps(),
listFolderWorkspaces: (repo, count) =>
projectRuntimeFolderWorkspaces(this.requireStore(), repo, count)
}),
scans,
true
cachedScans.flatMap(({ repo, entry, scan }) => [getRepoHostIdentity(repo), entry, scan]),
() => {
const scans: ResolvedWorktreeRepoScans = new Map()
for (const { repo, entry, scan } of cachedScans) {
if (entry && scan) {
scans.set(getRepoHostIdentity(repo), {
...scan,
gitRun: entry
})
}
}
return projectResolvedWorktreeScans(
this.store,
() => ({
...this.repoWorktreeRowDeps(),
listFolderWorkspaces: (repo, count) =>
projectRuntimeFolderWorkspaces(this.requireStore(), repo, count)
}),
scans
)
}
)
}