Measure renderer terminal backlog pressure (#4809)

This commit is contained in:
Neil 2026-06-07 10:36:26 -07:00 committed by GitHub
parent 7ac509ed62
commit f6a576d09c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 142 additions and 6 deletions

View File

@ -82,6 +82,12 @@ function printMarkdownTable(rows) {
['Foreground Enqueues', 'deferredForegroundEnqueue'],
['Foreground Writes', 'deferredForegroundWrite'],
['Drains', 'scheduledDrains'],
['Renderer Queued Terms', 'rendererQueuedTerminals'],
['Renderer Queued Chars', 'rendererQueuedChars'],
['Renderer Peak Terms', 'rendererPeakQueuedTerminals'],
['Renderer Peak Chars', 'rendererPeakQueuedChars'],
['Renderer Peak By Term', 'rendererPeakQueuedCharsByTerminal'],
['Renderer Drops', 'rendererDroppedBacklogs'],
['Main Pending PTYs', 'mainPendingPtys'],
['Main Pending Chars', 'mainPendingChars'],
['Main Max Pending', 'mainMaxPendingChars'],

View File

@ -1,5 +1,9 @@
/* eslint-disable max-lines -- Why: the scheduler tests cover one queue state machine; keeping ordering and overflow cases together makes regressions easier to audit. */
import { afterEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('@/lib/e2e-config', () => ({
e2eConfig: { exposeStore: true }
}))
function createTerminal() {
const classes = new Set<string>()
@ -45,8 +49,15 @@ async function loadScheduler() {
}
describe('pane terminal output scheduler', () => {
beforeEach(() => {
vi.stubGlobal('window', globalThis)
})
afterEach(() => {
vi.useRealTimers()
delete (globalThis as { __terminalOutputSchedulerDebug?: unknown })
.__terminalOutputSchedulerDebug
vi.unstubAllGlobals()
})
it('writes foreground output immediately', async () => {
@ -663,6 +674,50 @@ describe('pane terminal output scheduler', () => {
expect(terminals[0].write).toHaveBeenCalledTimes(2)
})
it('reports current and peak queued renderer backlog in debug snapshots', async () => {
vi.useFakeTimers()
const { writeTerminalOutput } = await loadScheduler()
const terminalA = createTerminal()
const terminalB = createTerminal()
const debug = (
window as unknown as {
__terminalOutputSchedulerDebug?: {
snapshot: () => {
queuedTerminalCount: number
queuedChars: number
peakQueuedTerminalCount: number
peakQueuedChars: number
peakQueuedCharsByTerminal: number
droppedBacklogCount: number
}
}
}
).__terminalOutputSchedulerDebug
writeTerminalOutput(terminalA, 'a'.repeat(10), { foreground: false })
writeTerminalOutput(terminalB, 'b'.repeat(20), { foreground: false })
expect(debug?.snapshot()).toMatchObject({
queuedTerminalCount: 2,
queuedChars: 30,
peakQueuedTerminalCount: 2,
peakQueuedChars: 30,
peakQueuedCharsByTerminal: 20,
droppedBacklogCount: 0
})
vi.advanceTimersByTime(50)
expect(debug?.snapshot()).toMatchObject({
queuedTerminalCount: 0,
queuedChars: 0,
peakQueuedTerminalCount: 2,
peakQueuedChars: 30,
peakQueuedCharsByTerminal: 20,
droppedBacklogCount: 0
})
})
it('keeps draining background chunks without per-write parse callback backpressure', async () => {
vi.useFakeTimers()
const { writeTerminalOutput } = await loadScheduler()

View File

@ -105,6 +105,12 @@ type TerminalOutputSchedulerDebugSnapshot = {
deferredForegroundWriteCount: number
flushWriteCount: number
scheduledDrainCount: number
queuedTerminalCount: number
queuedChars: number
peakQueuedTerminalCount: number
peakQueuedChars: number
peakQueuedCharsByTerminal: number
droppedBacklogCount: number
drainWrites: number[]
}
@ -121,6 +127,12 @@ const debugState: TerminalOutputSchedulerDebugSnapshot = {
deferredForegroundWriteCount: 0,
flushWriteCount: 0,
scheduledDrainCount: 0,
queuedTerminalCount: 0,
queuedChars: 0,
peakQueuedTerminalCount: 0,
peakQueuedChars: 0,
peakQueuedCharsByTerminal: 0,
droppedBacklogCount: 0,
drainWrites: []
}
@ -132,9 +144,51 @@ function resetDebugState(): void {
debugState.deferredForegroundWriteCount = 0
debugState.flushWriteCount = 0
debugState.scheduledDrainCount = 0
debugState.queuedTerminalCount = 0
debugState.queuedChars = 0
debugState.peakQueuedTerminalCount = 0
debugState.peakQueuedChars = 0
debugState.peakQueuedCharsByTerminal = 0
debugState.droppedBacklogCount = 0
debugState.drainWrites = []
}
function readQueueDebugSnapshot(): {
queuedTerminalCount: number
queuedChars: number
queuedCharsByTerminal: number
} {
let queuedChars = 0
let queuedCharsByTerminal = 0
for (const entry of queuedByTerminal.values()) {
queuedChars += entry.queuedChars
queuedCharsByTerminal = Math.max(queuedCharsByTerminal, entry.queuedChars)
}
return {
queuedTerminalCount: queuedByTerminal.size,
queuedChars,
queuedCharsByTerminal
}
}
function recordQueueDebugPressure(): void {
if (!debugEnabled) {
return
}
const current = readQueueDebugSnapshot()
debugState.queuedTerminalCount = current.queuedTerminalCount
debugState.queuedChars = current.queuedChars
debugState.peakQueuedTerminalCount = Math.max(
debugState.peakQueuedTerminalCount,
current.queuedTerminalCount
)
debugState.peakQueuedChars = Math.max(debugState.peakQueuedChars, current.queuedChars)
debugState.peakQueuedCharsByTerminal = Math.max(
debugState.peakQueuedCharsByTerminal,
current.queuedCharsByTerminal
)
}
function exposeDebugApi(): void {
if (!debugEnabled || typeof window === 'undefined') {
return
@ -146,10 +200,13 @@ function exposeDebugApi(): void {
}
target.__terminalOutputSchedulerDebug ??= {
reset: resetDebugState,
snapshot: () => ({
...debugState,
drainWrites: [...debugState.drainWrites]
})
snapshot: () => {
recordQueueDebugPressure()
return {
...debugState,
drainWrites: [...debugState.drainWrites]
}
}
}
}
@ -368,6 +425,7 @@ function takeQueuedChunk(entry: QueueEntry, limit: number): QueuedWrite | null {
if (entry.queuedChars < 0) {
entry.queuedChars = 0
}
recordQueueDebugPressure()
return data
? {
data,
@ -412,6 +470,7 @@ function enqueueChunk(
stripTransientCursorShows: options?.stripTransientCursorShows === true
})
entry.queuedChars += data.length
recordQueueDebugPressure()
}
function replaceBacklogWithWarning(entry: QueueEntry): void {
@ -431,7 +490,11 @@ function replaceBacklogWithWarning(entry: QueueEntry): void {
entry.backgroundBacklogDropped = true
entry.highPriority = true
entry.foregroundHold = false
if (debugEnabled && shouldNotify) {
debugState.droppedBacklogCount++
}
clearForegroundCoalesce(entry)
recordQueueDebugPressure()
if (shouldNotify) {
entry.onBackgroundBacklogDropped?.()
}
@ -510,6 +573,7 @@ function writeQueuedChunk(entry: QueueEntry): 'foreground' | 'background' | null
entry.queuedChars = 0
clearForegroundHoldSafety(entry)
clearForegroundCoalesce(entry)
recordQueueDebugPressure()
return null
}
return queuedWrite.foreground ? 'foreground' : 'background'
@ -552,6 +616,7 @@ function drainQueuedOutput(): void {
if (debugEnabled && writes > 0) {
debugState.drainWrites.push(writes)
}
recordQueueDebugPressure()
if (queuedByTerminal.size > 0 && hasDrainableBacklog()) {
scheduleDrain(
hasHighPriorityBacklog() ? HIGH_PRIORITY_DRAIN_INTERVAL_MS : BACKGROUND_DRAIN_INTERVAL_MS
@ -760,6 +825,7 @@ export function flushTerminalOutput(
entry.highPriority = false
clearForegroundHoldSafety(entry)
clearForegroundCoalesce(entry)
recordQueueDebugPressure()
return
}
@ -792,6 +858,7 @@ export function flushTerminalOutput(
// the scheduler for other panes still draining.
clearForegroundHoldSafety(entry)
clearForegroundCoalesce(entry)
recordQueueDebugPressure()
return
}
if (options?.maxChars !== undefined && flushedChars >= options.maxChars) {
@ -808,6 +875,7 @@ export function flushTerminalOutput(
clearForegroundCoalesce(entry)
clearForegroundHoldSafety(entry)
}
recordQueueDebugPressure()
}
function requestRegisteredTerminalBacklogRecovery(terminal: TerminalOutputTarget): boolean {
@ -865,6 +933,7 @@ export function discardTerminalOutput(terminal: TerminalOutputTarget): void {
queuedByTerminal.delete(terminal)
activeOutputTargets.delete(terminal)
discardForegroundRenderSettle(terminal)
recordQueueDebugPressure()
}
exposeDebugApi()

View File

@ -69,6 +69,12 @@ type TerminalOutputSchedulerDebugSnapshot = {
deferredForegroundWriteCount: number
flushWriteCount: number
scheduledDrainCount: number
queuedTerminalCount: number
queuedChars: number
peakQueuedTerminalCount: number
peakQueuedChars: number
peakQueuedCharsByTerminal: number
droppedBacklogCount: number
drainWrites: number[]
}
@ -468,7 +474,7 @@ function annotateTypingMeasurement(
? ` hiddenSkips=${debug.hiddenRendererSkipCount} hiddenSkippedChars=${debug.hiddenRendererSkippedChars} mode2031Replies=${debug.hiddenRendererMode2031ReplyCount}`
: ''
const schedulerSummary = scheduler
? ` deferredForegroundEnqueue=${scheduler.deferredForegroundEnqueueCount} deferredForegroundWrite=${scheduler.deferredForegroundWriteCount} scheduledDrains=${scheduler.scheduledDrainCount}`
? ` deferredForegroundEnqueue=${scheduler.deferredForegroundEnqueueCount} deferredForegroundWrite=${scheduler.deferredForegroundWriteCount} scheduledDrains=${scheduler.scheduledDrainCount} rendererQueuedTerminals=${scheduler.queuedTerminalCount} rendererQueuedChars=${scheduler.queuedChars} rendererPeakQueuedTerminals=${scheduler.peakQueuedTerminalCount} rendererPeakQueuedChars=${scheduler.peakQueuedChars} rendererPeakQueuedCharsByTerminal=${scheduler.peakQueuedCharsByTerminal} rendererDroppedBacklogs=${scheduler.droppedBacklogCount}`
: ''
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} mainPeakPendingChars=${mainPressure.peakPendingChars} mainPeakMaxPendingChars=${mainPressure.peakMaxPendingCharsByPty} mainPeakInFlightChars=${mainPressure.peakRendererInFlightChars} mainPeakMaxInFlightChars=${mainPressure.peakMaxRendererInFlightCharsByPty} mainAckGatedFlushSkips=${mainPressure.ackGatedFlushSkipCount}`