perf: cap usage worktree canonicalization (#4263)
This commit is contained in:
parent
c56f151cb6
commit
0856df806f
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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)
|
||||
}
|
||||
Loading…
Reference in New Issue