perf: bound filesystem auth cache rebuild (#4186)

This commit is contained in:
Neil
2026-05-31 07:23:26 -07:00
committed by GitHub
parent 11ada84c25
commit b0a2a96a53
2 changed files with 52 additions and 10 deletions
+24 -2
View File
@@ -32,9 +32,9 @@ const repo: Repo = {
kind: 'git'
}
function makeStore(): Store {
function makeStore(repos: Repo[] = [repo]): Store {
return {
getRepos: () => [repo],
getRepos: () => repos,
getSettings: () => ({})
} as unknown as Store
}
@@ -67,6 +67,28 @@ describe('filesystem auth worktree roots', () => {
)
expect(listRepoWorktrees).toHaveBeenCalledTimes(1)
})
it('bounds concurrent repo probes while rebuilding authorized roots', async () => {
const repos = Array.from({ length: 20 }, (_, index) => ({
...repo,
id: `repo-${index}`,
path: `/repos/app-${index}`
}))
let active = 0
let maxActive = 0
vi.mocked(listRepoWorktrees).mockImplementation(async () => {
active += 1
maxActive = Math.max(maxActive, active)
await new Promise((resolve) => setTimeout(resolve, 1))
active -= 1
return []
})
await rebuildAuthorizedRootsCache(makeStore(repos))
expect(listRepoWorktrees).toHaveBeenCalledTimes(repos.length)
expect(maxActive).toBeLessThanOrEqual(8)
})
})
describe('filesystem-auth path containment', () => {
+28 -8
View File
@@ -16,6 +16,7 @@ const registeredWorktreeRootsByRepo = new Map<string, Set<string>>()
const registeredWorktreeRootRepoIds = new Set<string>()
let registeredWorktreeRootsDirty = true
let registeredWorktreeRootsRefresh: Promise<void> | null = null
const AUTHORIZED_ROOTS_REBUILD_CONCURRENCY = 8
export function authorizeExternalPath(targetPath: string): void {
const resolvedTarget = resolve(targetPath)
@@ -91,11 +92,8 @@ export function isPathAllowed(targetPath: string, store: Store): boolean {
}
export async function rebuildAuthorizedRootsCache(store: Store): Promise<void> {
// Why: repos are processed in parallel so the cache rebuild completes in
// wall-clock time proportional to the slowest single repo, not the sum of
// all repos. The previous sequential loop was the main bottleneck on
// Windows where each `git worktree list` + realpath chain takes 500 ms+
// due to slower process creation and antivirus I/O scanning.
// Why: repos are processed with bounded parallelism so the cache rebuild
// keeps the Windows speedup without spawning one git process per repo.
//
// Why no realpath() here: this rebuild runs on repo/worktree invalidation,
// so canonicalizing every repo root would repeatedly touch TCC-protected
@@ -104,8 +102,10 @@ export async function rebuildAuthorizedRootsCache(store: Store): Promise<void> {
// destructive or read/write operation, so the security boundary remains
// enforced where it matters.
const repos = getLocalRepos(store)
const perProjectResults = await Promise.all(
repos.map(async (repo) => {
const perProjectResults = await mapWithConcurrency(
repos,
AUTHORIZED_ROOTS_REBUILD_CONCURRENCY,
async (repo) => {
const roots: string[] = []
try {
roots.push(resolve(repo.path))
@@ -121,7 +121,7 @@ export async function rebuildAuthorizedRootsCache(store: Store): Promise<void> {
console.warn(`[filesystem-auth] skipping repo ${repo.path} during cache rebuild:`, error)
}
return { repoId: repo.id, roots }
})
}
)
registeredWorktreeRoots.clear()
@@ -139,6 +139,26 @@ export async function rebuildAuthorizedRootsCache(store: Store): Promise<void> {
registeredWorktreeRootsDirty = false
}
async function mapWithConcurrency<T, R>(
items: readonly T[],
maxConcurrent: number,
mapper: (item: T) => Promise<R>
): Promise<R[]> {
const results: R[] = []
let nextIndex = 0
const workerCount = Math.min(maxConcurrent, items.length)
await Promise.all(
Array.from({ length: workerCount }, async () => {
while (nextIndex < items.length) {
const index = nextIndex
nextIndex += 1
results[index] = await mapper(items[index])
}
})
)
return results
}
export function registerWorktreeRootsForRepo(
store: Store,
repoId: string,