mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
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:
@@ -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 }
|
||||
}
|
||||
Reference in New Issue
Block a user