perf: cap usage worktree canonicalization (#4263)

This commit is contained in:
Neil 2026-05-31 10:11:51 -07:00 committed by GitHub
parent c56f151cb6
commit 0856df806f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 77 additions and 15 deletions

View File

@ -7,6 +7,7 @@ import type { Repo } from '../../shared/types'
import { areWorktreePathsEqual } from '../ipc/worktree-logic'
import { getOrcaManagedCodexHomePath, getSystemCodexHomePath } from '../codex/codex-home-paths'
import { getLegacyCopiedCodexSessionBridgeScanPreference } from '../codex/codex-session-bridge'
import { canonicalizeUsageWorktreePaths } from '../usage-worktree-canonicalizer'
import type {
CodexUsageAttributedEvent,
CodexUsageDailyAggregate,
@ -429,14 +430,7 @@ function localDayFromTimestamp(timestamp: string): string | null {
async function buildWorktreesWithCanonicalPaths(
worktrees: CodexUsageWorktreeRef[]
): Promise<(CodexUsageWorktreeRef & { canonicalPath: string })[]> {
const canonicalized = await Promise.all(
worktrees.map(async (worktree) => ({
...worktree,
canonicalPath: await canonicalizePath(worktree.path)
}))
)
return canonicalized.sort((left, right) => right.canonicalPath.length - left.canonicalPath.length)
return canonicalizeUsageWorktreePaths(worktrees, canonicalizePath)
}
function isContainingPath(candidatePath: string, targetPath: string): boolean {

View File

@ -6,6 +6,7 @@ import { isAbsolute, join, posix, win32 } from 'path'
import type { Repo } from '../../shared/types'
import { areWorktreePathsEqual } from '../ipc/worktree-logic'
import Database from '../sqlite/sync-database'
import { canonicalizeUsageWorktreePaths } from '../usage-worktree-canonicalizer'
import type {
OpenCodeUsageAttributedEvent,
OpenCodeUsageDailyAggregate,
@ -441,13 +442,7 @@ function isContainingPath(candidatePath: string, targetPath: string): boolean {
async function buildWorktreesWithCanonicalPaths(
worktrees: OpenCodeUsageWorktreeRef[]
): Promise<(OpenCodeUsageWorktreeRef & { canonicalPath: string })[]> {
const canonicalized = await Promise.all(
worktrees.map(async (worktree) => ({
...worktree,
canonicalPath: await canonicalizePath(worktree.path)
}))
)
return canonicalized.sort((left, right) => right.canonicalPath.length - left.canonicalPath.length)
return canonicalizeUsageWorktreePaths(worktrees, canonicalizePath)
}
async function canonicalizePath(pathValue: string): Promise<string> {

View File

@ -0,0 +1,37 @@
import { describe, expect, it } from 'vitest'
import {
USAGE_WORKTREE_CANONICALIZATION_CONCURRENCY,
canonicalizeUsageWorktreePaths
} from './usage-worktree-canonicalizer'
describe('canonicalizeUsageWorktreePaths', () => {
it('caps concurrent canonicalization work and preserves longest-path-first ordering', async () => {
let active = 0
let maxActive = 0
const seenPaths: string[] = []
const worktrees = Array.from(
{ length: USAGE_WORKTREE_CANONICALIZATION_CONCURRENCY + 3 },
(_value, index) => ({
path: `/repo/${index}`,
worktreeId: `worktree-${index}`
})
)
const result = await canonicalizeUsageWorktreePaths(worktrees, async (path) => {
active++
maxActive = Math.max(maxActive, active)
seenPaths.push(path)
await new Promise((resolve) => setTimeout(resolve, 0))
active--
return path.endsWith('/10') ? `${path}/nested/longer` : path
})
expect(maxActive).toBeLessThanOrEqual(USAGE_WORKTREE_CANONICALIZATION_CONCURRENCY)
expect(seenPaths).toEqual(worktrees.map((worktree) => worktree.path))
expect(result[0]).toMatchObject({
path: '/repo/10',
canonicalPath: '/repo/10/nested/longer'
})
})
})

View File

@ -0,0 +1,36 @@
export const USAGE_WORKTREE_CANONICALIZATION_CONCURRENCY = 8
export type CanonicalizedUsageWorktree<T extends { path: string }> = T & {
canonicalPath: string
}
export async function canonicalizeUsageWorktreePaths<T extends { path: string }>(
worktrees: readonly T[],
canonicalizePath: (path: string) => Promise<string>,
concurrency = USAGE_WORKTREE_CANONICALIZATION_CONCURRENCY
): Promise<CanonicalizedUsageWorktree<T>[]> {
if (worktrees.length === 0) {
return []
}
// Why: usage scans can see many stale remembered worktrees. Bound realpath
// fanout so opening the usage pane does not stampede the filesystem.
const workerCount = Math.min(worktrees.length, Math.max(1, Math.floor(concurrency)))
const canonicalized = Array.from<CanonicalizedUsageWorktree<T>>({ length: worktrees.length })
let nextIndex = 0
async function worker(): Promise<void> {
while (nextIndex < worktrees.length) {
const index = nextIndex
nextIndex++
const worktree = worktrees[index]
canonicalized[index] = {
...worktree,
canonicalPath: await canonicalizePath(worktree.path)
}
}
}
await Promise.all(Array.from({ length: workerCount }, worker))
return canonicalized.sort((left, right) => right.canonicalPath.length - left.canonicalPath.length)
}