Coalesce renderer perf polling (#2079)

This commit is contained in:
Neil 2026-05-16 10:55:40 -07:00 committed by GitHub
parent 42e3bc3c42
commit 9409c43bec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 235 additions and 16 deletions

View File

@ -0,0 +1,106 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
registerRuntimeTerminalTab,
scheduleRuntimeGraphSync,
setRuntimeGraphStoreStateGetter,
setRuntimeGraphSyncEnabled
} from './sync-runtime-graph'
import type { AppState } from '../store/types'
import type { TerminalTab } from '../../../shared/types'
function makeState(overrides: Partial<AppState> = {}): AppState {
return {
tabsByWorktree: {},
terminalLayoutsByTabId: {} as AppState['terminalLayoutsByTabId'],
runtimePaneTitlesByTabId: {} as AppState['runtimePaneTitlesByTabId'],
groupsByWorktree: {},
activeGroupIdByWorktree: {},
unifiedTabsByWorktree: {},
tabBarOrderByWorktree: {},
activeFileId: null,
activeFileIdByWorktree: {},
openFiles: [],
editorDrafts: {},
activeTabId: null,
...overrides
} as AppState
}
function makeTerminalTab(): TerminalTab {
return {
id: 'term-1',
ptyId: null,
worktreeId: 'wt-1',
title: 'Terminal',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1
}
}
function deferred<T>(): {
promise: Promise<T>
resolve: (value: T | PromiseLike<T>) => void
} {
let resolve: (value: T | PromiseLike<T>) => void = () => {}
const promise = new Promise<T>((r) => {
resolve = r
})
return { promise, resolve }
}
async function flushMicrotasks(): Promise<void> {
await Promise.resolve()
await Promise.resolve()
}
afterEach(() => {
setRuntimeGraphSyncEnabled(false)
setRuntimeGraphStoreStateGetter(null)
vi.unstubAllGlobals()
})
describe('scheduleRuntimeGraphSync', () => {
it('coalesces updates that arrive while the runtime graph IPC is in flight', async () => {
const syncCalls: {
promise: Promise<void>
resolve: (value: void | PromiseLike<void>) => void
}[] = []
const syncWindowGraph = vi.fn(() => {
const call = deferred<void>()
syncCalls.push(call)
return call.promise
})
vi.stubGlobal('window', { api: { runtime: { syncWindowGraph } } })
vi.stubGlobal('HTMLElement', class HTMLElement {})
const unregister = registerRuntimeTerminalTab({
tabId: 'term-1',
worktreeId: 'wt-1',
getManager: () => null,
getContainer: () => null,
getPtyIdForPane: () => null
})
setRuntimeGraphStoreStateGetter(() =>
makeState({
tabsByWorktree: { 'wt-1': [makeTerminalTab()] } as AppState['tabsByWorktree']
})
)
setRuntimeGraphSyncEnabled(true)
await flushMicrotasks()
expect(syncWindowGraph).toHaveBeenCalledTimes(1)
scheduleRuntimeGraphSync()
scheduleRuntimeGraphSync()
await flushMicrotasks()
expect(syncWindowGraph).toHaveBeenCalledTimes(1)
syncCalls[0]?.resolve()
await flushMicrotasks()
expect(syncWindowGraph).toHaveBeenCalledTimes(2)
syncCalls[1]?.resolve()
unregister()
})
})

View File

@ -9,6 +9,8 @@ import type { AppState } from '../store/types'
function makeState(overrides: Partial<AppState> = {}): AppState {
return {
tabsByWorktree: {},
terminalLayoutsByTabId: {} as AppState['terminalLayoutsByTabId'],
runtimePaneTitlesByTabId: {} as AppState['runtimePaneTitlesByTabId'],
groupsByWorktree: {},
activeGroupIdByWorktree: {},
unifiedTabsByWorktree: {},

View File

@ -51,6 +51,8 @@ const registeredTabs = new Map<string, RegisteredTerminalTab>()
const tabRegisteredAt = new Map<string, number>()
const NO_TRANSPORT_GRACE_MS = 10_000
let syncScheduled = false
let syncInFlight = false
let syncPendingAfterFlight = false
let syncEnabled = false
let getStoreState: (() => AppState) | null = null
let mobileSessionSnapshotVersion = 0
@ -106,13 +108,37 @@ export function scheduleRuntimeGraphSync(): void {
if (!syncEnabled || syncScheduled) {
return
}
if (syncInFlight) {
syncPendingAfterFlight = true
return
}
syncScheduled = true
queueMicrotask(() => {
syncScheduled = false
void syncRuntimeGraph()
void runRuntimeGraphSync()
})
}
async function runRuntimeGraphSync(): Promise<void> {
if (syncInFlight) {
syncPendingAfterFlight = true
return
}
syncInFlight = true
try {
await syncRuntimeGraph()
} finally {
syncInFlight = false
if (syncPendingAfterFlight) {
syncPendingAfterFlight = false
// Why: syncWindowGraph crosses IPC and can be slower than title/layout
// churn. Collapse all updates that arrived during one in-flight sync
// into a single trailing graph instead of stacking concurrent IPC calls.
scheduleRuntimeGraphSync()
}
}
}
export type RuntimeMobileSessionSyncKey = {
// Why: large maps the renderer never reshapes are compared by reference.
// Reallocating `terminalLayoutsByTabId` / `runtimePaneTitlesByTabId` is the

View File

@ -0,0 +1,69 @@
import { create } from 'zustand'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { createMemorySlice } from './memory'
import type { AppState } from '../types'
import type { MemorySnapshot } from '../../../../shared/types'
function makeMemorySnapshot(overrides: Partial<MemorySnapshot> = {}): MemorySnapshot {
return {
app: {
cpu: 1,
memory: 1024,
main: { cpu: 1, memory: 512 },
renderer: { cpu: 0, memory: 256 },
other: { cpu: 0, memory: 256 },
history: [1024]
},
worktrees: [],
host: {
totalMemory: 8192,
freeMemory: 4096,
usedMemory: 4096,
memoryUsagePercent: 50,
cpuCoreCount: 8,
loadAverage1m: 1
},
totalCpu: 1,
totalMemory: 1024,
collectedAt: 1,
...overrides
}
}
function makeStore() {
return create<Pick<AppState, 'memorySnapshot' | 'memorySnapshotError' | 'fetchMemorySnapshot'>>()(
(...args) => createMemorySlice(...(args as Parameters<typeof createMemorySlice>))
)
}
afterEach(() => {
vi.unstubAllGlobals()
})
describe('createMemorySlice', () => {
it('dedupes concurrent memory snapshot IPC calls', async () => {
let resolveSnapshot: (snapshot: MemorySnapshot) => void = () => {}
const getSnapshot = vi.fn(
() =>
new Promise<MemorySnapshot>((resolve) => {
resolveSnapshot = resolve
})
)
vi.stubGlobal('window', { api: { memory: { getSnapshot } } })
const store = makeStore()
const first = store.getState().fetchMemorySnapshot()
const second = store.getState().fetchMemorySnapshot()
expect(getSnapshot).toHaveBeenCalledTimes(1)
resolveSnapshot(makeMemorySnapshot({ collectedAt: 10 }))
await Promise.all([first, second])
expect(store.getState().memorySnapshot?.collectedAt).toBe(10)
getSnapshot.mockResolvedValueOnce(makeMemorySnapshot({ collectedAt: 11 }))
await store.getState().fetchMemorySnapshot()
expect(getSnapshot).toHaveBeenCalledTimes(2)
expect(store.getState().memorySnapshot?.collectedAt).toBe(11)
})
})

View File

@ -8,22 +8,38 @@ export type MemorySlice = {
fetchMemorySnapshot: () => Promise<void>
}
export const createMemorySlice: StateCreator<AppState, [], [], MemorySlice> = (set) => ({
memorySnapshot: null,
memorySnapshotError: null,
export const createMemorySlice: StateCreator<AppState, [], [], MemorySlice> = (set) => {
let inFlightSnapshot: Promise<void> | null = null
fetchMemorySnapshot: async () => {
try {
const snapshot = await window.api.memory.getSnapshot()
set({ memorySnapshot: snapshot, memorySnapshotError: null })
} catch (err) {
// Why: the always-on Resource Manager status-bar segment needs to know when
// the snapshot IPC is failing so it can surface a "daemon not responding"
// banner with a Restart CTA. Prior code only console.error'd.
console.error('Failed to fetch memory snapshot:', err)
set({
memorySnapshotError: err instanceof Error ? err.message : String(err)
return {
memorySnapshot: null,
memorySnapshotError: null,
fetchMemorySnapshot: () => {
if (inFlightSnapshot) {
return inFlightSnapshot
}
const request = (async () => {
try {
const snapshot = await window.api.memory.getSnapshot()
set({ memorySnapshot: snapshot, memorySnapshotError: null })
} catch (err) {
// Why: the always-on Resource Manager status-bar segment needs to know when
// the snapshot IPC is failing so it can surface a "daemon not responding"
// banner with a Restart CTA. Prior code only console.error'd.
console.error('Failed to fetch memory snapshot:', err)
set({
memorySnapshotError: err instanceof Error ? err.message : String(err)
})
}
})()
const trackedRequest = request.finally(() => {
if (inFlightSnapshot === trackedRequest) {
inFlightSnapshot = null
}
})
inFlightSnapshot = trackedRequest
return trackedRequest
}
}
})
}