mirror of
https://github.com/stablyai/orca.git
synced 2026-10-29 16:17:51 +00:00
* fix(workspaces): support full cleanup scans * feat(workspaces): persist cleanup snapshots * feat(workspaces): add cleanup filter model * refactor(workspaces): remove cleanup presets * feat(workspaces): rework cleanup dialog * fix(workspaces): keep cleanup row ordering render-pure * refactor(workspaces): simplify cleanup browsing * refactor(workspaces): show cleanup facts * refactor(workspaces): surface cleanup row facts * fix(workspaces): remove misleading cleanup count * fix(workspaces): preserve full scan semantics * fix(workspaces): scope snapshot persistence * fix(workspaces): preserve cleanup browse compatibility * fix(workspaces): reconcile cleanup dialog state * test(workspaces): update snapshot store fixtures * test(workspaces): preserve cleanup scan modes * perf(workspace-cleanup): stream scan progress and size results * fix(workspace-cleanup): address review feedback * fix(workspace-cleanup): preserve host-scoped cleanup metadata * fix(workspace-cleanup): declare review source dependencies * fix(workspace-cleanup): align size scan banner * fix(workspace-cleanup): shorten scan action * perf(workspace-cleanup): avoid redundant scan IO * perf(workspace-cleanup): bound restarted evidence scans * fix(workspace-cleanup): satisfy scan queue lint * perf(workspace-cleanup): bound scan and snapshot work * perf(workspace-cleanup): serialize final enrichment * test(workspace-cleanup): assert final enrichment drain * fix(workspace-cleanup): stop progress after renderer teardown * perf: batch workspace cleanup git evidence scans * perf(workspace-cleanup): stop redundant snapshot and scan work * fix(workspace-cleanup): resolve review findings across scan, store, and dialog Correctness: - Chunk git-evidence dispatches at the shared 500-target limit and exclude queued/in-flight ids from target selection, so fleets past the limit can no longer strand rows permanently mislabeled as checked-but-unknown. - Key destructive selection pruning on the user's filter state instead of the per-tick matched-set identity; streaming reclassification no longer silently deselects rows. - Clamp the facet clock to max(scannedAt, open time): a stale hydrated snapshot no longer misbuckets idle thresholds or keeps dead agents fresh; row labels use the same clock. - Supersede and cancel the previous broad scan when a new one starts (renderer registry and same-sender guard in main) instead of racing two fleet scans. - Gate snapshot persistence on hasTargetedWorkspaceCleanupScan so worktreeIds: [] can never persist an empty fleet snapshot. - Re-apply dismissals at set-time in progress application so a dismissal landing mid-enrichment is not clobbered. - Record a one-off local snapshot prune for single (unbatched) remote deletes so removed workspaces cannot resurrect from cache. - Strip .exe when normalizing foreground process names so Windows agent processes match. Performance: - Cache per-candidate facet and review-info objects on candidate identity; no-op streaming ticks reuse the previous rows array and skip every downstream pass; matched-set identity is stable under equal membership. - Compute facet counts/options only while the filter popover is open. - Equality-bail git-evidence publishes; structural (non-stringify) facet-group comparison memoized in the toolbar. - Identity-token fast path for the enrichment cache (cache hits skip both JSON.stringify signatures); prune viewed/dismissal records on removal and expiry; bound the superseded-scan-id set. - Restore the no-op bail in removeWorkspaceSpaceWorktrees (regression). - Abort main-side scans when the renderer is destroyed; module-scope controller maps survive handler re-registration. - Batch removal preflight into one targeted scan (with refreshActivity) per 500 ids instead of one scan per row. - Scan repos at concurrency 2, report discovered counts upfront for honest progress, share fs-activity probes per path (folder workspaces), read only the reflog tail, and skip the snapshot read-before-write via a remembered scannedAt. Split workspace-cleanup-worktree-listing, workspace-cleanup-facet-row-caches, and workspace-cleanup-selection-model out of files that crossed max-lines. * fix(workspace-cleanup): address verifier findings - Fall back to a full reflog read when the newest record exceeds the 8KB tail window, so an oversized subject cannot hide recent ref activity. - Bound the single-removal snapshot prune batch id with a UUID; embedding the unbounded worktreeId silently failed main's 128-char validation and skipped the prune for long remote ids. - Key the main-side broad-scan supersession by sender AND scan mode so legacy suggestion-only and full-workspace scans stay isolated, matching the renderer registry. * fix(workspace-cleanup): own facet caches with useMemo instead of render-time ref writes React Doctor (CI changed-lines gate) correctly flagged the three cache refs written during render. Each per-candidate cache now lives in one memo with the derived context it is keyed on, so the memo deps are the invalidation and interior fills stay content-addressed; the matched-set identity stabilization is dropped since its only consumer reads through a useEffectEvent and never keys on identity.
154 lines
5.4 KiB
TypeScript
154 lines
5.4 KiB
TypeScript
// Opt in: ORCA_WORKSPACE_SNAPSHOT_PRUNE_BENCH=1 pnpm exec vitest run --config config/vitest.config.ts src/main/workspace-snapshot-pruning.bench.test.ts
|
|
import { performance } from 'node:perf_hooks'
|
|
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
|
import type { WorkspaceCleanupCandidate } from '../shared/workspace-cleanup'
|
|
import type { WorkspaceSpaceWorktree } from '../shared/workspace-space-types'
|
|
import {
|
|
persistWorkspaceCleanupScanResult,
|
|
pruneWorkspaceCleanupScanSnapshots
|
|
} from './workspace-cleanup-scan-snapshot'
|
|
import {
|
|
persistWorkspaceSpaceAnalysisSnapshot,
|
|
pruneWorkspaceSpaceAnalysisSnapshots
|
|
} from './workspace-space-analysis-snapshot'
|
|
|
|
const describeBench = process.env.ORCA_WORKSPACE_SNAPSHOT_PRUNE_BENCH ? describe : describe.skip
|
|
const ROW_COUNT = 610
|
|
const DELETE_COUNT = 100
|
|
|
|
describeBench('workspace snapshot bulk pruning', () => {
|
|
let snapshotDirectory = ''
|
|
|
|
beforeAll(async () => {
|
|
snapshotDirectory = await mkdtemp(join(tmpdir(), 'orca-snapshot-prune-bench-'))
|
|
})
|
|
|
|
afterAll(async () => {
|
|
await rm(snapshotDirectory, { recursive: true, force: true })
|
|
})
|
|
|
|
it('prunes the measured 610-row/100-delete shape with one write per sidecar', async () => {
|
|
const candidates = Array.from({ length: ROW_COUNT }, (_, index) => makeCandidate(index))
|
|
const worktrees = Array.from({ length: ROW_COUNT }, (_, index) => makeWorktree(index))
|
|
const targets = candidates.slice(0, DELETE_COUNT).map((candidate) => ({
|
|
worktreeId: candidate.worktreeId,
|
|
executionHostId: candidate.executionHostId
|
|
}))
|
|
await persistWorkspaceCleanupScanResult(
|
|
snapshotDirectory,
|
|
{ includeAllWorkspaces: true },
|
|
{ scannedAt: 1, candidates, errors: [] }
|
|
)
|
|
await persistWorkspaceSpaceAnalysisSnapshot(snapshotDirectory, {
|
|
scannedAt: 1,
|
|
totalSizeBytes: ROW_COUNT * 1_000,
|
|
reclaimableBytes: ROW_COUNT * 1_000,
|
|
worktreeCount: ROW_COUNT,
|
|
scannedWorktreeCount: ROW_COUNT,
|
|
unavailableWorktreeCount: 0,
|
|
repos: makeRepoSummaries(worktrees),
|
|
worktrees
|
|
})
|
|
|
|
const startedAt = performance.now()
|
|
await Promise.all([
|
|
pruneWorkspaceCleanupScanSnapshots(snapshotDirectory, targets),
|
|
pruneWorkspaceSpaceAnalysisSnapshots(snapshotDirectory, targets)
|
|
])
|
|
const pruneMs = performance.now() - startedAt
|
|
const cleanupPayload = JSON.parse(
|
|
await readFile(join(snapshotDirectory, 'orca-workspace-cleanup-scan.json'), 'utf-8')
|
|
) as { result: { candidates: unknown[] } }
|
|
const spacePayload = JSON.parse(
|
|
await readFile(join(snapshotDirectory, 'orca-workspace-space-analysis.json'), 'utf-8')
|
|
) as { analysis: { worktrees: unknown[] } }
|
|
|
|
console.log(
|
|
`[bench] rows=${ROW_COUNT} deleted=${DELETE_COUNT} bulkPrune=${pruneMs.toFixed(2)}ms`
|
|
)
|
|
expect(cleanupPayload.result.candidates).toHaveLength(ROW_COUNT - DELETE_COUNT)
|
|
expect(spacePayload.analysis.worktrees).toHaveLength(ROW_COUNT - DELETE_COUNT)
|
|
})
|
|
})
|
|
|
|
function executionHostId(index: number): 'local' | `ssh:${string}` {
|
|
return index % 3 === 0 ? `ssh:ssh-${index % 7}` : 'local'
|
|
}
|
|
|
|
function makeCandidate(index: number): WorkspaceCleanupCandidate {
|
|
const hostId = executionHostId(index)
|
|
return {
|
|
worktreeId: `repo-${index % 20}::/workspace-${index}`,
|
|
repoId: `repo-${index % 20}`,
|
|
repoName: `Repo ${index % 20}`,
|
|
connectionId: hostId === 'local' ? null : hostId.slice('ssh:'.length),
|
|
executionHostId: hostId,
|
|
displayName: `Workspace ${index}`,
|
|
branch: `branch-${index}`,
|
|
path: `/workspace-${index}`,
|
|
tier: 'ready',
|
|
selectedByDefault: true,
|
|
reasons: ['idle-clean'],
|
|
blockers: [],
|
|
lastActivityAt: 0,
|
|
localContext: {
|
|
terminalTabCount: 0,
|
|
cleanEditorTabCount: 0,
|
|
browserTabCount: 0,
|
|
diffCommentCount: 0,
|
|
newestDiffCommentAt: null,
|
|
retainedDoneAgentCount: 0
|
|
},
|
|
git: { clean: true, upstreamAhead: 0, upstreamBehind: 0, checkedAt: 0 },
|
|
fingerprint: `fingerprint-${index}`
|
|
}
|
|
}
|
|
|
|
function makeWorktree(index: number): WorkspaceSpaceWorktree {
|
|
const candidate = makeCandidate(index)
|
|
return {
|
|
worktreeId: candidate.worktreeId,
|
|
repoId: candidate.repoId,
|
|
executionHostId: candidate.executionHostId,
|
|
repoDisplayName: candidate.repoName,
|
|
repoPath: `/repo-${index % 20}`,
|
|
displayName: candidate.displayName,
|
|
path: candidate.path,
|
|
branch: candidate.branch,
|
|
isMainWorktree: false,
|
|
isRemote: candidate.executionHostId !== 'local',
|
|
isSparse: false,
|
|
canDelete: true,
|
|
lastActivityAt: 0,
|
|
status: 'ok',
|
|
error: null,
|
|
scannedAt: 1,
|
|
sizeBytes: 1_000,
|
|
reclaimableBytes: 1_000,
|
|
skippedEntryCount: 0,
|
|
topLevelItems: [],
|
|
omittedTopLevelItemCount: 0,
|
|
omittedTopLevelSizeBytes: 0
|
|
}
|
|
}
|
|
|
|
function makeRepoSummaries(worktrees: WorkspaceSpaceWorktree[]) {
|
|
const groups = Map.groupBy(worktrees, (row) => `${row.executionHostId ?? 'local'}\0${row.repoId}`)
|
|
return [...groups.values()].map((rows) => ({
|
|
repoId: rows[0].repoId,
|
|
executionHostId: rows[0].executionHostId,
|
|
displayName: rows[0].repoDisplayName,
|
|
path: rows[0].repoPath,
|
|
isRemote: rows[0].isRemote,
|
|
worktreeCount: rows.length,
|
|
scannedWorktreeCount: rows.length,
|
|
unavailableWorktreeCount: 0,
|
|
totalSizeBytes: rows.length * 1_000,
|
|
reclaimableBytes: rows.length * 1_000,
|
|
error: null
|
|
}))
|
|
}
|