Track peak PTY renderer delivery pressure (#4800)

This commit is contained in:
Neil 2026-06-07 09:14:30 -07:00 committed by GitHub
parent 228da20e93
commit 16fde16e90
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 130 additions and 8 deletions

View File

@ -87,7 +87,12 @@ function printMarkdownTable(rows) {
['Main In-Flight Chars', 'mainInFlightChars'],
['Main Max In-Flight', 'mainMaxInFlightChars'],
['Main Active PTYs', 'mainActivePtys'],
['Main Flush Scheduled', 'mainFlushScheduled']
['Main Flush Scheduled', 'mainFlushScheduled'],
['Main Peak Pending', 'mainPeakPendingChars'],
['Main Peak Max Pending', 'mainPeakMaxPendingChars'],
['Main Peak In-Flight', 'mainPeakInFlightChars'],
['Main Peak Max In-Flight', 'mainPeakMaxInFlightChars'],
['Main ACK-Gated Skips', 'mainAckGatedFlushSkips']
]
console.log(`| ${columns.map(([label]) => label).join(' | ')} |`)

View File

@ -152,6 +152,7 @@ import {
clearProviderPtyState,
deletePtyOwnership,
getPtyRendererDeliveryDebugSnapshot,
resetPtyRendererDeliveryDebug,
getPtyIdForPaneKey,
hasPendingRendererSerializerForPaneKey,
setPtyOwnership,
@ -4231,7 +4232,12 @@ describe('registerPtyHandlers', () => {
rendererInFlightPtyCount: 1,
rendererInFlightChars: 512 * 1024,
maxRendererInFlightCharsByPty: 512 * 1024,
flushScheduled: false
flushScheduled: false,
peakPendingChars: 600 * 1024,
peakMaxPendingCharsByPty: 600 * 1024,
peakRendererInFlightChars: 512 * 1024,
peakMaxRendererInFlightCharsByPty: 512 * 1024,
ackGatedFlushSkipCount: 1
})
secondProc.emitData('second-terminal-output')
@ -4254,7 +4260,20 @@ describe('registerPtyHandlers', () => {
expect(getPtyRendererDeliveryDebugSnapshot()).toMatchObject({
pendingPtyCount: 1,
pendingChars: 72 * 1024,
rendererInFlightChars: 512 * 1024 + 'second-terminal-output'.length
rendererInFlightChars: 512 * 1024 + 'second-terminal-output'.length,
peakPendingChars: 600 * 1024,
peakRendererInFlightChars: 512 * 1024 + 'second-terminal-output'.length
})
resetPtyRendererDeliveryDebug()
expect(getPtyRendererDeliveryDebugSnapshot()).toMatchObject({
pendingPtyCount: 1,
pendingChars: 72 * 1024,
rendererInFlightChars: 512 * 1024 + 'second-terminal-output'.length,
peakPendingChars: 72 * 1024,
peakRendererInFlightChars: 512 * 1024 + 'second-terminal-output'.length,
ackGatedFlushSkipCount: 0
})
} finally {
vi.useRealTimers()

View File

@ -902,6 +902,11 @@ export type PtyRendererDeliveryDebugSnapshot = {
maxRendererInFlightCharsByPty: number
activeRendererPtyCount: number
flushScheduled: boolean
peakPendingChars: number
peakMaxPendingCharsByPty: number
peakRendererInFlightChars: number
peakMaxRendererInFlightCharsByPty: number
ackGatedFlushSkipCount: number
}
const EMPTY_PTY_RENDERER_DELIVERY_DEBUG_SNAPSHOT: PtyRendererDeliveryDebugSnapshot = {
@ -912,17 +917,27 @@ const EMPTY_PTY_RENDERER_DELIVERY_DEBUG_SNAPSHOT: PtyRendererDeliveryDebugSnapsh
rendererInFlightChars: 0,
maxRendererInFlightCharsByPty: 0,
activeRendererPtyCount: 0,
flushScheduled: false
flushScheduled: false,
peakPendingChars: 0,
peakMaxPendingCharsByPty: 0,
peakRendererInFlightChars: 0,
peakMaxRendererInFlightCharsByPty: 0,
ackGatedFlushSkipCount: 0
}
let readPtyRendererDeliveryDebugSnapshot = (): PtyRendererDeliveryDebugSnapshot => ({
...EMPTY_PTY_RENDERER_DELIVERY_DEBUG_SNAPSHOT
})
let resetPtyRendererDeliveryDebugSnapshot = (): void => {}
export function getPtyRendererDeliveryDebugSnapshot(): PtyRendererDeliveryDebugSnapshot {
return readPtyRendererDeliveryDebugSnapshot()
}
export function resetPtyRendererDeliveryDebug(): void {
resetPtyRendererDeliveryDebugSnapshot()
}
function clearDidFinishLoadHandler(): void {
if (didFinishLoadHandler && didFinishLoadWebContents) {
didFinishLoadWebContents.removeListener('did-finish-load', didFinishLoadHandler)
@ -966,6 +981,7 @@ export function registerPtyHandlers(
ipcMain.removeHandler('pty:clearPendingPaneSerializer')
ipcMain.removeHandler('pty:getMainBufferSnapshot')
ipcMain.removeHandler('pty:getRendererDeliveryDebugSnapshot')
ipcMain.removeHandler('pty:resetRendererDeliveryDebug')
ipcMain.removeHandler('pty:writeAccepted')
ipcMain.removeAllListeners('pty:write')
ipcMain.removeAllListeners('pty:ackColdRestore')
@ -1063,6 +1079,11 @@ export function registerPtyHandlers(
const INTERACTIVE_OUTPUT_MAX_CHARS = 1024
const INTERACTIVE_REDRAW_MAX_CHARS = PTY_BATCH_FLUSH_CHUNK_CHARS
const INTERACTIVE_OUTPUT_BUDGET_CHARS = 32 * 1024
let peakPendingChars = 0
let peakMaxPendingCharsByPty = 0
let peakRendererInFlightChars = 0
let peakMaxRendererInFlightCharsByPty = 0
let ackGatedFlushSkipCount = 0
function getMaxMapValue(values: Iterable<number>): number {
let max = 0
@ -1072,7 +1093,7 @@ export function registerPtyHandlers(
return max
}
readPtyRendererDeliveryDebugSnapshot = () => {
function readCurrentPtyRendererDeliveryDebugSnapshot(): PtyRendererDeliveryDebugSnapshot {
let pendingChars = 0
let maxPendingCharsByPty = 0
for (const pending of pendingData.values()) {
@ -1088,10 +1109,36 @@ export function registerPtyHandlers(
rendererInFlightChars: rendererInFlightTotalChars,
maxRendererInFlightCharsByPty: getMaxMapValue(rendererInFlightCharsByPty.values()),
activeRendererPtyCount: activeRendererPtys.size,
flushScheduled: flushTimer !== null
flushScheduled: flushTimer !== null,
peakPendingChars,
peakMaxPendingCharsByPty,
peakRendererInFlightChars,
peakMaxRendererInFlightCharsByPty,
ackGatedFlushSkipCount
}
}
function recordPtyRendererDeliveryPressure(): void {
const current = readCurrentPtyRendererDeliveryDebugSnapshot()
peakPendingChars = Math.max(peakPendingChars, current.pendingChars)
peakMaxPendingCharsByPty = Math.max(peakMaxPendingCharsByPty, current.maxPendingCharsByPty)
peakRendererInFlightChars = Math.max(peakRendererInFlightChars, current.rendererInFlightChars)
peakMaxRendererInFlightCharsByPty = Math.max(
peakMaxRendererInFlightCharsByPty,
current.maxRendererInFlightCharsByPty
)
}
readPtyRendererDeliveryDebugSnapshot = readCurrentPtyRendererDeliveryDebugSnapshot
resetPtyRendererDeliveryDebugSnapshot = () => {
peakPendingChars = 0
peakMaxPendingCharsByPty = 0
peakRendererInFlightChars = 0
peakMaxRendererInFlightCharsByPty = 0
ackGatedFlushSkipCount = 0
recordPtyRendererDeliveryPressure()
}
function isLikelyInteractiveRedraw(data: string): boolean {
if (data.length <= INTERACTIVE_OUTPUT_MAX_CHARS) {
return true
@ -1159,6 +1206,7 @@ export function registerPtyHandlers(
const charCount = getPtyPayloadCharCount(payload)
rendererInFlightCharsByPty.set(id, (rendererInFlightCharsByPty.get(id) ?? 0) + charCount)
rendererInFlightTotalChars += charCount
recordPtyRendererDeliveryPressure()
mainWindow.webContents.send('pty:data', payload)
}
@ -1206,6 +1254,7 @@ export function registerPtyHandlers(
pendingData.clear()
rendererInFlightCharsByPty.clear()
rendererInFlightTotalChars = 0
recordPtyRendererDeliveryPressure()
return
}
let writes = 0
@ -1230,6 +1279,10 @@ export function registerPtyHandlers(
sendPtyDataToRenderer(id, makePtyDataPayload(id, chunk, pending.startSeq))
writes++
}
if (pendingData.size > 0 && writes === 0) {
ackGatedFlushSkipCount++
}
recordPtyRendererDeliveryPressure()
if (pendingData.size > 0 && writes > 0) {
// Why: a background terminal can dump megabytes at once. Yield between
// small IPC slices so keystroke writes are not stuck behind one flush.
@ -1277,6 +1330,7 @@ export function registerPtyHandlers(
pendingData.clear()
rendererInFlightCharsByPty.clear()
rendererInFlightTotalChars = 0
recordPtyRendererDeliveryPressure()
return
}
const existing = pendingData.get(payload.id)
@ -1293,6 +1347,7 @@ export function registerPtyHandlers(
// bounded, and the per-PTY cap still prevents an active TUI runaway.
if (!canSendPtyDataToRenderer(payload.id, { interactive: true })) {
pendingData.set(payload.id, pending)
recordPtyRendererDeliveryPressure()
return
}
pendingData.delete(payload.id)
@ -1309,6 +1364,7 @@ export function registerPtyHandlers(
return
}
pendingData.set(payload.id, pending)
recordPtyRendererDeliveryPressure()
if (!flushTimer) {
schedulePendingDataFlush(PTY_BATCH_INTERVAL_MS)
}
@ -1339,6 +1395,7 @@ export function registerPtyHandlers(
rendererInFlightTotalChars - (rendererInFlightCharsByPty.get(payload.id) ?? 0)
)
rendererInFlightCharsByPty.delete(payload.id)
recordPtyRendererDeliveryPressure()
mainWindow.webContents.send('pty:exit', payload)
}
})
@ -1861,6 +1918,9 @@ export function registerPtyHandlers(
ipcMain.handle('pty:getRendererDeliveryDebugSnapshot', (): PtyRendererDeliveryDebugSnapshot => {
return getPtyRendererDeliveryDebugSnapshot()
})
ipcMain.handle('pty:resetRendererDeliveryDebug', (): void => {
resetPtyRendererDeliveryDebug()
})
ipcMain.handle(
'pty:spawn',
@ -2540,6 +2600,7 @@ export function registerPtyHandlers(
rendererInFlightCharsByPty.set(args.id, next)
}
tryGetProviderForPty(args.id)?.acknowledgeDataEvent(args.id, acknowledged)
recordPtyRendererDeliveryPressure()
if (pendingData.size > 0 && !flushTimer) {
schedulePendingDataFlush(0)
}

View File

@ -927,7 +927,13 @@ export type PreloadApi = {
maxRendererInFlightCharsByPty: number
activeRendererPtyCount: number
flushScheduled: boolean
peakPendingChars: number
peakMaxPendingCharsByPty: number
peakRendererInFlightChars: number
peakMaxRendererInFlightCharsByPty: number
ackGatedFlushSkipCount: number
}>
resetRendererDeliveryDebug: () => Promise<void>
onData: (
callback: (data: { id: string; data: string; seq?: number; rawLength?: number }) => void
) => () => void

View File

@ -717,8 +717,16 @@ const api = {
maxRendererInFlightCharsByPty: number
activeRendererPtyCount: number
flushScheduled: boolean
peakPendingChars: number
peakMaxPendingCharsByPty: number
peakRendererInFlightChars: number
peakMaxRendererInFlightCharsByPty: number
ackGatedFlushSkipCount: number
}> => ipcRenderer.invoke('pty:getRendererDeliveryDebugSnapshot'),
resetRendererDeliveryDebug: (): Promise<void> =>
ipcRenderer.invoke('pty:resetRendererDeliveryDebug'),
/** Check if a PTY's shell has child processes (e.g. a running command).
* Returns false for an idle shell prompt. */
hasChildProcesses: (id: string): Promise<boolean> =>

View File

@ -2160,6 +2160,23 @@ function createPtyApi(): NonNullable<Partial<PreloadApi>['pty']> {
getCwd: () => Promise.resolve('~'),
listSessions: () => Promise.resolve([]),
getMainBufferSnapshot: () => Promise.resolve(null),
getRendererDeliveryDebugSnapshot: () =>
Promise.resolve({
pendingPtyCount: 0,
pendingChars: 0,
maxPendingCharsByPty: 0,
rendererInFlightPtyCount: 0,
rendererInFlightChars: 0,
maxRendererInFlightCharsByPty: 0,
activeRendererPtyCount: 0,
flushScheduled: false,
peakPendingChars: 0,
peakMaxPendingCharsByPty: 0,
peakRendererInFlightChars: 0,
peakMaxRendererInFlightCharsByPty: 0,
ackGatedFlushSkipCount: 0
}),
resetRendererDeliveryDebug: () => Promise.resolve(),
onData: () => noopUnsubscribe,
onReplay: () => noopUnsubscribe,
onExit: () => noopUnsubscribe,

View File

@ -74,6 +74,11 @@ type MainPtyPressureDebugSnapshot = {
maxRendererInFlightCharsByPty: number
activeRendererPtyCount: number
flushScheduled: boolean
peakPendingChars: number
peakMaxPendingCharsByPty: number
peakRendererInFlightChars: number
peakMaxRendererInFlightCharsByPty: number
ackGatedFlushSkipCount: number
}
const KEY_LATENCY_SAMPLES = 'abcdefghijklmnop'
@ -343,9 +348,10 @@ async function measureTypingDuringLoad(
}
async function resetTerminalPtyOutputDebug(page: Page): Promise<void> {
await page.evaluate(() => {
await page.evaluate(async () => {
;(window as SyntheticOpenCodeWindow).__terminalPtyOutputDebug?.reset()
;(window as SyntheticOpenCodeWindow).__terminalOutputSchedulerDebug?.reset()
await window.api.pty.resetRendererDeliveryDebug()
})
}
@ -387,7 +393,7 @@ function annotateTypingMeasurement(
? ` deferredForegroundEnqueue=${scheduler.deferredForegroundEnqueueCount} deferredForegroundWrite=${scheduler.deferredForegroundWriteCount} scheduledDrains=${scheduler.scheduledDrainCount}`
: ''
const mainPressureSummary = mainPressure
? ` mainPendingPtys=${mainPressure.pendingPtyCount} mainPendingChars=${mainPressure.pendingChars} mainMaxPendingChars=${mainPressure.maxPendingCharsByPty} mainInFlightPtys=${mainPressure.rendererInFlightPtyCount} mainInFlightChars=${mainPressure.rendererInFlightChars} mainMaxInFlightChars=${mainPressure.maxRendererInFlightCharsByPty} mainActivePtys=${mainPressure.activeRendererPtyCount} mainFlushScheduled=${mainPressure.flushScheduled}`
? ` mainPendingPtys=${mainPressure.pendingPtyCount} mainPendingChars=${mainPressure.pendingChars} mainMaxPendingChars=${mainPressure.maxPendingCharsByPty} mainInFlightPtys=${mainPressure.rendererInFlightPtyCount} mainInFlightChars=${mainPressure.rendererInFlightChars} mainMaxInFlightChars=${mainPressure.maxRendererInFlightCharsByPty} mainActivePtys=${mainPressure.activeRendererPtyCount} mainFlushScheduled=${mainPressure.flushScheduled} mainPeakPendingChars=${mainPressure.peakPendingChars} mainPeakMaxPendingChars=${mainPressure.peakMaxPendingCharsByPty} mainPeakInFlightChars=${mainPressure.peakRendererInFlightChars} mainPeakMaxInFlightChars=${mainPressure.peakMaxRendererInFlightCharsByPty} mainAckGatedFlushSkips=${mainPressure.ackGatedFlushSkipCount}`
: ''
testInfo.annotations.push({
type,