Fix untracked line-stat cache thrash and add source-control scale benchmark (#8013)

The untracked line-stat cache capped at 2,048 entries while a git status
scan can carry up to DEFAULT_GIT_STATUS_LIMIT (10,000) untracked entries.
A sequential scan over more files than the cap FIFO-evicted every entry
before the next poll revisited it (~0% hit rate), so every 3s status poll
re-read every untracked file's full contents. Measured with 64KB files:
warm rescan cost per file was 17x higher just past the cap.

- Size the cache to 2x the status entry limit and make eviction LRU
  (delete-before-set on hit and refresh) so a hot worktree's entries
  survive another worktree's scan.
- Add tests/e2e/source-control-large-file-count.spec.ts: a 5-scenario
  Playwright benchmark (pnpm run test:e2e:source-control-scale) that
  reproduces #8013 deterministically — event-loop stall, DOM node count,
  JS heap, and OS-level renderer working set at 5k/9.5k/11k changed files,
  plus a clean-repo control and a cache-effectiveness gate. Scenarios
  asserting bounded row mounting go green with the SourceControl
  virtualization fix (#7619).

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinjing
2026-07-09 22:05:47 -07:00
co-authored by Orca
parent 7a733862cb
commit a54f0bc336
5 changed files with 598 additions and 1 deletions
+1
View File
@@ -82,6 +82,7 @@
"test:e2e:terminal-perf:check-report": "node config/scripts/check-terminal-perf-report-budgets.mjs",
"test:e2e:terminal-perf:summarize": "node config/scripts/summarize-terminal-perf-report.mjs",
"test:e2e:ssh-docker-perf": "node config/scripts/run-ssh-docker-perf-e2e.mjs",
"test:e2e:source-control-scale": "pnpm run ensure:electron-runtime && npx playwright test tests/e2e/source-control-large-file-count.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
"win-update-e2e": "node tools/win-update-e2e/run.mjs",
"test:e2e:ssh-codex-artifacts-repro": "node config/scripts/run-ssh-codex-artifacts-repro-e2e.mjs",
"test:e2e:headful": "pnpm run ensure:electron-runtime && npx playwright test --config tests/playwright.config.ts --project electron-headful",
@@ -135,6 +135,23 @@ describe('collectUntrackedAdditions', () => {
expect(stats.get('cached.ts')).toEqual({ added: 3 })
expect(readFileMock).toHaveBeenCalledTimes(1)
})
it('keeps the cache effective across polls for a status-limit-sized change set', async () => {
// Why: git status caps at DEFAULT_GIT_STATUS_LIMIT (10,000) entries. A
// cache smaller than one scan FIFO-evicts every entry mid-scan, so the
// next poll re-reads every file (#8013). Scan the full limit twice; the
// second pass must be stat-only.
lstatMock.mockResolvedValue(mockFileStat(5, 7))
readFileMock.mockResolvedValue(Buffer.from('a\nb\nc'))
const paths = Array.from({ length: 10_000 }, (_, i) => `poll-scale/file-${i}.ts`)
await collectUntrackedAdditions('/repo', paths)
const firstPassReads = readFileMock.mock.calls.length
await collectUntrackedAdditions('/repo', paths)
expect(firstPassReads).toBe(paths.length)
expect(readFileMock).toHaveBeenCalledTimes(paths.length)
})
})
describe('applyLineStats', () => {
+16 -1
View File
@@ -2,6 +2,7 @@ import { lstat, readFile } from 'node:fs/promises'
import * as path from 'node:path'
import { isBinaryBuffer } from './binary-buffer'
import { decodeGitCQuotedPath } from './git-cquoted-path'
import { DEFAULT_GIT_STATUS_LIMIT } from './git-status-limit'
export type GitLineStats = { added?: number; removed?: number }
@@ -11,7 +12,13 @@ const UNTRACKED_READ_CONCURRENCY = 8
// Keep status polling cheap: large untracked files are commonly generated
// assets, and reading them every poll can stall the source-control sidebar.
export const MAX_UNTRACKED_LINE_COUNT_BYTES = 2 * 1024 * 1024
const UNTRACKED_STATS_CACHE_MAX_ENTRIES = 2048
// Why: the cache must hold at least one full status scan's untracked set
// (capped at DEFAULT_GIT_STATUS_LIMIT entries). A smaller cache is worse than
// none: a sequential scan over more files than the cap evicts every entry
// before the next poll revisits it, so every poll re-reads every untracked
// file's contents (#8013). 2x leaves headroom for a second window polling a
// different worktree; entries are ~200 bytes, so worst case is a few MB.
const UNTRACKED_STATS_CACHE_MAX_ENTRIES = 2 * DEFAULT_GIT_STATUS_LIMIT
const NEWLINE_BYTE = 0x0a
type CachedUntrackedStats = {
@@ -107,6 +114,11 @@ async function countFileAdditions(absolutePath: string): Promise<GitLineStats> {
cached.mtimeMs === fileStat.mtimeMs &&
cached.ctimeMs === fileStat.ctimeMs
) {
// Why: Map eviction below removes the oldest-inserted key; re-inserting
// on hit makes that LRU instead of FIFO, so a hot worktree's entries
// survive another worktree's scan sharing this cache.
untrackedStatsCache.delete(absolutePath)
untrackedStatsCache.set(absolutePath, cached)
return cached.stats
}
if (fileStat.isSymbolicLink()) {
@@ -144,6 +156,9 @@ function rememberUntrackedStats(
fileStat: { size: number; mtimeMs: number; ctimeMs: number },
stats: GitLineStats
): GitLineStats {
// Why: delete-before-set keeps refreshed entries at the recent end of the
// Map's insertion order, preserving the LRU eviction contract.
untrackedStatsCache.delete(absolutePath)
untrackedStatsCache.set(absolutePath, {
size: fileStat.size,
mtimeMs: fileStat.mtimeMs,
+133
View File
@@ -0,0 +1,133 @@
import { execFileSync } from 'node:child_process'
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
import os from 'node:os'
import path from 'node:path'
export type LargeFileCountRepoOptions = {
/** Files committed on the initial commit. */
trackedFiles?: number
/** Files written after the commit and left untracked. */
untrackedFiles?: number
/** Tracked files rewritten after the commit (unstaged modifications). */
modifiedFiles?: number
/** Fan-out per directory; issue #8013 repos are wide trees, not one flat dir. */
filesPerDirectory?: number
/**
* Minimum size of each untracked file. Untracked line stats read full file
* contents (mtime-cached, but the cache caps at 2,048 entries), so per-poll
* I/O cost only shows up when untracked files have realistic sizes.
*/
untrackedFileBytes?: number
}
export type LargeFileCountRepo = {
repoPath: string
trackedFiles: number
untrackedFiles: number
modifiedFiles: number
}
function runGit(repoPath: string, args: string[]): void {
execFileSync('git', args, { cwd: repoPath, stdio: 'pipe' })
}
/**
* Removes a fixture repo, retrying ENOTEMPTY/EBUSY races: the app's git status
* poll can still be writing index.lock (or the watcher holding dirs) while the
* test tears down.
*/
export async function removeLargeFileCountRepo(repoPath: string): Promise<void> {
for (let attempt = 0; attempt < 5; attempt += 1) {
try {
rmSync(repoPath, { recursive: true, force: true })
return
} catch (error) {
if (attempt === 4) {
throw error
}
await new Promise((resolve) => setTimeout(resolve, 250 * (attempt + 1)))
}
}
}
function writeFileTree(
repoPath: string,
rootDirName: string,
fileCount: number,
filesPerDirectory: number,
contentForIndex: (index: number) => string
): void {
let currentDir: string | null = null
for (let index = 0; index < fileCount; index += 1) {
const dirIndex = Math.floor(index / filesPerDirectory)
const dirPath = path.join(repoPath, rootDirName, `dir-${String(dirIndex).padStart(4, '0')}`)
if (dirPath !== currentDir) {
mkdirSync(dirPath, { recursive: true })
currentDir = dirPath
}
writeFileSync(
path.join(dirPath, `file-${String(index).padStart(6, '0')}.ts`),
contentForIndex(index)
)
}
}
/**
* Repro fixture for issue #8013: a repo whose file count (tracked, untracked,
* or modified) is large enough to stress every per-file code path — the
* streamed `git status` scan, per-entry line stats, the IPC payload, and the
* Source Control row rendering.
*/
export function createLargeFileCountRepo(
options: LargeFileCountRepoOptions = {}
): LargeFileCountRepo {
const trackedFiles = options.trackedFiles ?? 0
const untrackedFiles = options.untrackedFiles ?? 0
const modifiedFiles = Math.min(options.modifiedFiles ?? 0, trackedFiles)
const filesPerDirectory = options.filesPerDirectory ?? 100
const repoPath = realpathSync(mkdtempSync(path.join(os.tmpdir(), 'orca-large-file-count-')))
runGit(repoPath, ['init'])
runGit(repoPath, ['config', 'user.email', 'e2e@test.local'])
runGit(repoPath, ['config', 'user.name', 'E2E Test'])
runGit(repoPath, ['config', 'core.autocrlf', 'false'])
writeFileSync(path.join(repoPath, 'README.md'), '# Large file count fixture\n')
writeFileTree(
repoPath,
'src',
trackedFiles,
filesPerDirectory,
(index) => `export const tracked${index} = ${index}\n`
)
runGit(repoPath, ['add', '-A'])
runGit(repoPath, ['commit', '-m', 'Initial large file count fixture', '--quiet'])
const untrackedFileBytes = options.untrackedFileBytes ?? 0
const untrackedPadding =
untrackedFileBytes > 0
? `// ${'x'.repeat(78)}\n`.repeat(Math.ceil(untrackedFileBytes / 81))
: ''
writeFileTree(
repoPath,
'generated',
untrackedFiles,
filesPerDirectory,
(index) => `export const untracked${index} = ${index}\n${untrackedPadding}`
)
for (let index = 0; index < modifiedFiles; index += 1) {
const dirIndex = Math.floor(index / filesPerDirectory)
writeFileSync(
path.join(
repoPath,
'src',
`dir-${String(dirIndex).padStart(4, '0')}`,
`file-${String(index).padStart(6, '0')}.ts`
),
`export const tracked${index} = ${index}\nexport const modified${index} = true\n`
)
}
return { repoPath, trackedFiles, untrackedFiles, modifiedFiles }
}
@@ -0,0 +1,431 @@
/**
* Benchmark + repro suite for issue #8013: repos with ~10,000+ files make the
* renderer's memory grow and the UI unresponsive.
*
* Each scenario builds an isolated on-disk repo at a controlled scale, drives
* the real status pipeline (git scan → line stats → IPC → store → Source
* Control rows), and measures:
* - scanMs main-process `git status` + line-stat duration
* - payloadBytes serialized status payload size crossing IPC
* - renderMs time from setGitStatus until rows are in the DOM
* - maxLagMs/p95 event-loop lag while loading (UI responsiveness)
* - domNodeCount total DOM nodes after render
* - heap growth JS heap across repeated poll cycles (leak signal)
*
* Run a single scenario at a custom scale:
* ORCA_LARGE_FILE_COUNT=9500 npx playwright test \
* tests/e2e/source-control-large-file-count.spec.ts \
* --config tests/playwright.config.ts --project electron-headless
*/
import type { ElectronApplication, Page } from '@stablyai/playwright-test'
import { test, expect } from './helpers/orca-app'
import { waitForSessionReady } from './helpers/store'
import { createLargeFileCountRepo, removeLargeFileCountRepo } from './large-file-count-fixtures'
import { DEFAULT_GIT_STATUS_LIMIT } from '../../src/shared/git-status-limit'
// Matches the large-diff freeze budget: a blocking stall past 1s is the
// "UI becomes unresponsive" symptom reported in #8013.
const MAX_EVENT_LOOP_LAG_MS = 1_000
// A second full status→store→render cycle with identical data must not leak
// unbounded memory; allow generous headroom for GC timing noise.
const MAX_HEAP_GROWTH_PER_CYCLE_MB = 75
// A virtualized list mounts viewport + overscan rows only; anything past this
// bound means the panel is mounting rows proportional to the change set again.
const MAX_MOUNTED_ROWS = 1_000
type LoadMeasurement = {
entryCount: number
didHitLimit: boolean
scanMs: number
rescanMs: number
payloadBytes: number
renderMs: number
renderedRows: number
maxLagMs: number
p95LagMs: number
domNodeCount: number
heapUsedMbAfterRender: number
heapUsedMbPerCycle: number[]
cycleMaxLagMs: number[]
}
async function addAndActivateRepo(orcaPage: Page, repoPath: string): Promise<string> {
const repoId = await orcaPage.evaluate(async (pathToRepo: string) => {
const store = window.__store
if (!store) {
throw new Error('window.__store is not available')
}
const addedRepo = await store.getState().addRepoPath(pathToRepo)
if (!addedRepo) {
throw new Error(`isolated repo not found: ${pathToRepo}`)
}
return addedRepo.id
}, repoPath)
// Why: fetchWorktrees() resolves before Zustand always reflects the async
// worktree scan, so poll the same public store path real repo setup uses.
await expect
.poll(
() =>
orcaPage.evaluate(async (targetRepoId: string) => {
const store = window.__store
if (!store) {
return 0
}
await store.getState().fetchWorktrees(targetRepoId)
return store.getState().worktreesByRepo[targetRepoId]?.length ?? 0
}, repoId),
{ timeout: 30_000, message: 'isolated large-file-count worktree did not load' }
)
.toBeGreaterThan(0)
return await orcaPage.evaluate(
({ targetRepoId, pathToRepo }) => {
const store = window.__store
if (!store) {
throw new Error('window.__store is not available')
}
const state = store.getState()
const worktrees = state.worktreesByRepo[targetRepoId] ?? []
const worktree = worktrees.find((entry) => entry.path === pathToRepo) ?? worktrees[0]
if (!worktree) {
throw new Error(`isolated worktree not found: ${pathToRepo}`)
}
state.setActiveRepo(targetRepoId)
state.setActiveWorktree(worktree.id)
state.setRightSidebarOpen(true)
state.setRightSidebarTab('source-control')
return worktree.id
},
{ targetRepoId: repoId, pathToRepo: repoPath }
)
}
/**
* Drives the exact pipeline the panel's poll uses (api.git.status →
* setGitStatus) so results are deterministic even while the built-in 3s poll
* runs concurrently — the poll produces identical entries, which the store
* dedupes.
*/
async function measureSourceControlLoad(
orcaPage: Page,
args: { worktreeId: string; repoPath: string; expectedRows: number; pollCycles: number }
): Promise<LoadMeasurement> {
return await orcaPage.evaluate(async ({ worktreeId, repoPath, expectedRows, pollCycles }) => {
const store = window.__store
if (!store) {
throw new Error('window.__store is not available')
}
const lagSamples: number[] = []
const probeIntervalMs = 50
let lastTick = performance.now()
const probe = window.setInterval(() => {
const now = performance.now()
lagSamples.push(Math.max(0, now - lastTick - probeIntervalMs))
lastTick = now
}, probeIntervalMs)
const readHeapMb = (): number => {
const memory = (performance as unknown as { memory?: { usedJSHeapSize: number } }).memory
return memory ? memory.usedJSHeapSize / (1024 * 1024) : -1
}
try {
const scanStart = performance.now()
const status = await window.api.git.status({ worktreePath: repoPath })
const scanMs = performance.now() - scanStart
const payloadBytes = JSON.stringify(status).length
// Why: a virtualized panel mounts only viewport rows, so "all rows in
// the DOM" would never happen post-fix. First rows appearing is the
// user-visible "list loaded" moment either way.
const firstRowsTarget = Math.min(expectedRows, 30)
const renderStart = performance.now()
store.getState().setGitStatus(worktreeId, status)
let renderedRows = 0
while (performance.now() - renderStart < 60_000) {
renderedRows = document.querySelectorAll('[data-testid="source-control-entry"]').length
if (renderedRows >= firstRowsTarget) {
break
}
await new Promise((resolve) => window.setTimeout(resolve, 50))
}
const renderMs = performance.now() - renderStart
// Settle so the lag probe captures post-render layout/paint stalls too.
await new Promise((resolve) => window.setTimeout(resolve, 1_000))
renderedRows = document.querySelectorAll('[data-testid="source-control-entry"]').length
const heapUsedMbAfterRender = readHeapMb()
// Why: #8013 reports memory that keeps growing — replay the poll cycle
// (fresh scan, fresh entry array, store update) and track heap + lag.
const heapUsedMbPerCycle: number[] = []
const cycleMaxLagMs: number[] = []
let rescanMs = 0
for (let cycle = 0; cycle < pollCycles; cycle += 1) {
const cycleLagStart = lagSamples.length
const cycleScanStart = performance.now()
const cycleStatus = await window.api.git.status({ worktreePath: repoPath })
if (cycle === 0) {
// First rescan isolates warm-cache scan cost (untracked line-stat
// reads are mtime-cached after the initial scan).
rescanMs = performance.now() - cycleScanStart
}
store.getState().setGitStatus(worktreeId, cycleStatus)
await new Promise((resolve) => window.setTimeout(resolve, 500))
heapUsedMbPerCycle.push(readHeapMb())
cycleMaxLagMs.push(Math.max(0, ...lagSamples.slice(cycleLagStart)))
}
const sorted = [...lagSamples].sort((a, b) => a - b)
return {
entryCount: status.entries.length,
didHitLimit: status.didHitLimit === true,
scanMs,
rescanMs,
payloadBytes,
renderMs,
renderedRows,
maxLagMs: sorted.length ? (sorted.at(-1) ?? 0) : 0,
p95LagMs: sorted.length ? sorted[Math.floor(sorted.length * 0.95)] : 0,
domNodeCount: document.getElementsByTagName('*').length,
heapUsedMbAfterRender,
heapUsedMbPerCycle,
cycleMaxLagMs
}
} finally {
window.clearInterval(probe)
}
}, args)
}
function logMeasurement(
label: string,
measurement: LoadMeasurement & { rendererWorkingSetMb?: { before: number; after: number } }
): void {
console.log(`[large-file-count] ${label} ${JSON.stringify(measurement)}`)
}
/**
* OS-level working set of all renderer processes, in MB. The #8013 report is
* about renderer memory; JS heap alone misses DOM/native memory, which
* dominates when hundreds of thousands of nodes mount.
*/
async function readRendererWorkingSetMb(electronApp: ElectronApplication): Promise<number> {
return await electronApp.evaluate(({ app }) => {
let totalKb = 0
for (const metric of app.getAppMetrics()) {
if (metric.type === 'Tab') {
totalKb += metric.memory.workingSetSize
}
}
return totalKb / 1024
})
}
test.describe('Source Control large file count (#8013)', () => {
// Why: each scenario gets its own Electron app + isolated fixture repo, so a
// failing scale must not skip the others — every scenario is a data point.
test.use({ seedTestRepo: false })
test('thousands of untracked files under the status cap stay responsive', async ({
orcaPage,
electronApp
}) => {
test.setTimeout(600_000)
const untrackedFiles = Number(process.env.ORCA_LARGE_FILE_COUNT ?? '9500')
// Why: ORCA_LARGE_FILE_BYTES gives untracked files realistic sizes so the
// per-poll line-stat reads (cache-capped at 2,048 entries) become visible
// in rescanMs instead of hiding behind ~30-byte fixture files.
const untrackedFileBytes = Number(process.env.ORCA_LARGE_FILE_BYTES ?? '0')
const fixture = createLargeFileCountRepo({
trackedFiles: 100,
untrackedFiles,
untrackedFileBytes
})
try {
await waitForSessionReady(orcaPage)
const worktreeId = await addAndActivateRepo(orcaPage, fixture.repoPath)
const workingSetBeforeMb = await readRendererWorkingSetMb(electronApp)
const measurement = await measureSourceControlLoad(orcaPage, {
worktreeId,
repoPath: fixture.repoPath,
expectedRows: untrackedFiles,
pollCycles: 3
})
const workingSetAfterMb = await readRendererWorkingSetMb(electronApp)
logMeasurement(`untracked=${untrackedFiles}`, {
...measurement,
rendererWorkingSetMb: { before: workingSetBeforeMb, after: workingSetAfterMb }
})
expect(measurement.didHitLimit).toBe(false)
expect(measurement.entryCount).toBeGreaterThanOrEqual(untrackedFiles)
expect(measurement.renderedRows).toBeGreaterThan(0)
// Why: mounting rows proportional to the change set is the #8013 bug;
// a virtualized panel mounts viewport + overscan only.
expect(measurement.renderedRows).toBeLessThan(MAX_MOUNTED_ROWS)
expect(measurement.maxLagMs).toBeLessThan(MAX_EVENT_LOOP_LAG_MS)
if (measurement.heapUsedMbPerCycle.length > 1) {
const first = measurement.heapUsedMbPerCycle[0]
const last = measurement.heapUsedMbPerCycle.at(-1) ?? first
expect(last - first).toBeLessThan(
MAX_HEAP_GROWTH_PER_CYCLE_MB * (measurement.heapUsedMbPerCycle.length - 1)
)
}
} finally {
await removeLargeFileCountRepo(fixture.repoPath)
}
})
test('thousands of modified tracked files under the status cap stay responsive', async ({
orcaPage,
electronApp
}) => {
test.setTimeout(600_000)
const modifiedFiles = Number(process.env.ORCA_LARGE_FILE_COUNT ?? '5000')
const fixture = createLargeFileCountRepo({ trackedFiles: modifiedFiles, modifiedFiles })
try {
await waitForSessionReady(orcaPage)
const worktreeId = await addAndActivateRepo(orcaPage, fixture.repoPath)
const workingSetBeforeMb = await readRendererWorkingSetMb(electronApp)
const measurement = await measureSourceControlLoad(orcaPage, {
worktreeId,
repoPath: fixture.repoPath,
expectedRows: modifiedFiles,
pollCycles: 3
})
const workingSetAfterMb = await readRendererWorkingSetMb(electronApp)
logMeasurement(`modified=${modifiedFiles}`, {
...measurement,
rendererWorkingSetMb: { before: workingSetBeforeMb, after: workingSetAfterMb }
})
expect(measurement.didHitLimit).toBe(false)
expect(measurement.entryCount).toBeGreaterThanOrEqual(modifiedFiles)
expect(measurement.renderedRows).toBeGreaterThan(0)
expect(measurement.renderedRows).toBeLessThan(MAX_MOUNTED_ROWS)
expect(measurement.maxLagMs).toBeLessThan(MAX_EVENT_LOOP_LAG_MS)
} finally {
await removeLargeFileCountRepo(fixture.repoPath)
}
})
test('a change set over the status cap degrades to the too-many-changes state', async ({
orcaPage,
electronApp
}) => {
test.setTimeout(600_000)
const untrackedFiles = DEFAULT_GIT_STATUS_LIMIT + 1_000
const fixture = createLargeFileCountRepo({ trackedFiles: 100, untrackedFiles })
try {
await waitForSessionReady(orcaPage)
const worktreeId = await addAndActivateRepo(orcaPage, fixture.repoPath)
const workingSetBeforeMb = await readRendererWorkingSetMb(electronApp)
const measurement = await measureSourceControlLoad(orcaPage, {
worktreeId,
repoPath: fixture.repoPath,
// The capped payload still carries DEFAULT_GIT_STATUS_LIMIT entries;
// the panel may render them, but must stay responsive while doing so.
expectedRows: 0,
pollCycles: 1
})
const workingSetAfterMb = await readRendererWorkingSetMb(electronApp)
logMeasurement(`over-cap untracked=${untrackedFiles}`, {
...measurement,
rendererWorkingSetMb: { before: workingSetBeforeMb, after: workingSetAfterMb }
})
expect(measurement.didHitLimit).toBe(true)
expect(measurement.entryCount).toBeLessThanOrEqual(DEFAULT_GIT_STATUS_LIMIT)
expect(measurement.renderedRows).toBeLessThan(MAX_MOUNTED_ROWS)
expect(measurement.maxLagMs).toBeLessThan(MAX_EVENT_LOOP_LAG_MS)
// Why: didHitLimit must park the worktree in the huge-status state so
// background polling stops re-running tens-of-seconds git scans.
const hugeState = await orcaPage.evaluate(
(wId) => window.__store?.getState().gitStatusHugeByWorktree?.[wId] ?? null,
worktreeId
)
expect(hugeState).not.toBeNull()
} finally {
await removeLargeFileCountRepo(fixture.repoPath)
}
})
test('untracked line-stat cache stays effective above 2,048 files', async ({ orcaPage }) => {
test.setTimeout(600_000)
// Why: the untracked line-stat cache caps at 2,048 entries with FIFO
// eviction. A sequential scan over more files than the cap evicts every
// entry before the next poll revisits it (0% hit rate), so each 3s poll
// re-reads every untracked file's contents. Compare per-file warm rescan
// cost under vs over the cap on the same machine — a healthy cache keeps
// the ratio near 1; thrash makes it several-fold.
const fileBytes = 65_536
const smallRepo = createLargeFileCountRepo({
trackedFiles: 10,
untrackedFiles: 2_000,
untrackedFileBytes: fileBytes
})
const largeRepo = createLargeFileCountRepo({
trackedFiles: 10,
untrackedFiles: 4_000,
untrackedFileBytes: fileBytes
})
try {
await waitForSessionReady(orcaPage)
const warmRescanPerFileMs = async (repoPath: string, files: number): Promise<number> => {
const worktreeId = await addAndActivateRepo(orcaPage, repoPath)
const measurement = await measureSourceControlLoad(orcaPage, {
worktreeId,
repoPath,
expectedRows: files,
pollCycles: 1
})
return measurement.rescanMs / files
}
const smallPerFileMs = await warmRescanPerFileMs(smallRepo.repoPath, 2_000)
const largePerFileMs = await warmRescanPerFileMs(largeRepo.repoPath, 4_000)
console.log(
`[large-file-count] line-stat-cache smallPerFileMs=${smallPerFileMs.toFixed(4)} largePerFileMs=${largePerFileMs.toFixed(4)} ratio=${(largePerFileMs / smallPerFileMs).toFixed(2)}`
)
expect(largePerFileMs).toBeLessThan(smallPerFileMs * 2)
} finally {
await removeLargeFileCountRepo(smallRepo.repoPath)
await removeLargeFileCountRepo(largeRepo.repoPath)
}
})
test('a large clean repo (tracked files only) loads instantly', async ({
orcaPage,
electronApp
}) => {
test.setTimeout(600_000)
const trackedFiles = Number(process.env.ORCA_LARGE_FILE_COUNT ?? '15000')
const fixture = createLargeFileCountRepo({ trackedFiles })
try {
await waitForSessionReady(orcaPage)
const worktreeId = await addAndActivateRepo(orcaPage, fixture.repoPath)
const workingSetBeforeMb = await readRendererWorkingSetMb(electronApp)
const measurement = await measureSourceControlLoad(orcaPage, {
worktreeId,
repoPath: fixture.repoPath,
expectedRows: 0,
pollCycles: 2
})
const workingSetAfterMb = await readRendererWorkingSetMb(electronApp)
logMeasurement(`clean tracked=${trackedFiles}`, {
...measurement,
rendererWorkingSetMb: { before: workingSetBeforeMb, after: workingSetAfterMb }
})
expect(measurement.entryCount).toBe(0)
expect(measurement.maxLagMs).toBeLessThan(MAX_EVENT_LOOP_LAG_MS)
} finally {
await removeLargeFileCountRepo(fixture.repoPath)
}
})
})