From c1e9cae18fa41de211b1ff3df03f6c24b7e04e81 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Sat, 8 Aug 2026 09:07:32 -0700 Subject: [PATCH 1/5] test(fetch-leak): keep the RSS leak thresholds clear of measurement noise Two RSS-based checks in fetch-leak.test.ts failed intermittently without anything leaking: - "Request body HiveRef pool returns slot": each cycle keeps 512 Requests with 128 KiB bodies alive at once (64 MiB), and memory freed by gc() is not necessarily returned to the OS by the time rss() is sampled, so baseline and final each carry up to one cycle's live set of slack. The 64 MiB threshold equalled that noise (observed deltas on a non-leaking build: -73..-10 MiB locally, +68/+74 in CI). Use 16 KiB bodies: one cycle is 8 MiB, a real leak is still 256 MiB, the threshold stays 64. - "should not leak using readable stream": the fixture allows RSS to grow 5 MiB between iteration 250 and 500. The regression it guards retained the 128 KiB body per request (~32 MiB over that window); a non-leaking run drifts a few MiB (CI failures were at 5.05-5.1). Allow 16 MiB. No-Verification-Needed: test-only change --- test/js/web/fetch/fetch-leak.test.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/test/js/web/fetch/fetch-leak.test.ts b/test/js/web/fetch/fetch-leak.test.ts index 5c9a9cf290d1..b673fdf02121 100644 --- a/test/js/web/fetch/fetch-leak.test.ts +++ b/test/js/web/fetch/fetch-leak.test.ts @@ -673,7 +673,10 @@ describe("Request body HiveRef pool returns slot via Body.Value.deinit (does not kind, async () => { const script = ` - const payload = Buffer.alloc(128 * 1024, 0x61); // 128 KiB of 'a' + // 16 KiB bodies: cycle() keeps 512 alive at once and gc() may not have returned that + // memory to the OS by the time rss() is sampled, so one cycle's live set (8 MiB here) + // is the noise floor of this measurement and must stay far below the threshold. + const payload = Buffer.alloc(16 * 1024, 0x61); // 16 KiB of 'a' const str = payload.toString("latin1"); const sharedBlob = new Blob([payload]); const rss = process.platform === "darwin" && typeof Bun.unsafe.memoryFootprint === "function" ? Bun.unsafe.memoryFootprint : process.memoryUsage.rss; @@ -705,8 +708,8 @@ describe("Request body HiveRef pool returns slot via Body.Value.deinit (does not const deltaMB = (final - baseline) / 1024 / 1024; console.log(JSON.stringify({ kind: "${kind}", baselineMB: (baseline / 1024 / 1024) | 0, finalMB: (final / 1024 / 1024) | 0, deltaMB: Math.round(deltaMB) })); - // 32 cycles * 512 Requests * 128 KiB = 2 GiB through the pool. If deinit() is - // skipped for any heap-backed variant, RSS climbs by ~2 GiB; with the + // 32 cycles * 512 Requests * 16 KiB = 256 MiB through the pool. If deinit() is + // skipped for any heap-backed variant, RSS climbs by ~256 MiB; with the // Zig semantics it stays flat. 64 MiB is well above GC/allocator noise. // ASAN's quarantine retains freed allocations so widen the threshold there. if (deltaMB > ${isASAN ? 320 : 64}) { @@ -742,9 +745,12 @@ test("should not leak using readable stream", async () => { env: { ...bunEnv, SERVER_URL: server.url.href, - // ASAN's quarantine retains freed allocations so RSS stays elevated - // under bun-asan; the fixture only allows MAX_MEMORY_INCREASE MiB. - MAX_MEMORY_INCREASE: isASAN ? "64" : "5", // in MB + // The regression this guards retained the 128 KiB body per request, i.e. + // ~32 MiB over the 250 sampled iterations; RSS of a non-leaking run still + // drifts by several MiB over that window (allocator high-water, lazily + // freed pages), so the allowance sits between the two. ASAN's quarantine + // retains freed allocations so RSS stays elevated under bun-asan. + MAX_MEMORY_INCREASE: isASAN ? "64" : "16", // in MB // The fixture asserts RSS stabilizes after iteration 250, but with the // default 256 MB quarantine the freed 128 KB bodies are never reused and // RSS keeps climbing through all 500 iterations (~97 MB past the sample From 3a676e282b7b5ded5bb02d615763c0b497460f92 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Sat, 8 Aug 2026 11:50:17 -0700 Subject: [PATCH 2/5] ci: retrigger From 23d51ac534623ecb6ea31991e3c5f2a75fced12d Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 8 Aug 2026 18:59:06 +0000 Subject: [PATCH 3/5] test(fetch-leak): scale the ASAN HiveRef bound with the smaller signal, widen the stream allowance to 20 MiB The 16 KiB bodies shrank the guarded HiveRef leak signature to 256 MiB, which the 320 MiB ASAN bound sat above, so the ASAN lane could no longer fail on the regression it guards. Cap the child's quarantine like the readable-stream test already does and bound at 128 MiB: clean ASAN runs measure deltaMB 5-7, a simulated retained-body leak measures +278. Windows lanes measured up to 14 MiB of non-leak RSS drift over the fixture-6 sample window (scan in #33988), leaving the 16 MiB allowance only 2 MiB of headroom; move it to 20 MiB, still well under the 32 MiB the guarded regression produces. --- test/js/web/fetch/fetch-leak.test.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/test/js/web/fetch/fetch-leak.test.ts b/test/js/web/fetch/fetch-leak.test.ts index b673fdf02121..e6eda1daa9b1 100644 --- a/test/js/web/fetch/fetch-leak.test.ts +++ b/test/js/web/fetch/fetch-leak.test.ts @@ -711,15 +711,22 @@ describe("Request body HiveRef pool returns slot via Body.Value.deinit (does not // 32 cycles * 512 Requests * 16 KiB = 256 MiB through the pool. If deinit() is // skipped for any heap-backed variant, RSS climbs by ~256 MiB; with the // Zig semantics it stays flat. 64 MiB is well above GC/allocator noise. - // ASAN's quarantine retains freed allocations so widen the threshold there. - if (deltaMB > ${isASAN ? 320 : 64}) { + // The ASAN bound must also stay below that 256 MiB signal; the child's + // quarantine is capped (env below) so freed churn recycles, and 128 MiB + // covers what ASAN still retains (observed delta ~6 MiB). + if (deltaMB > ${isASAN ? 128 : 64}) { throw new Error("Request body (${kind}) leaked " + Math.round(deltaMB) + " MB over 32 cycles of 512 Requests"); } `; await using proc = Bun.spawn({ cmd: [bunExe(), "--smol", "-e", script], - env: bunEnv, + env: { + ...bunEnv, + // Cap the quarantine so it cannot retain freed bodies toward the + // default 256 MB, which would cross the 128 MiB leak threshold. + ...(isASAN && { ASAN_OPTIONS: `${bunEnv.ASAN_OPTIONS ?? ""}:quarantine_size_mb=32`.replace(/^:/, "") }), + }, stdout: "pipe", stderr: "pipe", }); @@ -747,10 +754,11 @@ test("should not leak using readable stream", async () => { SERVER_URL: server.url.href, // The regression this guards retained the 128 KiB body per request, i.e. // ~32 MiB over the 250 sampled iterations; RSS of a non-leaking run still - // drifts by several MiB over that window (allocator high-water, lazily - // freed pages), so the allowance sits between the two. ASAN's quarantine + // drifts over that window (allocator high-water, lazily freed pages): + // a few MiB on Linux, up to 14 observed on Windows lanes. The allowance + // sits between that drift and the 32 MiB signal. ASAN's quarantine // retains freed allocations so RSS stays elevated under bun-asan. - MAX_MEMORY_INCREASE: isASAN ? "64" : "16", // in MB + MAX_MEMORY_INCREASE: isASAN ? "64" : "20", // in MB // The fixture asserts RSS stabilizes after iteration 250, but with the // default 256 MB quarantine the freed 128 KB bodies are never reused and // RSS keeps climbing through all 500 iterations (~97 MB past the sample From 3734c39e66774f9063079c2ae88bc677cfc7f158 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 8 Aug 2026 19:04:34 +0000 Subject: [PATCH 4/5] test(fetch-leak): scale the stream fixture's ASAN allowance below the leak signal Same class as the HiveRef bound: the 64 MiB ASAN allowance sat above the ~32 MiB the #23697 regression produces over the sampled window. With the quarantine capped at 32 MB it saturates before the sample point; measured under the debug ASAN build, clean runs drift 1-3 MiB and a simulated regression (every read chunk retained) measures +76 MiB. Bound at 24. --- test/js/web/fetch/fetch-leak.test.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/js/web/fetch/fetch-leak.test.ts b/test/js/web/fetch/fetch-leak.test.ts index e6eda1daa9b1..afbd8eca3421 100644 --- a/test/js/web/fetch/fetch-leak.test.ts +++ b/test/js/web/fetch/fetch-leak.test.ts @@ -756,9 +756,11 @@ test("should not leak using readable stream", async () => { // ~32 MiB over the 250 sampled iterations; RSS of a non-leaking run still // drifts over that window (allocator high-water, lazily freed pages): // a few MiB on Linux, up to 14 observed on Windows lanes. The allowance - // sits between that drift and the 32 MiB signal. ASAN's quarantine - // retains freed allocations so RSS stays elevated under bun-asan. - MAX_MEMORY_INCREASE: isASAN ? "64" : "20", // in MB + // sits between that drift and the 32 MiB signal. Under bun-asan the + // capped quarantine (below) saturates before the sample point: clean + // runs measure 1-3 MiB, a simulated regression ~76, so 24 keeps this + // bound below the signal there too. + MAX_MEMORY_INCREASE: isASAN ? "24" : "20", // in MB // The fixture asserts RSS stabilizes after iteration 250, but with the // default 256 MB quarantine the freed 128 KB bodies are never reused and // RSS keeps climbing through all 500 iterations (~97 MB past the sample From 0d9df3db4a96906510b02ee351f220d0bca5e1b5 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 8 Aug 2026 19:24:57 +0000 Subject: [PATCH 5/5] test(fetch-leak): drop the stale allowance figure from the quarantine-cap comment --- test/js/web/fetch/fetch-leak.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/js/web/fetch/fetch-leak.test.ts b/test/js/web/fetch/fetch-leak.test.ts index afbd8eca3421..6018e4b1a88e 100644 --- a/test/js/web/fetch/fetch-leak.test.ts +++ b/test/js/web/fetch/fetch-leak.test.ts @@ -764,7 +764,7 @@ test("should not leak using readable stream", async () => { // The fixture asserts RSS stabilizes after iteration 250, but with the // default 256 MB quarantine the freed 128 KB bodies are never reused and // RSS keeps climbing through all 500 iterations (~97 MB past the sample - // point, over the 64 MB allowance). Cap the quarantine so freed churn + // point, far over the allowance). Cap the quarantine so freed churn // recycles and the stabilization the test asserts can actually happen. ...(isASAN && { ASAN_OPTIONS: `${bunEnv.ASAN_OPTIONS ?? ""}:quarantine_size_mb=32`.replace(/^:/, "") }), },