From c4e397bcdc4fd66d31266c0e7aabfc4b2ad024ac Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 15 Aug 2026 04:27:58 -0700 Subject: [PATCH] perf(renderer): index web session editor tab reconciliation (#8098) Replace the per-editor-tab Array.find over the worktree's unified tabs with a lazily-built id/fileId map, and swap two O(n^2) includes-in-a-loop scans for Set membership. The map is only materialized when a snapshot actually carries a mirrored editor tab, so terminal-only snapshots pay nothing. --- .../web-session-existing-tab-index.test.ts | 50 ++++++++++++++++ .../runtime/web-session-existing-tab-index.ts | 60 +++++++++++++++++++ .../src/runtime/web-session-tabs-sync.ts | 38 +++++------- 3 files changed, 125 insertions(+), 23 deletions(-) create mode 100644 src/renderer/src/runtime/web-session-existing-tab-index.test.ts create mode 100644 src/renderer/src/runtime/web-session-existing-tab-index.ts diff --git a/src/renderer/src/runtime/web-session-existing-tab-index.test.ts b/src/renderer/src/runtime/web-session-existing-tab-index.test.ts new file mode 100644 index 00000000000..3cfecf0ca77 --- /dev/null +++ b/src/renderer/src/runtime/web-session-existing-tab-index.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest' +import type { Tab } from '../../../shared/tab-types' +import { buildWebSessionExistingTabIndex } from './web-session-existing-tab-index' + +const WT = 'repo::/worktree' + +function makeTab(id: string, entityId: string, contentType: 'editor' | 'browser'): Tab { + return { + id, + entityId, + groupId: 'group-1', + worktreeId: WT, + contentType, + label: id, + customLabel: null, + color: null, + sortOrder: 0, + createdAt: 1 + } +} + +describe('buildWebSessionExistingTabIndex', () => { + it('preserves first-match editor lookup behavior across both accepted keys', () => { + const firstFileId = '/repo/file.ts' + const firstByFileId = makeTab('older-host-id', firstFileId, 'editor') + const laterByHostId = makeTab('current-host-id', '/repo/other.ts', 'editor') + const index = buildWebSessionExistingTabIndex({ + unifiedTabs: [firstByFileId, laterByHostId] + }) + + expect(index.getEditorUnifiedTab(firstFileId, laterByHostId.id)).toBe(firstByFileId) + expect(index.getEditorUnifiedTab(laterByHostId.entityId, firstByFileId.id)).toBe(firstByFileId) + }) + + it('ignores browser tabs and returns null when neither key matches', () => { + const index = buildWebSessionExistingTabIndex({ + unifiedTabs: [makeTab('browser-tab', 'workspace-1', 'browser')] + }) + + expect(index.getEditorUnifiedTab('/repo/missing.ts', 'absent-host-id')).toBeNull() + }) + + it('resolves by host tab id and by file id independently', () => { + const byHostId = makeTab('host-1', '/repo/a.ts', 'editor') + const index = buildWebSessionExistingTabIndex({ unifiedTabs: [byHostId] }) + + expect(index.getEditorUnifiedTab('/repo/unrelated.ts', 'host-1')).toBe(byHostId) + expect(index.getEditorUnifiedTab('/repo/a.ts', 'unrelated-host')).toBe(byHostId) + }) +}) diff --git a/src/renderer/src/runtime/web-session-existing-tab-index.ts b/src/renderer/src/runtime/web-session-existing-tab-index.ts new file mode 100644 index 00000000000..5245b13169b --- /dev/null +++ b/src/renderer/src/runtime/web-session-existing-tab-index.ts @@ -0,0 +1,60 @@ +import type { Tab } from '../../../shared/tab-types' + +type PositionedTab = { + position: number + tab: Tab +} + +export type WebSessionExistingTabIndex = { + getEditorUnifiedTab: (fileId: string, hostTabId: string) => Tab | null +} + +type BuildWebSessionExistingTabIndexArgs = { + unifiedTabs: readonly Tab[] +} + +function setFirst(map: Map, key: K, value: V): void { + if (!map.has(key)) { + map.set(key, value) + } +} + +export function buildWebSessionExistingTabIndex({ + unifiedTabs +}: BuildWebSessionExistingTabIndexArgs): WebSessionExistingTabIndex { + let indexes: { + editorTabById: Map + editorTabByFileId: Map + } | null = null + // Why: terminal-only snapshots are the common case, so a snapshot carrying no + // mirrored editor tab never pays to walk the worktree's unified tab list. + const getIndexes = (): NonNullable => { + if (!indexes) { + const editorTabById = new Map() + const editorTabByFileId = new Map() + unifiedTabs.forEach((tab, position) => { + if (tab.contentType === 'editor') { + const positioned = { position, tab } + setFirst(editorTabById, tab.id, positioned) + setFirst(editorTabByFileId, tab.entityId, positioned) + } + }) + indexes = { editorTabById, editorTabByFileId } + } + return indexes + } + + return { + getEditorUnifiedTab: (fileId, hostTabId) => { + const { editorTabById, editorTabByFileId } = getIndexes() + const byHostId = editorTabById.get(hostTabId) + const byFileId = editorTabByFileId.get(fileId) + // Why: the former Array.find accepted either key, so duplicate legacy + // entries must still resolve to whichever candidate appeared first. + if (byHostId && byFileId) { + return byHostId.position <= byFileId.position ? byHostId.tab : byFileId.tab + } + return byHostId?.tab ?? byFileId?.tab ?? null + } + } +} diff --git a/src/renderer/src/runtime/web-session-tabs-sync.ts b/src/renderer/src/runtime/web-session-tabs-sync.ts index 9ee15c8421f..2a5eb362dfb 100644 --- a/src/renderer/src/runtime/web-session-tabs-sync.ts +++ b/src/renderer/src/runtime/web-session-tabs-sync.ts @@ -96,6 +96,10 @@ import { resetWebSessionBrowserPlacementsForTests } from './web-session-browser-placement' import { suppressE2eWebRuntimeBrowserSnapshot } from './web-runtime-browser-creation-e2e-fault' +import { + buildWebSessionExistingTabIndex, + type WebSessionExistingTabIndex +} from './web-session-existing-tab-index' const WEB_SESSION_GROUP_PREFIX = 'web-session-tabs:' export const WEB_SESSION_TABS_VISIBILITY_RESUME_STAGGER_MS = 100 @@ -1394,24 +1398,11 @@ function buildEditorUnifiedTab( } } -function findExistingEditorUnifiedTab( - state: WebSessionTabsSyncState, - worktreeId: string, - fileId: string, - hostTabId: string -): Tab | null { - return ( - (state.unifiedTabsByWorktree[worktreeId] ?? []).find( - (tab) => tab.contentType === 'editor' && (tab.id === hostTabId || tab.entityId === fileId) - ) ?? null - ) -} - function buildMirroredEditorTabs( snapshot: RuntimeMobileSessionTabsResult, environmentId: string, - state: WebSessionTabsSyncState, worktreeOpenFileById: ReadonlyMap, + existingTabIndex: WebSessionExistingTabIndex, hostGroupIdByTabId: ReadonlyMap, fallbackGroupId: string, sortOffset: number, @@ -1420,12 +1411,7 @@ function buildMirroredEditorTabs( return snapshot.tabs.filter(isReadyEditorTab).map((tab, index) => { const fileId = localEditorFileId(tab) const existingFile = worktreeOpenFileById.get(fileId) - const existingUnifiedTab = findExistingEditorUnifiedTab( - state, - snapshot.worktree, - fileId, - tab.id - ) + const existingUnifiedTab = existingTabIndex.getEditorUnifiedTab(fileId, tab.id) const sourceFileId = editorSourceFileId(tab) const groupId = hostGroupIdByTabId.get(tab.id) ?? fallbackGroupId const file: OpenFile = { @@ -1850,8 +1836,9 @@ function buildMirroredHostGroups({ validUnifiedTabIds.has(tabId) && !clientGroupIdByLocalTabId.has(tabId) ) + const localHostOrderIds = new Set(localHostOrder) const hostTabOrder = [ - ...(existing?.tabOrder.filter((tabId) => !localHostOrder.includes(tabId)) ?? []), + ...(existing?.tabOrder.filter((tabId) => !localHostOrderIds.has(tabId)) ?? []), ...localHostOrder ] // Why: a pending client reorder wins over a stale pre-move host order until the host echoes the move (or membership changes). @@ -2459,6 +2446,9 @@ function applyWebSessionTabsSnapshotWithContext( const targetGroupId = chooseTargetGroupId(state, snapshot) const hostGroupIdByTabId = buildHostGroupIdByTabId(snapshot.tabGroups) + const existingTabIndex = buildWebSessionExistingTabIndex({ + unifiedTabs: state.unifiedTabsByWorktree[worktreeId] ?? [] + }) const readyBrowserTabs = snapshot.tabs.filter(isReadyBrowserTab) const nextRemoteBrowserPageIds = new Set(readyBrowserTabs.map((tab) => tab.browserPageId)) const mirroredBrowserTabs = buildMirroredBrowserTabs( @@ -2505,8 +2495,8 @@ function applyWebSessionTabsSnapshotWithContext( const mirroredEditorTabs = buildMirroredEditorTabs( snapshot, environmentId, - state, firstOpenFileByIdForWorktree(worktreeOpenFiles), + existingTabIndex, hostGroupIdByTabId, targetGroupId, mirroredTerminalTabEntries.length + mirroredBrowserTabs.length, @@ -2830,8 +2820,10 @@ function applyWebSessionTabsSnapshotWithContext( .filter((tabId): tabId is string => tabId !== undefined && validTabBarIds.has(tabId)) ) ?? [] const next: string[] = [] + const seen = new Set() const push = (tabId: string): void => { - if (validTabBarIds.has(tabId) && !next.includes(tabId)) { + if (validTabBarIds.has(tabId) && !seen.has(tabId)) { + seen.add(tabId) next.push(tabId) } }