From e15bb949d73e2c802d4db533039aaecef1409481 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 30 May 2026 14:11:19 -0700 Subject: [PATCH] perf: cache terminal tab lookup per tab array (#3938) --- .../components/terminal-pane/TerminalPane.tsx | 5 +- .../terminal-pane/terminal-tab-lookup.test.ts | 57 +++++++++++++++++++ .../terminal-pane/terminal-tab-lookup.ts | 25 ++++++++ 3 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 src/renderer/src/components/terminal-pane/terminal-tab-lookup.test.ts create mode 100644 src/renderer/src/components/terminal-pane/terminal-tab-lookup.ts diff --git a/src/renderer/src/components/terminal-pane/TerminalPane.tsx b/src/renderer/src/components/terminal-pane/TerminalPane.tsx index 125bd8ee1f1..c08e9bec7a4 100644 --- a/src/renderer/src/components/terminal-pane/TerminalPane.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalPane.tsx @@ -88,6 +88,7 @@ import { applyTerminalPaneAttentionToManager, subscribeTerminalPaneAttention } from './terminal-pane-attention-subscriptions' +import { getCachedTerminalTabForWorktree } from './terminal-tab-lookup' type TerminalPaneProps = { tabId: string @@ -335,8 +336,8 @@ export default function TerminalPane({ ) const clearCodexRestartNotice = useAppStore((store) => store.clearCodexRestartNotice) const savedLayout = useAppStore((store) => store.terminalLayoutsByTabId[tabId] ?? EMPTY_LAYOUT) - const terminalTab = useAppStore( - (store) => (store.tabsByWorktree[worktreeId] ?? []).find((tab) => tab.id === tabId) ?? null + const terminalTab = useAppStore((store) => + getCachedTerminalTabForWorktree(store.tabsByWorktree, worktreeId, tabId) ) const setTabLayout = useAppStore((store) => store.setTabLayout) const restoredLayout = useMemo( diff --git a/src/renderer/src/components/terminal-pane/terminal-tab-lookup.test.ts b/src/renderer/src/components/terminal-pane/terminal-tab-lookup.test.ts new file mode 100644 index 00000000000..a31fc9b0ec4 --- /dev/null +++ b/src/renderer/src/components/terminal-pane/terminal-tab-lookup.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it, vi } from 'vitest' +import type { TerminalTab } from '../../../../shared/types' +import { getCachedTerminalTabForWorktree } from './terminal-tab-lookup' + +function makeTab(id: string): TerminalTab { + return { + id, + ptyId: null, + worktreeId: 'wt-1', + title: id, + customTitle: null, + color: null, + sortOrder: 0, + createdAt: 0 + } +} + +function iterableTabs(tabs: TerminalTab[]): { + value: TerminalTab[] + iterator: ReturnType +} { + const iterator = vi.fn(function* () { + yield* tabs + }) + return { + value: { [Symbol.iterator]: iterator } as unknown as TerminalTab[], + iterator + } +} + +describe('getCachedTerminalTabForWorktree', () => { + it('reuses the tab lookup while the worktree tab array is unchanged', () => { + const tabs = Array.from({ length: 200 }, (_, index) => makeTab(`tab-${index}`)) + const { value, iterator } = iterableTabs(tabs) + const tabsByWorktree = { 'wt-1': value } + + expect(getCachedTerminalTabForWorktree(tabsByWorktree, 'wt-1', 'tab-199')).toBe(tabs[199]) + expect(getCachedTerminalTabForWorktree(tabsByWorktree, 'wt-1', 'tab-0')).toBe(tabs[0]) + + expect(iterator).toHaveBeenCalledTimes(1) + }) + + it('rebuilds the lookup when the tab array reference changes', () => { + const first = iterableTabs([makeTab('tab-1')]) + const second = iterableTabs([makeTab('tab-2')]) + + expect(getCachedTerminalTabForWorktree({ 'wt-1': first.value }, 'wt-1', 'tab-1')?.id).toBe( + 'tab-1' + ) + expect(getCachedTerminalTabForWorktree({ 'wt-1': second.value }, 'wt-1', 'tab-2')?.id).toBe( + 'tab-2' + ) + + expect(first.iterator).toHaveBeenCalledTimes(1) + expect(second.iterator).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/renderer/src/components/terminal-pane/terminal-tab-lookup.ts b/src/renderer/src/components/terminal-pane/terminal-tab-lookup.ts new file mode 100644 index 00000000000..74447f0a87e --- /dev/null +++ b/src/renderer/src/components/terminal-pane/terminal-tab-lookup.ts @@ -0,0 +1,25 @@ +import type { TerminalTab } from '../../../../shared/types' + +const terminalTabLookupByArray = new WeakMap>() + +export function getCachedTerminalTabForWorktree( + tabsByWorktree: Record, + worktreeId: string, + tabId: string +): TerminalTab | null { + const tabs = tabsByWorktree[worktreeId] + if (!tabs) { + return null + } + let lookup = terminalTabLookupByArray.get(tabs) + if (!lookup) { + // Why: every mounted TerminalPane asks for its own tab on store updates. + // Build the per-array lookup once so 200 panes do not repeat the same scan. + lookup = new Map() + for (const tab of tabs) { + lookup.set(tab.id, tab) + } + terminalTabLookupByArray.set(tabs, lookup) + } + return lookup.get(tabId) ?? null +}