mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
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:
@@ -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
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user