Files
orca/tools/benchmarks
Neil 66a4897754 cherry-pick: AI Vault workspace-switch perf fixes for 1.4.161 (#11320)
* test(vault): guard the workspace-switch regression #11303 fixed (#11311)

#11303 removed the O(sessions x roots) path normalization from the session
worktree map, but nothing fails if that shape comes back. Adds the two
checks that were missing, plus the tool that would have caught it.

- timing guard: 1200 worktrees x 400 sessions must build in <150ms.
  Verified fail-first — restoring the pre-#11303 per-session
  buildWorktreeCandidates call takes it to 238ms; it is ~15ms as merged.
- path boundary: '/repo/alpha-sibling' must not be attributed to
  '/repo/alpha'. Hoisting the root normalization out of the loop must not
  degrade containment into a bare startsWith.

Also adds tools/benchmarks/workspace-switch-paint-latency.mjs, which
attaches over CDP and measures first-paint-after-click and max frame gap.
The existing worktree-switch-responsiveness.spec.ts only times the
synchronous click task, which stays ~1ms because the highlight is a direct
DOM mutation — that is why a ~1s stall could ship without tripping a
budget. On the affected build it read maxFrameGap p50=973ms.

(cherry picked from commit c5102e1262)

* perf(vault): dedupe scope paths by key instead of rescanning (#11314)

* perf(vault): dedupe scope paths by key instead of rescanning

#11303 took the session maps off the workspace-switch path, but scope path
derivation still follows the active worktree and stayed quadratic.

addAiVaultWorkspaceScopePath deduped by re-normalizing every already
accepted path on each insert, so accepting K paths cost O(K^2)
normalize('NFC') calls — ~632k at 1124 workspaces. isAiVaultWorkspaceScopePathClaimed
separately rescanned every live worktree per prior id, and runs twice per
switch via activeWorktreePaths and scopePaths.

- carry a Set of comparison keys alongside the paths, so each insert is one
  normalize plus one Set lookup
- thread that accumulator from the workspace pass into the project pass
  rather than restarting deduplication against a plain array
- build one comparison-path -> worktree id map for the claim check, keeping
  first-writer-wins to match the previous some() short-circuit

deriveAiVaultScopeSessionPaths on a real 1124-workspace profile:
189.7ms -> 0.8ms. Output is unchanged, including ordering: verified against
the previous implementation across 117 scenario/option combinations
covering monorepo and mixed-repo layouts, priors both claimed and
unclaimed, duplicate paths, NFD/NFC, WSL UNC, trailing and doubled
separators, relative and blank paths, and four project-key shapes.

Adds the first test file for this module: scope semantics (priors, claimed
priors, cross-repo rejection, dedupe, NFD/NFC) plus a timing guard.
Verified fail-first — the guard reports 220ms on the previous
implementation. Path length is chosen deliberately, since normalize() cost
scales with it and short synthetic paths understate the old shape.

* fix(vault): make the claim check independent of worktree ordering

Review catch on the first pass: keying claims by comparison path meant a
duplicate path had to pick one owner, and picking the active worktree
masked a real claimant later in the list. Concretely, with the active
worktree also listed at its own prior path, the prior was reported
unclaimed where the previous some() reported it claimed.

Excludes the active worktree while building the set instead of comparing
ids at read time, so any surviving entry is a claim by construction and
ordering cannot decide the result.

Adds a test over four orderings, verified fail-first against the previous
commit. Also adds a timing guard for deriveAiVaultWorkspaceScopePaths,
which the session-scope guard did not cover.

Equivalence rerun against the pre-optimization implementation: 156
scenario/option combinations, identical paths and ordering.

(cherry picked from commit 6cc579a48c)

* perf(vault): look up resume worktrees through the shared index (#11317)

Resolving a session's resume target scanned every worktree in every repo:
two `Object.values(worktreesByRepo).flat().find(...)` calls, which also
allocate a fresh 1124-element array each time, plus an equivalent `some()`
walk. All three run per visible session row, so a panel render repeated
them ~20 times.

`getIndexedWorktreeMap` already exists for this and is WeakMap-cached on
`worktreesByRepo`, so the index is built once per store snapshot rather
than per call. `connection-owner-resolution.ts` already resolves worktrees
this way.

~1.6ms -> ~0.002ms per render pass at 1124 worktrees. Net -4 lines.

Behavior is unchanged: the map dedupes by id, which only diverges from
`find()` when one id appears twice with different objects. Worktree ids are
`repoId::path`, so a duplicate id within a repo array — the documented race
the index was built for — refers to the same worktree.

(cherry picked from commit b4b3bcdb84)
2026-07-29 00:36:09 -07:00
..