fix(editor): make Close All linear instead of cubic in tab count

closeAllFiles resolved a reopen position for every closing tab by calling
getRecentlyClosedTabPosition, which itself rescanned the worktree's tab-bar
order and group membership with a nested find per entry. That made the whole
bulk close O(N^3) in open editor tabs, and because it runs inside a synchronous
zustand set() the renderer is blocked for the entire duration: measured against
the real store, 500 tabs took 1.2s and 1000 tabs took 13.0s of frozen UI.
Sessions that accumulate tabs — long SSH/relay work, a mirrored host republishing
open files, a folder workspace opened wide — hit this on a single menu click.

Hoist the per-tab rescans into one precomputed index. The new
createRecentlyClosedTabPositionIndex builds tab-bar, unified-tab, and group
lookups in a single linear pass over the worktree's state, then answers each
position query in O(1); getRecentlyClosedTabPosition now delegates to it so
single-tab callers keep identical behavior. Positions, reopen order, and the
tab-bar/group indices recorded by #12236 are unchanged — first-occurrence wins
everywhere, matching the indexOf/find scans this replaces.

Position derivation moves to its own module so recently-closed-tabs.ts stays
under the max-lines limit without a suppression; existing importers are
unaffected via re-export.

Same measurement after the change: 500 tabs 44ms, 1000 tabs 142ms, and the
instrumented scan count drops from 503,000,500 to 4,000 at N=1000.
This commit is contained in:
OrcaWin
2026-08-03 22:35:42 -04:00
parent a69ca6d5b7
commit 5e899bd06f
4 changed files with 245 additions and 108 deletions
@@ -97,6 +97,22 @@ function ownedEditorFileId(
return `editor:${encodeURIComponent(worktreeId)}:${encodeURIComponent(runtimeKey)}:${encodeURIComponent(filePath)}`
}
/** Counts how often store code reads `entityId` off a worktree's unified tabs —
* a scan-count proxy that pins complexity without timing the wall clock. */
function withCountedEntityIdReads(tabs: readonly Tab[], onRead: () => void): Tab[] {
return tabs.map((tab) => {
const { entityId, ...rest } = tab
return Object.defineProperty(rest, 'entityId', {
get: () => {
onRead()
return entityId
},
enumerable: true,
configurable: true
}) as Tab
})
}
function mirroredEditorUnifiedTab(id: string, entityId: string, worktreeId: string): Tab {
return {
id,
@@ -1712,6 +1728,50 @@ describe('createEditorSlice recently closed editor tabs', () => {
expect(store.getState().openFiles[0]?.id).toBe(firstId)
expect(store.getState().tabBarOrderByWorktree['wt-1']).toEqual([firstId])
})
it('captures close-all snapshot positions without rescanning tab state per closed tab', () => {
const countEntityIdReads = (fileCount: number): number => {
const store = createEditorTabsStore()
for (let index = 0; index < fileCount; index += 1) {
store.getState().openFile(
{
filePath: `/repo/file-${index}.ts`,
relativePath: `file-${index}.ts`,
worktreeId: 'wt-1',
language: 'typescript',
mode: 'edit'
},
{ preview: false }
)
}
let reads = 0
store.setState({
unifiedTabsByWorktree: {
'wt-1': withCountedEntityIdReads(
store.getState().unifiedTabsByWorktree['wt-1'] ?? [],
() => {
reads += 1
}
)
}
} as Partial<AppState>)
reads = 0
store.getState().closeAllFiles()
expect(store.getState().openFiles).toHaveLength(0)
expect(store.getState().recentlyClosedEditorTabsByWorktree['wt-1']?.[0]?.position).toEqual({
tabBarIndex: 0,
groupId: expect.any(String),
groupIndex: 0
})
return reads
}
// Why: resolving each closed tab's position by rescanning tab order and group membership
// made close-all cubic — 4x the tabs cost ~64x the scans and froze the renderer for seconds.
expect(countEntityIdReads(80)).toBeLessThanOrEqual(countEntityIdReads(20) * 8)
})
})
describe('createEditorSlice markdown view state', () => {
+4 -1
View File
@@ -2,6 +2,7 @@
import type { StateCreator } from 'zustand'
import type { AppState } from '../types'
import {
createRecentlyClosedTabPositionIndex,
getRecentlyClosedTabPosition,
restoreRecentlyClosedTabPosition,
pushRecentlyClosedTabKind
@@ -2394,6 +2395,8 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
const closingFiles = s.openFiles.filter((f) => f.worktreeId === activeWorktreeId)
let nextRecentClosed = s.recentlyClosedEditorTabsByWorktree[activeWorktreeId] ?? []
let capturedCloseCount = 0
// Why: one shared index — a per-file position lookup rescans tab order and group membership, making close-all cubic.
const positionIndex = createRecentlyClosedTabPositionIndex(s, activeWorktreeId)
for (const f of [...closingFiles].toReversed()) {
// Why: skip untitled non-dirty files (deleted from disk after close) and ephemeral preview tabs so the reopen stack has no vanished/junk paths.
if (
@@ -2403,7 +2406,7 @@ export const createEditorSlice: StateCreator<AppState, [], [], EditorSlice> = (s
continue
}
const { id: _id, isDirty: _dirty, mirroredFromRuntimeSession: _mirrored, ...snap } = f
const position = getRecentlyClosedTabPosition(s, activeWorktreeId, f.id)
const position = positionIndex.positionFor(f.id)
nextRecentClosed = [
{
...(snap as ClosedEditorTabSnapshot),
@@ -0,0 +1,167 @@
import type { AppState } from '../types'
export type RecentlyClosedTabPosition = {
tabBarIndex?: number
groupId?: string
groupIndex?: number
}
/** Reusable position lookup for one worktree. Bulk closes resolve a position per
* closed tab; without a shared index every lookup rescans tab order and group
* membership, which makes close-all cubic in the tab count. */
export type RecentlyClosedTabPositionIndex = {
positionFor: (entityId: string) => RecentlyClosedTabPosition | undefined
}
export function createRecentlyClosedTabPositionIndex(
state: Pick<AppState, 'tabBarOrderByWorktree' | 'groupsByWorktree' | 'unifiedTabsByWorktree'>,
worktreeId: string
): RecentlyClosedTabPositionIndex {
const tabBarOrder = state.tabBarOrderByWorktree?.[worktreeId]
const unifiedTabs = state.unifiedTabsByWorktree?.[worktreeId] ?? []
const groups = state.groupsByWorktree?.[worktreeId] ?? []
// First occurrence wins everywhere, matching the `indexOf`/`find` scans this replaces.
const tabBarIndexByEntityId = new Map<string, number>()
const tabByEntityId = new Map<string, (typeof unifiedTabs)[number]>()
const tabById = new Map<string, (typeof unifiedTabs)[number]>()
for (const tab of unifiedTabs) {
if (!tabByEntityId.has(tab.entityId)) {
tabByEntityId.set(tab.entityId, tab)
}
if (!tabById.has(tab.id)) {
tabById.set(tab.id, tab)
}
}
const tabBarEntityIdsByGroupId = new Map<string, (string | undefined)[]>()
tabBarOrder?.forEach((entityId, index) => {
if (!tabBarIndexByEntityId.has(entityId)) {
tabBarIndexByEntityId.set(entityId, index)
}
const tab = tabByEntityId.get(entityId)
if (!tab?.groupId) {
return
}
const bucket = tabBarEntityIdsByGroupId.get(tab.groupId)
if (bucket) {
bucket.push(tab.entityId)
} else {
tabBarEntityIdsByGroupId.set(tab.groupId, [tab.entityId])
}
})
const groupById = new Map<string, (typeof groups)[number]>()
const tabOrderIndexByGroupId = new Map<string, Map<string, number>>()
const groupOrderMatchesTabBarByGroupId = new Map<string, boolean>()
for (const group of groups) {
if (groupById.has(group.id)) {
continue
}
groupById.set(group.id, group)
const indexByTabId = new Map<string, number>()
const groupTabEntityIds = group.tabOrder.map((tabId, index) => {
if (!indexByTabId.has(tabId)) {
indexByTabId.set(tabId, index)
}
return tabById.get(tabId)?.entityId
})
tabOrderIndexByGroupId.set(group.id, indexByTabId)
const tabBarGroupEntityIds = tabBarEntityIdsByGroupId.get(group.id) ?? []
groupOrderMatchesTabBarByGroupId.set(
group.id,
groupTabEntityIds.length === tabBarGroupEntityIds.length &&
groupTabEntityIds.every((entityId, index) => entityId === tabBarGroupEntityIds[index])
)
}
return {
positionFor: (entityId) => {
const tabBarIndex = tabBarOrder ? (tabBarIndexByEntityId.get(entityId) ?? -1) : -1
const unifiedTab = tabByEntityId.get(entityId)
const group = unifiedTab ? groupById.get(unifiedTab.groupId) : undefined
const groupIndex =
group && unifiedTab ? (tabOrderIndexByGroupId.get(group.id)?.get(unifiedTab.id) ?? -1) : -1
if (tabBarIndex < 0 && (!group || groupIndex < 0)) {
return undefined
}
const groupOrderMatchesTabBar =
!group || (groupOrderMatchesTabBarByGroupId.get(group.id) ?? true)
return {
...(tabBarIndex >= 0 && groupOrderMatchesTabBar ? { tabBarIndex } : {}),
...(group && groupIndex >= 0 ? { groupId: group.id, groupIndex } : {})
}
}
}
}
export function getRecentlyClosedTabPosition(
state: Pick<AppState, 'tabBarOrderByWorktree' | 'groupsByWorktree' | 'unifiedTabsByWorktree'>,
worktreeId: string,
entityId: string
): RecentlyClosedTabPosition | undefined {
return createRecentlyClosedTabPositionIndex(state, worktreeId).positionFor(entityId)
}
export function insertTabAtRecentlyClosedPosition(
order: readonly string[],
tabId: string,
position?: RecentlyClosedTabPosition
): string[] {
const nextOrder = order.filter((id) => id !== tabId)
const index = position?.tabBarIndex
if (index === undefined) {
return [...nextOrder, tabId]
}
nextOrder.splice(Math.min(Math.max(index, 0), nextOrder.length), 0, tabId)
return nextOrder
}
export function restoreRecentlyClosedTabPosition(
getState: () => Pick<
AppState,
| 'tabBarOrderByWorktree'
| 'groupsByWorktree'
| 'unifiedTabsByWorktree'
| 'setTabBarOrder'
| 'reorderUnifiedTabs'
>,
worktreeId: string,
entityId: string,
position?: RecentlyClosedTabPosition
): void {
if (!position) {
return
}
const state = getState()
const order = state.tabBarOrderByWorktree?.[worktreeId]
if (order && typeof state.setTabBarOrder === 'function') {
state.setTabBarOrder(worktreeId, insertTabAtRecentlyClosedPosition(order, entityId, position))
}
if (position?.groupIndex === undefined) {
return
}
const unifiedTab = (getState().unifiedTabsByWorktree?.[worktreeId] ?? []).find(
(candidate) =>
candidate.entityId === entityId &&
(position.groupId === undefined || candidate.groupId === position.groupId)
)
if (!unifiedTab) {
return
}
const group = (getState().groupsByWorktree?.[worktreeId] ?? []).find(
(candidate) => candidate.id === unifiedTab.groupId
)
if (!group) {
return
}
if (typeof getState().reorderUnifiedTabs === 'function') {
getState().reorderUnifiedTabs(
group.id,
insertTabAtRecentlyClosedPosition(group.tabOrder, unifiedTab.id, {
tabBarIndex: position.groupIndex
}),
{ recordInteraction: false }
)
}
}
@@ -5,114 +5,21 @@ import {
isWindowsAbsolutePathLike,
relativePathInsideRoot
} from '../../../../shared/cross-platform-path'
import {
restoreRecentlyClosedTabPosition,
type RecentlyClosedTabPosition
} from './recently-closed-tab-position'
export type RecentlyClosedTabPosition = {
tabBarIndex?: number
groupId?: string
groupIndex?: number
}
export function getRecentlyClosedTabPosition(
state: Pick<AppState, 'tabBarOrderByWorktree' | 'groupsByWorktree' | 'unifiedTabsByWorktree'>,
worktreeId: string,
entityId: string
): RecentlyClosedTabPosition | undefined {
const tabBarOrder = state.tabBarOrderByWorktree?.[worktreeId]
const unifiedTabs = state.unifiedTabsByWorktree?.[worktreeId] ?? []
const tabBarIndex = tabBarOrder?.indexOf(entityId) ?? -1
const unifiedTab = unifiedTabs.find((tab) => tab.entityId === entityId)
const group = unifiedTab
? (state.groupsByWorktree?.[worktreeId] ?? []).find(
(candidate) => candidate.id === unifiedTab.groupId
)
: undefined
const groupIndex = group?.tabOrder.indexOf(unifiedTab?.id ?? '') ?? -1
const groupTabEntityIds = group
? group.tabOrder.map((tabId) => unifiedTabs.find((tab) => tab.id === tabId)?.entityId)
: []
const tabBarGroupEntityIds = group
? (tabBarOrder ?? [])
.map((tabId) => unifiedTabs.find((tab) => tab.entityId === tabId))
.filter((tab) => tab?.groupId === group.id)
.map((tab) => tab?.entityId)
: []
const groupOrderMatchesTabBar =
!group ||
(groupTabEntityIds.length === tabBarGroupEntityIds.length &&
groupTabEntityIds.every((entityId, index) => entityId === tabBarGroupEntityIds[index]))
if (tabBarIndex < 0 && (!group || groupIndex < 0)) {
return undefined
}
return {
...(tabBarIndex >= 0 && groupOrderMatchesTabBar ? { tabBarIndex } : {}),
...(group && groupIndex >= 0 ? { groupId: group.id, groupIndex } : {})
}
}
export function insertTabAtRecentlyClosedPosition(
order: readonly string[],
tabId: string,
position?: RecentlyClosedTabPosition
): string[] {
const nextOrder = order.filter((id) => id !== tabId)
const index = position?.tabBarIndex
if (index === undefined) {
return [...nextOrder, tabId]
}
nextOrder.splice(Math.min(Math.max(index, 0), nextOrder.length), 0, tabId)
return nextOrder
}
export function restoreRecentlyClosedTabPosition(
getState: () => Pick<
AppState,
| 'tabBarOrderByWorktree'
| 'groupsByWorktree'
| 'unifiedTabsByWorktree'
| 'setTabBarOrder'
| 'reorderUnifiedTabs'
>,
worktreeId: string,
entityId: string,
position?: RecentlyClosedTabPosition
): void {
if (!position) {
return
}
const state = getState()
const order = state.tabBarOrderByWorktree?.[worktreeId]
if (order && typeof state.setTabBarOrder === 'function') {
state.setTabBarOrder(worktreeId, insertTabAtRecentlyClosedPosition(order, entityId, position))
}
if (position?.groupIndex === undefined) {
return
}
const unifiedTab = (getState().unifiedTabsByWorktree?.[worktreeId] ?? []).find(
(candidate) =>
candidate.entityId === entityId &&
(position.groupId === undefined || candidate.groupId === position.groupId)
)
if (!unifiedTab) {
return
}
const group = (getState().groupsByWorktree?.[worktreeId] ?? []).find(
(candidate) => candidate.id === unifiedTab.groupId
)
if (!group) {
return
}
if (typeof getState().reorderUnifiedTabs === 'function') {
getState().reorderUnifiedTabs(
group.id,
insertTabAtRecentlyClosedPosition(group.tabOrder, unifiedTab.id, {
tabBarIndex: position.groupIndex
}),
{ recordInteraction: false }
)
}
}
export {
createRecentlyClosedTabPositionIndex,
getRecentlyClosedTabPosition,
insertTabAtRecentlyClosedPosition,
restoreRecentlyClosedTabPosition
} from './recently-closed-tab-position'
export type {
RecentlyClosedTabPosition,
RecentlyClosedTabPositionIndex
} from './recently-closed-tab-position'
/** Snapshot of a terminal tab captured at user-initiated close time. Reopen
* recreates a fresh shell in the same startup directory (Ghostty semantics) —