mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 16:03:20 +00:00
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:
@@ -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', () => {
|
||||
|
||||
@@ -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) —
|
||||
|
||||
Reference in New Issue
Block a user