Track peak PTY renderer delivery pressure (#4800)
This commit is contained in:
parent
228da20e93
commit
16fde16e90
|
|
@ -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(' | ')} |`)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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> =>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue