From 78afd75e72f00c4c2b3c7dd105b41b3f2f1778ed Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Wed, 27 May 2026 07:30:45 -0700 Subject: [PATCH] stop materializing response bodies in requestfinished listener MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Bun-side accelerant on the gbrowser-OOM investigation. Pre-fix, the per-page requestfinished listener called \`await res.body()\` just to read .length — Playwright fetches the bytes from Chromium across CDP into a Bun Buffer, only for the listener to discard the buffer after a single length read. On a long-lived headed browser with media-heavy pages this is multi-GB/hour of Buffer allocation churn. Bun GCs it, but the cross-process CDP traffic + transient allocation pressure feeds the OOM trajectory. The fix: req.sizes() pulls from the Network.loadingFinished event Chromium already emits. No body materialization. Accurate for chunked transfer, gzip-compressed responses, and streaming media — the cases where a naive Content-Length header read (the original review's proposal) would have missed the size entirely (Codex flag on the eng review, D10 USE_CDP_EVENT_BATCHED). The D10 stretch goal — replacing N per-page listeners with a single context-level CDP listener via Target.setAutoAttach — is deferred and tracked in TODOS. The listener architecture change is significantly more plumbing than the leak fix and not on the critical path for stopping the body materialization. Co-Authored-By: Claude Opus 4.7 (1M context) --- browse/src/browser-manager.ts | 41 ++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/browse/src/browser-manager.ts b/browse/src/browser-manager.ts index a70393f3f..0cd111049 100644 --- a/browse/src/browser-manager.ts +++ b/browse/src/browser-manager.ts @@ -1710,23 +1710,38 @@ export class BrowserManager { } }); - // Capture response sizes via response finished + // Capture response sizes via requestfinished — but DO NOT call + // response.body() here. Pre-fix, this listener materialized every + // response body across CDP just to read .length: multi-GB/hour of + // Buffer churn on long-lived headed Chromium with media-heavy + // pages, the primary Bun-side accelerant on the gbrowser-OOM + // investigation. req.sizes() pulls from the Network.loadingFinished + // event Chromium already emits — accurate for chunked transfer, + // gzip-compressed responses, and streaming media, all the cases + // where the previous Content-Length-header approach would have + // missed the size. + // + // The "single context-level CDP listener" architecture (D10's + // stretch goal — would reduce per-page listener count from N to 1 + // via Target.setAutoAttach) is deferred. TODOS.md tracks it. page.on('requestfinished', async (req) => { try { - const res = await req.response(); - if (res) { - const url = req.url(); - const body = await res.body().catch(() => null); - const size = body ? body.length : 0; - for (let i = networkBuffer.length - 1; i >= 0; i--) { - const entry = networkBuffer.get(i); - if (entry && entry.url === url && !entry.size) { - networkBuffer.set(i, { ...entry, size }); - break; - } + const sizes = await req.sizes().catch(() => null); + if (!sizes) return; + const url = req.url(); + const size = sizes.responseBodySize ?? 0; + for (let i = networkBuffer.length - 1; i >= 0; i--) { + const entry = networkBuffer.get(i); + if (entry && entry.url === url && !entry.size) { + networkBuffer.set(i, { ...entry, size }); + break; } } - } catch {} + } catch { + // Best-effort: requestfinished fires for aborted/cached requests too, + // where sizes() is unavailable. Missing size is acceptable; an + // unbounded throw would noise the console for every cache hit. + } }); } }