perf(editor): reuse model owner keys in closed-tab cleanup (#34691)

Each openFiles change (e.g. a dirty toggle) re-resolved every tab's owner
route twice. Cache owner keys per tab until an owner input changes, so a
single-tab update resolves only that tab.
This commit is contained in:
Neil
2026-10-27 20:12:02 -07:00
committed by GitHub
parent 280dee2477
commit 68f0c7999f
4 changed files with 102 additions and 21 deletions
@@ -11,7 +11,6 @@
1 src/renderer/src/components/dashboard/dashboard-worktree-launch-options.ts # P5b-2 must clear: optional state field defaults to empty
4 src/renderer/src/components/dashboard/useDashboardPopoutBridge.ts # P5b-2 must clear: change detection on optional fields never fires when absent
2 src/renderer/src/components/dashboard/useRetainedAgents.ts # reads a passed-in copy
6 src/renderer/src/components/editor/closed-editor-tab-controller.ts # P5b-2 must clear: identity compare of store slices passes when absent
1 src/renderer/src/components/editor/markdown-document-worktree-path-selector.ts # P5b-2 must clear: optional state field
1 src/renderer/src/components/feature-wall/feature-wall-setup-progress.ts # reads a passed-in copy
3 src/renderer/src/components/quick-open-file-list.ts # P5b-2 must clear: store read; file at the max-lines limit
@@ -0,0 +1,42 @@
// @vitest-environment happy-dom
import { afterEach, expect, it, vi } from 'vitest'
import {
createModelLifetimeFixture,
resetModelLifetimeFixtures
} from './editor-model-lifetime-fixture'
import { getEditorModelOwnerKey } from './editor-model-owner'
import type * as EditorModelOwner from './editor-model-owner'
vi.mock('sonner', () => ({ toast: { info: vi.fn(), error: vi.fn(), success: vi.fn() } }))
vi.mock('./editor-model-owner', async (importOriginal) => {
const original = await importOriginal<typeof EditorModelOwner>()
return { ...original, getEditorModelOwnerKey: vi.fn(original.getEditorModelOwnerKey) }
})
afterEach(resetModelLifetimeFixtures)
it('re-resolves only the changed tab until an owner input changes', async () => {
const { store, attach, add } = createModelLifetimeFixture()
const tabs = Array.from({ length: 20 }, (_, index) => add(`tab-${index}`))
attach()
const resolve = vi.mocked(getEditorModelOwnerKey)
const markDirty = (id: string): void =>
store.setState({
openFiles: store
.getState()
.openFiles.map((file) => (file.id === id ? { ...file, isDirty: !file.isDirty } : file))
})
markDirty('tab-0')
resolve.mockClear()
markDirty('tab-1')
expect(resolve).toHaveBeenCalledTimes(1)
resolve.mockClear()
store.setState({ runtimeEnvironments: [] })
expect(resolve).toHaveBeenCalledTimes(tabs.length)
store.getState().closeFile('tab-3')
await Promise.resolve()
expect(tabs[3].model.isDisposed()).toBe(true)
expect(tabs[4].model.isDisposed()).toBe(false)
})
@@ -10,14 +10,14 @@ import {
} from './closed-editor-tab-disposal'
import type { MonacoModelRegistry, DisposableMonacoModel } from './diff-monaco-model-disposal'
import { toEditorModelUri } from './editor-model-uri'
import { getEditorModelOwnerKey } from './editor-model-owner'
import type { WorktreeOperationRouteState } from '@/lib/worktree-operation-route'
import { createEditorModelOwnerKeyCache } from './editor-model-owner-key-cache'
import type { EditorBufferViewIndex } from '@/store/slices/editor/buffers/editor-buffer-views'
import type { EditorBufferKeptRecords } from '@/store/slices/editor/buffers/editor-buffer-kept'
import type { EditorBufferRouteState } from '@/store/slices/editor/buffers/editor-buffer-identity-cache'
type EditorStore = Pick<
StoreApi<
WorktreeOperationRouteState &
EditorBufferRouteState &
Partial<EditorBufferViewIndex> & {
openFiles: OpenFile[]
editorBufferKept?: EditorBufferKeptRecords
@@ -48,6 +48,8 @@ export function attachClosedEditorTabCleanup(
let active = true
let scheduled = false
let generation = 0
const { ownerKeyOf, ownerKeyReaderFor, ownerInputsEqual } =
createEditorModelOwnerKeyCache(previousState)
const releaseRetainedModel = (model: editor.ITextModel): void => {
const retained = retainedModels.get(model)
@@ -138,7 +140,7 @@ export function attachClosedEditorTabCleanup(
openFiles
.filter((openFile) => openFile.mode === 'edit')
.map((openFile) =>
toEditorModelUri(openFile.filePath, getEditorModelOwnerKey(openFile, currentState))
toEditorModelUri(openFile.filePath, ownerKeyOf(openFile, currentState))
)
)
}
@@ -231,40 +233,28 @@ export function attachClosedEditorTabCleanup(
if (
openFiles === previous &&
// Detach/rejoin can change model ownership without replacing openFiles.
currentState.editorBufferIdOf === closedState.editorBufferIdOf &&
currentState.editorBufferKept === closedState.editorBufferKept &&
currentState.repos === closedState.repos &&
currentState.worktreesByRepo === closedState.worktreesByRepo &&
currentState.detectedWorktreesByRepo === closedState.detectedWorktreesByRepo &&
currentState.folderWorkspaces === closedState.folderWorkspaces &&
currentState.projectGroups === closedState.projectGroups &&
currentState.restoredRuntimeHostIdByWorkspaceSessionKey ===
closedState.restoredRuntimeHostIdByWorkspaceSessionKey &&
currentState.runtimeEnvironmentCatalogHydrated ===
closedState.runtimeEnvironmentCatalogHydrated &&
currentState.removedRuntimeEnvironmentIds === closedState.removedRuntimeEnvironmentIds &&
currentState.runtimeEnvironments === closedState.runtimeEnvironments &&
currentState.settings === closedState.settings
ownerInputsEqual(currentState, closedState)
) {
return
}
const priorOwnerOf = ownerKeyReaderFor(closedState)
const liveFiles = new Map(openFiles.map((file) => [file.id, file]))
let removed = false
let removedDiff = false
for (const file of previous) {
const priorOwner = getEditorModelOwnerKey(file, closedState)
const liveFile = liveFiles.get(file.id)
if (
!liveFile ||
liveFile.mode !== file.mode ||
liveFile.filePath !== file.filePath ||
(file.mode === 'edit' && getEditorModelOwnerKey(liveFile, currentState) !== priorOwner)
(file.mode === 'edit' && ownerKeyOf(liveFile, currentState) !== priorOwnerOf(file))
) {
const descriptor = {
id: file.id,
mode: file.mode,
filePath: file.filePath,
modelOwnerKey: priorOwner
modelOwnerKey: priorOwnerOf(file)
}
pendingFiles.set(ownerKey(descriptor), descriptor)
removed = true
@@ -0,0 +1,50 @@
import type { OpenFile } from '@/store/slices/editor'
import type { EditorBufferViewIndex } from '@/store/slices/editor/buffers/editor-buffer-views'
import {
EDITOR_BUFFER_ROUTE_INPUT_KEYS,
type EditorBufferRouteState
} from '@/store/slices/editor/buffers/editor-buffer-identity-cache'
import { getEditorModelOwnerKey } from './editor-model-owner'
type OwnerKeyState = EditorBufferRouteState & Partial<EditorBufferViewIndex>
function ownerInputsEqual(a: OwnerKeyState, b: OwnerKeyState): boolean {
return (
a.editorBufferIdOf === b.editorBufferIdOf &&
EDITOR_BUFFER_ROUTE_INPUT_KEYS.every((key) => a[key] === b[key])
)
}
function cachedOwnerKey(
keys: WeakMap<OpenFile, string>,
file: OpenFile,
state: OwnerKeyState
): string {
let key = keys.get(file)
if (key === undefined) {
key = getEditorModelOwnerKey(file, state)
keys.set(file, key)
}
return key
}
/** Keys stay valid until an owner input changes, so a tab edit re-resolves that tab, not every tab. */
export function createEditorModelOwnerKeyCache(initialState: OwnerKeyState) {
let keyState = initialState
let keys = new WeakMap<OpenFile, string>()
const ownerKeyOf = (file: OpenFile, state: OwnerKeyState): string => {
if (state !== keyState) {
if (!ownerInputsEqual(state, keyState)) {
keys = new WeakMap()
}
keyState = state
}
return cachedOwnerKey(keys, file, state)
}
/** Resolver for `state` that never evicts the current keys, for reading a superseded state. */
const ownerKeyReaderFor = (state: OwnerKeyState): ((file: OpenFile) => string) => {
const stateKeys = ownerInputsEqual(state, keyState) ? keys : new WeakMap<OpenFile, string>()
return (file) => cachedOwnerKey(stateKeys, file, state)
}
return { ownerKeyOf, ownerKeyReaderFor, ownerInputsEqual }
}