Coalesce renderer perf polling (#2079)
This commit is contained in:
parent
42e3bc3c42
commit
9409c43bec
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
@ -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: {},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue