perf(session): gate session-write subscriber on relevant field changes

The App-level Zustand subscriber that debounces buildWorkspaceSessionPayload
fires on every store update (agent status, usage refreshes, runtime title
ticks, …). Each fire reset the 150ms timer, and when the timer eventually
expired the rebuild crossed 70-110ms with many tabs open, tripping
setTimeout violation warnings. Add a shallow reference-equality gate over
the fields actually consumed by the payload so the timer only resets when
those fields change. The field list is co-located with
WorkspaceSessionSnapshot and locked to it via a compile-time exhaustiveness
check, so adding a future snapshot field will fail to typecheck rather
than silently skip the gate.

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
brennanb2025
2026-05-12 12:44:53 -07:00
co-authored by Orca
parent fd86e1869a
commit 09c7ed5cfd
3 changed files with 116 additions and 3 deletions
+28 -1
View File
@@ -34,7 +34,7 @@ import {
} from './runtime/sync-runtime-graph'
import { useGlobalFileDrop } from './hooks/useGlobalFileDrop'
import { registerUpdaterBeforeUnloadBypass } from './lib/updater-beforeunload'
import { buildWorkspaceSessionPayload } from './lib/workspace-session'
import { buildWorkspaceSessionPayload, SESSION_RELEVANT_FIELDS } from './lib/workspace-session'
import { countWorkingAgents, getWorkingAgentsPerWorktree } from './lib/agent-status'
import { activateAndRevealWorktree } from './lib/worktree-activation'
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
@@ -385,10 +385,37 @@ function App(): React.JSX.Element {
// App's render cycle, eliminating re-renders on every tab/file/browser change.
useEffect(() => {
let timer: number | null = null
// Why: the subscriber fires on every store update (agent status, usage
// refreshes, runtime title ticks, …). Without this gate each fire reset
// the debounce, and when it finally expired buildWorkspaceSessionPayload
// crossed 70-110ms with many tabs, tripping setTimeout violations. Compare
// each session-feeding field by reference against the prior snapshot and
// skip both the timer reset and the rebuild when none changed. `null`
// sentinel guarantees the very first fire always proceeds.
let prev: Record<string, unknown> | null = null
const unsub = useAppStore.subscribe((state) => {
if (!state.workspaceSessionReady) {
return
}
let changed = false
if (prev === null) {
changed = true
} else {
for (const key of SESSION_RELEVANT_FIELDS) {
if (prev[key] !== state[key]) {
changed = true
break
}
}
}
if (!changed) {
return
}
const next: Record<string, unknown> = {}
for (const key of SESSION_RELEVANT_FIELDS) {
next[key] = state[key]
}
prev = next
if (timer) {
window.clearTimeout(timer)
}
+50 -1
View File
@@ -1,5 +1,9 @@
import { describe, expect, it } from 'vitest'
import { buildWorkspaceSessionPayload } from './workspace-session'
import {
buildWorkspaceSessionPayload,
SESSION_RELEVANT_FIELDS,
type WorkspaceSessionSnapshot
} from './workspace-session'
import type { AppState } from '../store'
function createSnapshot(overrides: Partial<AppState> = {}): AppState {
@@ -140,3 +144,48 @@ describe('buildWorkspaceSessionPayload', () => {
expect(payload.activeTabTypeByWorktree).toEqual({ 'wt-2': 'terminal' })
})
})
describe('SESSION_RELEVANT_FIELDS', () => {
// Why: this list gates the App-level session-write debounce subscriber.
// If a future field is added to WorkspaceSessionSnapshot but not to the
// gate, the subscriber would silently stop noticing changes to that field
// and persist stale data. Listing every key here as a fixture and asserting
// the gate covers them catches the drift at test time. The compile-time
// _exhaustive check in workspace-session.ts is the primary line of defense;
// this test is the runtime backstop.
const fixture: Record<keyof WorkspaceSessionSnapshot, true> = {
activeRepoId: true,
activeWorktreeId: true,
activeTabId: true,
tabsByWorktree: true,
terminalLayoutsByTabId: true,
activeTabIdByWorktree: true,
openFiles: true,
activeFileIdByWorktree: true,
activeTabTypeByWorktree: true,
browserTabsByWorktree: true,
browserPagesByWorkspace: true,
activeBrowserTabIdByWorktree: true,
browserUrlHistory: true,
unifiedTabsByWorktree: true,
groupsByWorktree: true,
layoutByWorktree: true,
activeGroupIdByWorktree: true,
sshConnectionStates: true,
repos: true,
worktreesByRepo: true,
lastKnownRelayPtyIdByTabId: true
}
it('contains every key of WorkspaceSessionSnapshot', () => {
const fixtureKeys = Object.keys(fixture)
expect(
fixtureKeys.every((k) => (SESSION_RELEVANT_FIELDS as readonly string[]).includes(k))
).toBe(true)
expect(SESSION_RELEVANT_FIELDS.length).toBe(fixtureKeys.length)
})
it('has no duplicate entries', () => {
expect(new Set(SESSION_RELEVANT_FIELDS).size).toBe(SESSION_RELEVANT_FIELDS.length)
})
})
+38 -1
View File
@@ -8,7 +8,7 @@ import type {
import type { AppState } from '../store'
import type { OpenFile } from '../store/slices/editor'
type WorkspaceSessionSnapshot = Pick<
export type WorkspaceSessionSnapshot = Pick<
AppState,
| 'activeRepoId'
| 'activeWorktreeId'
@@ -33,6 +33,43 @@ type WorkspaceSessionSnapshot = Pick<
| 'lastKnownRelayPtyIdByTabId'
>
// Why: the App-level Zustand subscriber that debounces session writes uses
// this list as a shallow-equality gate so it only resets the timer when a
// field that actually feeds buildWorkspaceSessionPayload changes. Keeping
// the list co-located with WorkspaceSessionSnapshot means a future field
// added to the snapshot type fails the _exhaustive check below at compile
// time, preventing the gate from silently going stale.
export const SESSION_RELEVANT_FIELDS = [
'activeRepoId',
'activeWorktreeId',
'activeTabId',
'tabsByWorktree',
'terminalLayoutsByTabId',
'activeTabIdByWorktree',
'openFiles',
'activeFileIdByWorktree',
'activeTabTypeByWorktree',
'browserTabsByWorktree',
'browserPagesByWorkspace',
'activeBrowserTabIdByWorktree',
'browserUrlHistory',
'unifiedTabsByWorktree',
'groupsByWorktree',
'layoutByWorktree',
'activeGroupIdByWorktree',
'sshConnectionStates',
'repos',
'worktreesByRepo',
'lastKnownRelayPtyIdByTabId'
] as const satisfies readonly (keyof WorkspaceSessionSnapshot)[]
type _MissingSessionField = Exclude<
keyof WorkspaceSessionSnapshot,
(typeof SESSION_RELEVANT_FIELDS)[number]
>
const _exhaustive: [_MissingSessionField] extends [never] ? true : never = true
void _exhaustive
/** Build the editor-file portion of the workspace session for persistence.
* Only edit-mode files are saved — diffs and conflict views are transient. */
export function buildEditorSessionData(