From aa7e76ba613be843f985badd02fb8a94ac2df5cc Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:05:12 -0700 Subject: [PATCH] perf(tabs): index tab agent status by tab instead of scanning the global map (#12413) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf(tabs): index tab agent status by tab instead of scanning the global map resolveAnyCompletedTabAgent and its live/retained twins scanned the whole agentStatusByPaneKey map and parsed every pane key, once per tab per render — ~10^5 parsePaneKey calls per render pass with 200 tabs. Cache a per-tab pane index on the map's identity (the store replaces it on every write) so a render pass scans once instead of once per tab. Insertion order is preserved because the resolvers return the first match. * test(tabs): lock agent status index scan count --- .../src/lib/tab-agent-status-index.test.ts | 386 ++++++++++++++++++ .../src/lib/tab-agent-status-index.ts | 117 ++++++ src/renderer/src/lib/tab-agent.ts | 50 +-- 3 files changed, 522 insertions(+), 31 deletions(-) create mode 100644 src/renderer/src/lib/tab-agent-status-index.test.ts create mode 100644 src/renderer/src/lib/tab-agent-status-index.ts diff --git a/src/renderer/src/lib/tab-agent-status-index.test.ts b/src/renderer/src/lib/tab-agent-status-index.test.ts new file mode 100644 index 000000000..77ca75e4d --- /dev/null +++ b/src/renderer/src/lib/tab-agent-status-index.test.ts @@ -0,0 +1,386 @@ +import { describe, expect, it } from 'vitest' +import { + resolveFocusedCompletedTabAgent, + resolveFocusedRetainedTabAgent, + resolveFocusedTabAgent, + resolveSiblingCompletedTabAgent, + resolveSiblingRetainedTabAgent, + resolveSiblingTabAgent +} from './tab-agent' +import { agentTypeToIconAgent } from './agent-status' +import { isTerminalLeafId, parsePaneKey } from '../../../shared/stable-pane-id' +import type { + AgentStatusEntry, + AgentStatusState, + AgentType +} from '../../../shared/agent-status-types' +import type { TerminalLayoutSnapshot, TerminalTab, TuiAgent } from '../../../shared/types' +import type { RetainedAgentEntry } from '@/store/slices/agent-status' + +// ─── Oracle: the pre-index full-map scans, kept here (not in src) so the +// randomized suite can assert the indexed resolvers are byte-identical. ─── + +function oracleAnyTabAgent( + map: Record, + tabId: string, + excludedLeafId?: string +): TuiAgent | null { + for (const [paneKey, entry] of Object.entries(map)) { + const parsed = parsePaneKey(paneKey) + if (parsed?.tabId === tabId && parsed.leafId !== excludedLeafId) { + const agent = entry.state === 'done' ? null : agentTypeToIconAgent(entry.agentType) + if (agent) { + return agent + } + } + } + return null +} + +function oracleAnyCompletedTabAgent( + map: Record, + tabId: string, + excludedLeafId?: string +): TuiAgent | null { + for (const [paneKey, entry] of Object.entries(map)) { + const parsed = parsePaneKey(paneKey) + if (parsed?.tabId === tabId && parsed.leafId !== excludedLeafId) { + const agent = entry.state === 'done' ? agentTypeToIconAgent(entry.agentType) : null + if (agent) { + return agent + } + } + } + return null +} + +function oracleAnyRetainedTabAgent( + map: Record, + tabId: string, + excludedLeafId?: string +): TuiAgent | null { + for (const [paneKey, retained] of Object.entries(map)) { + const parsed = parsePaneKey(paneKey) + if (parsed?.tabId === tabId && parsed.leafId !== excludedLeafId) { + const agent = agentTypeToIconAgent(retained.agentType) + if (agent) { + return agent + } + } + } + return null +} + +function activeLeafOf(layout: TerminalLayoutSnapshot | undefined): string | null { + const activeLeafId = layout?.activeLeafId + return activeLeafId && isTerminalLeafId(activeLeafId) ? activeLeafId : null +} + +const ORACLES = { + focused: ( + map: Record, + layout: TerminalLayoutSnapshot | undefined, + tabId: string + ): TuiAgent | null => { + const activeLeafId = activeLeafOf(layout) + if (activeLeafId) { + const entry = map[`${tabId}:${activeLeafId}`] + return !entry || entry.state === 'done' ? null : agentTypeToIconAgent(entry.agentType) + } + return oracleAnyTabAgent(map, tabId) + }, + sibling: ( + map: Record, + layout: TerminalLayoutSnapshot | undefined, + tabId: string + ): TuiAgent | null => { + const activeLeafId = activeLeafOf(layout) + return activeLeafId ? oracleAnyTabAgent(map, tabId, activeLeafId) : null + }, + focusedCompleted: ( + map: Record, + layout: TerminalLayoutSnapshot | undefined, + tabId: string + ): TuiAgent | null => { + const activeLeafId = activeLeafOf(layout) + if (activeLeafId) { + const entry = map[`${tabId}:${activeLeafId}`] + return !entry || entry.state !== 'done' ? null : agentTypeToIconAgent(entry.agentType) + } + return oracleAnyCompletedTabAgent(map, tabId) + }, + siblingCompleted: ( + map: Record, + layout: TerminalLayoutSnapshot | undefined, + tabId: string + ): TuiAgent | null => { + const activeLeafId = activeLeafOf(layout) + return activeLeafId ? oracleAnyCompletedTabAgent(map, tabId, activeLeafId) : null + }, + focusedRetained: ( + map: Record, + layout: TerminalLayoutSnapshot | undefined, + tabId: string + ): TuiAgent | null => { + const activeLeafId = activeLeafOf(layout) + if (activeLeafId) { + return agentTypeToIconAgent(map[`${tabId}:${activeLeafId}`]?.agentType) + } + return oracleAnyRetainedTabAgent(map, tabId) + }, + siblingRetained: ( + map: Record, + layout: TerminalLayoutSnapshot | undefined, + tabId: string + ): TuiAgent | null => { + const activeLeafId = activeLeafOf(layout) + return activeLeafId ? oracleAnyRetainedTabAgent(map, tabId, activeLeafId) : null + } +} + +// ─── Fixtures ─── + +function mulberry32(seed: number): () => number { + let a = seed >>> 0 + return () => { + a = (a + 0x6d2b79f5) >>> 0 + let t = Math.imul(a ^ (a >>> 15), 1 | a) + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t + return ((t ^ (t >>> 14)) >>> 0) / 4294967296 + } +} + +function leafId(n: number): string { + const hex = n.toString(16).padStart(12, '0') + return `11111111-1111-4111-8111-${hex}` +} + +const STATES: readonly AgentStatusState[] = ['working', 'blocked', 'waiting', 'done'] +// Mixes iconable agents with ones agentTypeToIconAgent rejects. +const AGENT_TYPES: readonly (AgentType | undefined)[] = [ + 'claude', + 'codex', + 'gemini', + 'pi', + 'omp', + 'unknown', + 'some-custom-agent', + undefined +] + +function statusEntry( + paneKey: string, + state: AgentStatusState, + agentType?: AgentType +): AgentStatusEntry { + return { + state, + prompt: '', + updatedAt: 0, + stateStartedAt: 0, + paneKey, + stateHistory: [], + ...(agentType ? { agentType } : {}) + } +} + +function retainedEntry(paneKey: string, agentType: AgentType): RetainedAgentEntry { + const tabId = paneKey.slice(0, paneKey.indexOf(':')) + const tab: TerminalTab = { + id: tabId, + ptyId: null, + worktreeId: 'wt-1', + title: 'Terminal 1', + customTitle: null, + color: null, + sortOrder: 0, + createdAt: 0 + } + return { + entry: { ...statusEntry(paneKey, 'done', agentType), state: 'done' }, + worktreeId: tab.worktreeId, + tab, + agentType, + startedAt: 0 + } +} + +function layoutOf(activeLeafId: string | null): TerminalLayoutSnapshot { + return { root: null, activeLeafId, expandedLeafId: null } +} + +function countEntryScans(source: T): { source: T; scans: () => number } { + let scanCount = 0 + return { + source: new Proxy(source, { + ownKeys: (target) => { + scanCount += 1 + return Reflect.ownKeys(target) + } + }), + scans: () => scanCount + } +} + +type RandomCase = { + statusMap: Record + retainedMap: Record + probes: { tabId: string; layout: TerminalLayoutSnapshot | undefined }[] +} + +function generateCase(random: () => number): RandomCase { + const tabCount = 1 + Math.floor(random() * 6) + const tabIds = Array.from({ length: tabCount }, (_, i) => `tab-${i}`) + const paneKeys: string[] = [] + for (const tabId of tabIds) { + const leafCount = 1 + Math.floor(random() * 4) + for (let leaf = 0; leaf < leafCount; leaf += 1) { + paneKeys.push(`${tabId}:${leafId(leaf)}`) + } + } + // Malformed keys the resolvers must skip, plus a shuffle so insertion order varies. + paneKeys.push('tab-0:not-a-uuid', 'no-colon-key', `:${leafId(0)}`, `tab-0:${leafId(0)}:extra`) + for (let i = paneKeys.length - 1; i > 0; i -= 1) { + const j = Math.floor(random() * (i + 1)) + ;[paneKeys[i], paneKeys[j]] = [paneKeys[j]!, paneKeys[i]!] + } + + const statusMap: Record = {} + const retainedMap: Record = {} + for (const paneKey of paneKeys) { + if (random() < 0.8) { + const state = STATES[Math.floor(random() * STATES.length)]! + statusMap[paneKey] = statusEntry( + paneKey, + state, + AGENT_TYPES[Math.floor(random() * AGENT_TYPES.length)] + ) + } + if (random() < 0.5) { + const agentType = AGENT_TYPES[Math.floor(random() * AGENT_TYPES.length)] ?? 'claude' + retainedMap[paneKey] = retainedEntry(paneKey, agentType) + } + } + + const probes = tabIds.concat('tab-absent').map((tabId) => { + const roll = random() + const layout = + roll < 0.2 + ? undefined + : roll < 0.35 + ? layoutOf(null) + : roll < 0.45 + ? layoutOf('not-a-uuid') + : layoutOf(leafId(Math.floor(random() * 5))) + return { tabId, layout } + }) + return { statusMap, retainedMap, probes } +} + +function expectParity(testCase: RandomCase): void { + for (const { tabId, layout } of testCase.probes) { + const { statusMap, retainedMap } = testCase + expect(resolveFocusedTabAgent(statusMap, layout, tabId)).toBe( + ORACLES.focused(statusMap, layout, tabId) + ) + expect(resolveSiblingTabAgent(statusMap, layout, tabId)).toBe( + ORACLES.sibling(statusMap, layout, tabId) + ) + expect(resolveFocusedCompletedTabAgent(statusMap, layout, tabId)).toBe( + ORACLES.focusedCompleted(statusMap, layout, tabId) + ) + expect(resolveSiblingCompletedTabAgent(statusMap, layout, tabId)).toBe( + ORACLES.siblingCompleted(statusMap, layout, tabId) + ) + expect(resolveFocusedRetainedTabAgent(retainedMap, layout, tabId)).toBe( + ORACLES.focusedRetained(retainedMap, layout, tabId) + ) + expect(resolveSiblingRetainedTabAgent(retainedMap, layout, tabId)).toBe( + ORACLES.siblingRetained(retainedMap, layout, tabId) + ) + } +} + +describe('tab agent status index parity with the pre-index full-map scan', () => { + it('matches the oracle across 250 randomized status maps', () => { + const random = mulberry32(0xc0ffee) + for (let caseIndex = 0; caseIndex < 250; caseIndex += 1) { + expectParity(generateCase(random)) + } + }) + + it('returns the first done sibling in insertion order when siblings run different agents', () => { + const map = { + [`tab-1:${leafId(0)}`]: statusEntry(`tab-1:${leafId(0)}`, 'done', 'codex'), + [`tab-1:${leafId(1)}`]: statusEntry(`tab-1:${leafId(1)}`, 'done', 'claude'), + [`tab-1:${leafId(2)}`]: statusEntry(`tab-1:${leafId(2)}`, 'done', 'gemini') + } + expect(resolveSiblingCompletedTabAgent(map, layoutOf(leafId(2)), 'tab-1')).toBe('codex') + expect(resolveSiblingCompletedTabAgent(map, layoutOf(leafId(0)), 'tab-1')).toBe('claude') + // Fresh identity, reversed insertion order → first match flips. + const reversed = Object.fromEntries(Object.entries(map).toReversed()) + expect(resolveSiblingCompletedTabAgent(reversed, layoutOf(leafId(2)), 'tab-1')).toBe('claude') + }) + + it('excludes the active leaf and skips non-iconable agents', () => { + const map = { + [`tab-1:${leafId(0)}`]: statusEntry(`tab-1:${leafId(0)}`, 'done', 'unknown'), + [`tab-1:${leafId(1)}`]: statusEntry(`tab-1:${leafId(1)}`, 'done', 'claude'), + [`tab-2:${leafId(2)}`]: statusEntry(`tab-2:${leafId(2)}`, 'done', 'codex') + } + expect(resolveSiblingCompletedTabAgent(map, layoutOf(leafId(1)), 'tab-1')).toBeNull() + expect(resolveSiblingCompletedTabAgent(map, layoutOf(leafId(0)), 'tab-1')).toBe('claude') + }) + + it('ignores pane keys parsePaneKey rejects and empty maps', () => { + const malformed = { + 'tab-1:not-a-uuid': statusEntry('tab-1:not-a-uuid', 'done', 'claude'), + 'tab-1': statusEntry('tab-1', 'done', 'claude'), + [`:${leafId(0)}`]: statusEntry(`:${leafId(0)}`, 'done', 'claude') + } + expect(resolveFocusedCompletedTabAgent(malformed, undefined, 'tab-1')).toBeNull() + expect(resolveSiblingCompletedTabAgent(malformed, layoutOf(leafId(9)), 'tab-1')).toBeNull() + expect(resolveFocusedCompletedTabAgent({}, undefined, 'tab-1')).toBeNull() + expect(resolveFocusedRetainedTabAgent({}, undefined, 'tab-1')).toBeNull() + }) + + it('re-indexes when the store replaces the map identity', () => { + const paneKey = `tab-1:${leafId(0)}` + const before = { [paneKey]: statusEntry(paneKey, 'done', 'claude') } + expect(resolveFocusedCompletedTabAgent(before, undefined, 'tab-1')).toBe('claude') + const after = { [paneKey]: statusEntry(paneKey, 'done', 'codex') } + expect(resolveFocusedCompletedTabAgent(after, undefined, 'tab-1')).toBe('codex') + expect(resolveFocusedCompletedTabAgent(before, undefined, 'tab-1')).toBe('claude') + }) + + it('scans each source identity once across tabs and resolver variants', () => { + const firstPaneKey = `tab-1:${leafId(0)}` + const secondPaneKey = `tab-2:${leafId(1)}` + const status = countEntryScans({ + [firstPaneKey]: statusEntry(firstPaneKey, 'working', 'claude'), + [secondPaneKey]: statusEntry(secondPaneKey, 'done', 'codex') + }) + const retained = countEntryScans({ + [firstPaneKey]: retainedEntry(firstPaneKey, 'claude'), + [secondPaneKey]: retainedEntry(secondPaneKey, 'codex') + }) + + for (const tabId of ['tab-1', 'tab-2', 'tab-absent']) { + const layout = layoutOf(leafId(9)) + resolveFocusedTabAgent(status.source, undefined, tabId) + resolveSiblingTabAgent(status.source, layout, tabId) + resolveFocusedCompletedTabAgent(status.source, undefined, tabId) + resolveSiblingCompletedTabAgent(status.source, layout, tabId) + resolveFocusedRetainedTabAgent(retained.source, undefined, tabId) + resolveSiblingRetainedTabAgent(retained.source, layout, tabId) + } + + expect(status.scans()).toBe(1) + expect(retained.scans()).toBe(1) + + const replacement = countEntryScans({ ...status.source }) + resolveFocusedTabAgent(replacement.source, undefined, 'tab-1') + resolveFocusedCompletedTabAgent(replacement.source, undefined, 'tab-2') + expect(replacement.scans()).toBe(1) + }) +}) diff --git a/src/renderer/src/lib/tab-agent-status-index.ts b/src/renderer/src/lib/tab-agent-status-index.ts new file mode 100644 index 000000000..daa1b4fb2 --- /dev/null +++ b/src/renderer/src/lib/tab-agent-status-index.ts @@ -0,0 +1,117 @@ +import type { AgentStatusEntry } from '../../../shared/agent-status-types' +import type { TuiAgent } from '../../../shared/types' +import { parsePaneKey } from '../../../shared/stable-pane-id' +import type { RetainedAgentEntry } from '@/store/slices/agent-status' +import { agentTypeToIconAgent } from './agent-status' + +/** + * Per-tab index of icon-capable agent panes for the tab-bar resolvers in + * `tab-agent.ts`. Without it each of ~200 mounted tabs re-scanned (and + * re-parsed the pane key of) the whole global status map on every render. + * + * Panes keep the source map's insertion order because the resolvers return the + * FIRST match — order decides which icon a split tab shows. + */ +export type TabAgentPane = { readonly leafId: string; readonly agent: TuiAgent } + +type TabAgentPanesByTabId = ReadonlyMap + +const NO_PANES: readonly TabAgentPane[] = Object.freeze([]) +const EMPTY_INDEX: TabAgentPanesByTabId = new Map() + +function appendPane(index: Map, tabId: string, pane: TabAgentPane): void { + const panes = index.get(tabId) + if (panes) { + panes.push(pane) + } else { + index.set(tabId, [pane]) + } +} + +// Why: the store replaces these maps on every write, so identity is an exact +// invalidation signal — one scan per write instead of one per tab per render. +let cachedStatusSource: Record | null = null +let cachedLiveIndex: TabAgentPanesByTabId = EMPTY_INDEX +let cachedCompletedIndex: TabAgentPanesByTabId = EMPTY_INDEX + +function indexAgentStatus(source: Record): void { + if (source === cachedStatusSource) { + return + } + const live = new Map() + const completed = new Map() + for (const [paneKey, entry] of Object.entries(source)) { + const agent = agentTypeToIconAgent(entry?.agentType) + if (!agent) { + continue + } + const parsed = parsePaneKey(paneKey) + if (!parsed) { + continue + } + appendPane(entry.state === 'done' ? completed : live, parsed.tabId, { + leafId: parsed.leafId, + agent + }) + } + cachedLiveIndex = live + cachedCompletedIndex = completed + cachedStatusSource = source +} + +/** Panes of `tabId` whose agent is still running (any non-`done` state). */ +export function selectLiveTabAgentPanes( + agentStatusByPaneKey: Record, + tabId: string +): readonly TabAgentPane[] { + indexAgentStatus(agentStatusByPaneKey) + return cachedLiveIndex.get(tabId) ?? NO_PANES +} + +/** Panes of `tabId` whose agent reported `done`. */ +export function selectCompletedTabAgentPanes( + agentStatusByPaneKey: Record, + tabId: string +): readonly TabAgentPane[] { + indexAgentStatus(agentStatusByPaneKey) + return cachedCompletedIndex.get(tabId) ?? NO_PANES +} + +let cachedRetainedSource: Record | null = null +let cachedRetainedIndex: TabAgentPanesByTabId = EMPTY_INDEX + +/** Panes of `tabId` kept as sidebar completion evidence after the row went away. */ +export function selectRetainedTabAgentPanes( + retainedAgentsByPaneKey: Record, + tabId: string +): readonly TabAgentPane[] { + if (retainedAgentsByPaneKey !== cachedRetainedSource) { + const retainedIndex = new Map() + for (const [paneKey, retained] of Object.entries(retainedAgentsByPaneKey)) { + const agent = agentTypeToIconAgent(retained?.agentType) + if (!agent) { + continue + } + const parsed = parsePaneKey(paneKey) + if (!parsed) { + continue + } + appendPane(retainedIndex, parsed.tabId, { leafId: parsed.leafId, agent }) + } + cachedRetainedIndex = retainedIndex + cachedRetainedSource = retainedAgentsByPaneKey + } + return cachedRetainedIndex.get(tabId) ?? NO_PANES +} + +export function firstTabAgentExcludingLeaf( + panes: readonly TabAgentPane[], + excludedLeafId?: string +): TuiAgent | null { + for (const pane of panes) { + if (pane.leafId !== excludedLeafId) { + return pane.agent + } + } + return null +} diff --git a/src/renderer/src/lib/tab-agent.ts b/src/renderer/src/lib/tab-agent.ts index 3fa2da2a6..5512ce5e8 100644 --- a/src/renderer/src/lib/tab-agent.ts +++ b/src/renderer/src/lib/tab-agent.ts @@ -1,8 +1,14 @@ import type { AgentStatusEntry } from '../../../shared/agent-status-types' import type { TerminalLayoutSnapshot, TuiAgent } from '../../../shared/types' -import { isTerminalLeafId, makePaneKey, parsePaneKey } from '../../../shared/stable-pane-id' +import { isTerminalLeafId, makePaneKey } from '../../../shared/stable-pane-id' import type { RetainedAgentEntry } from '@/store/slices/agent-status' import { agentTypeToIconAgent } from './agent-status' +import { + firstTabAgentExcludingLeaf, + selectCompletedTabAgentPanes, + selectLiveTabAgentPanes, + selectRetainedTabAgentPanes +} from './tab-agent-status-index' /** * Resolve a terminal tab's agent from hook-reported status — the PRIMARY @@ -44,16 +50,10 @@ function resolveAnyTabAgent( tabId: string, excludedLeafId?: string ): TuiAgent | null { - for (const [paneKey, entry] of Object.entries(agentStatusByPaneKey)) { - const parsedPaneKey = parsePaneKey(paneKey) - if (parsedPaneKey?.tabId === tabId && parsedPaneKey.leafId !== excludedLeafId) { - const agent = agentFromStatusEntry(entry) - if (agent) { - return agent - } - } - } - return null + return firstTabAgentExcludingLeaf( + selectLiveTabAgentPanes(agentStatusByPaneKey, tabId), + excludedLeafId + ) } function agentFromStatusEntry(entry: AgentStatusEntry | undefined): TuiAgent | null { @@ -93,16 +93,10 @@ function resolveAnyCompletedTabAgent( tabId: string, excludedLeafId?: string ): TuiAgent | null { - for (const [paneKey, entry] of Object.entries(agentStatusByPaneKey)) { - const parsedPaneKey = parsePaneKey(paneKey) - if (parsedPaneKey?.tabId === tabId && parsedPaneKey.leafId !== excludedLeafId) { - const agent = completedAgentFromStatusEntry(entry) - if (agent) { - return agent - } - } - } - return null + return firstTabAgentExcludingLeaf( + selectCompletedTabAgentPanes(agentStatusByPaneKey, tabId), + excludedLeafId + ) } function completedAgentFromStatusEntry(entry: AgentStatusEntry | undefined): TuiAgent | null { @@ -142,16 +136,10 @@ function resolveAnyRetainedTabAgent( tabId: string, excludedLeafId?: string ): TuiAgent | null { - for (const [paneKey, retained] of Object.entries(retainedAgentsByPaneKey)) { - const parsedPaneKey = parsePaneKey(paneKey) - if (parsedPaneKey?.tabId === tabId && parsedPaneKey.leafId !== excludedLeafId) { - const agent = agentFromRetainedEntry(retained) - if (agent) { - return agent - } - } - } - return null + return firstTabAgentExcludingLeaf( + selectRetainedTabAgentPanes(retainedAgentsByPaneKey, tabId), + excludedLeafId + ) } function agentFromRetainedEntry(entry: RetainedAgentEntry | undefined): TuiAgent | null {