diff --git a/src/main/browser/cdp-client-response-writer.ts b/src/main/browser/cdp-client-response-writer.ts index ff617ae5c0c..43f5bc9d524 100644 --- a/src/main/browser/cdp-client-response-writer.ts +++ b/src/main/browser/cdp-client-response-writer.ts @@ -34,7 +34,17 @@ export class CdpClientResponseWriter { byteLengthOf: (frame) => Buffer.byteLength(frame), getBufferedAmount: () => client.bufferedAmount, isWritable: () => client.readyState === WebSocket.OPEN, - onOverflow: () => client.terminate(), + onOverflow: (evidence) => { + // The client only sees an abrupt socket close, so name the cap here. + console.warn('[cdp] outbound queue overflow; terminating automation client:', { + cap: evidence.cap, + queuedBytes: evidence.queuedBytes, + queuedFrames: evidence.queuedFrames, + maxQueuedBytes: evidence.maxQueuedBytes, + maxQueuedFrames: evidence.maxQueuedFrames + }) + client.terminate() + }, // Preserve large PDF/screenshot replies on a draining connection; queued bursts stay capped. maxFrameBytes: Number.POSITIVE_INFINITY, maxDrainFramesPerTurn: 128 diff --git a/src/shared/ws-outbound-backpressure-queue.ts b/src/shared/ws-outbound-backpressure-queue.ts index 5122d23c663..0c0667e307f 100644 --- a/src/shared/ws-outbound-backpressure-queue.ts +++ b/src/shared/ws-outbound-backpressure-queue.ts @@ -10,6 +10,16 @@ // Generic over the frame type so it serves both the text reply path (encrypted // base64 strings) and the binary send path (Uint8Array frames). +/** Which hard bound tripped, plus the backlog held when it did. */ +export type WsOutboundOverflowEvidence = { + cap: 'maxQueuedBytes' | 'maxQueuedFrames' | 'maxFrameBytes' | 'claimQueuedBytes' | 'sendFailed' + queuedBytes: number + queuedFrames: number + maxQueuedBytes: number + maxQueuedFrames: number + maxFrameBytes: number +} + export type WsOutboundBackpressureQueueOptions = { /** Send a frame on the wire. Called only when under the soft cap. */ send: (frame: TFrame) => void @@ -25,8 +35,9 @@ export type WsOutboundBackpressureQueueOptions = { * Called once when queued bytes exceed maxQueuedBytes — the link is wedged. * The caller should tear the connection down so a fresh subscription can * replay an authoritative snapshot. The queue drops its backlog afterward. + * Receives the backlog measured before that drop, so callers can report it. */ - onOverflow: () => void + onOverflow: (evidence: WsOutboundOverflowEvidence) => void /** Soft cap: stop draining onto the wire while bufferedAmount is above this. */ softCapBytes?: number /** Hard cap on bytes held in this queue before onOverflow fires. */ @@ -135,13 +146,22 @@ export function createWsOutboundBackpressureQueue( stopTimer() } - const failOverflow = (): void => { + const failOverflow = (cap: WsOutboundOverflowEvidence['cap']): void => { if (disposed || overflowed) { return } overflowed = true + // Snapshot before dropBacklog() zeroes the counters. + const evidence: WsOutboundOverflowEvidence = { + cap, + queuedBytes: queued, + queuedFrames, + maxQueuedBytes, + maxQueuedFrames, + maxFrameBytes + } dropBacklog() - options.onOverflow() + options.onOverflow(evidence) } const sendFrame = (frame: TFrame): boolean => { @@ -149,7 +169,7 @@ export function createWsOutboundBackpressureQueue( options.send(frame) return true } catch { - failOverflow() + failOverflow('sendFailed') return false } } @@ -243,7 +263,7 @@ export function createWsOutboundBackpressureQueue( } const bytes = options.byteLengthOf(frame) if (!Number.isFinite(bytes) || bytes < 0 || bytes > maxFrameBytes) { - failOverflow() + failOverflow('maxFrameBytes') return { accepted: false, queued: false, cancel: () => false } } // Fast path: nothing parked and the wire is under the cap — send directly. @@ -261,7 +281,7 @@ export function createWsOutboundBackpressureQueue( } const queuedBytesClaim = options.claimQueuedBytes?.(bytes) if (options.claimQueuedBytes && !queuedBytesClaim) { - failOverflow() + failOverflow('claimQueuedBytes') return { accepted: false, queued: false, cancel: () => false } } const entry: QueueEntry = { @@ -274,7 +294,7 @@ export function createWsOutboundBackpressureQueue( queued += bytes queuedFrames += 1 if (queued > maxQueuedBytes || queuedFrames > maxQueuedFrames) { - failOverflow() + failOverflow(queued > maxQueuedBytes ? 'maxQueuedBytes' : 'maxQueuedFrames') return { accepted: false, queued: false, cancel: () => false } } if (timer === null) {