Skip to content

test(fetch): deflake cyclic-reference leak tests by asserting on objectTypeCounts delta - #36759

Merged
Jarred-Sumner merged 3 commits into
mainfrom
farm/d8bb3ef1/deflake-cyclic-reference-heapstats
Aug 2, 2026
Merged

Jarred-Sumner merged 3 commits into
mainfrom
farm/d8bb3ef1/deflake-cyclic-reference-heapstats

Conversation

@robobun

@robobun robobun commented Aug 2, 2026 •

Copy link
Copy Markdown
Collaborator

What

request-cyclic-reference.test.ts and response-cyclic-reference.test.ts were flaky on Windows 11 aarch64 (build 86911) with:

expect(received).toBeLessThanOrEqual(expected)
Expected: <= 100
Received: 109

They pass when run alone; they only fail in the CI parallel batch. request-clone-leak.test.ts was flaking the same way on debian x64-asan and both Windows lanes (RSS delta 78MB vs the 64MB ASAN threshold).

Why

bun test --parallel --isolate runs several test files per worker process against the same JSC VM. --isolate swaps the JSGlobalObject per file and gcUnprotect()s the old one, but a stale pointer to the just-swapped global sits on the runner loop's stack, and JSC's conservative root scanner pins it through the next file.

The four objectTypeCounts tests asserted on an absolute VM-wide count, so streams from the previous file in the worker counted against the threshold. Repro on any platform:

# holder.test.ts holds 120 ReadableStreams in a module-level const
$ bun test --parallel=1 --isolate holder.test.ts test/js/web/fetch/response-cyclic-reference.test.ts
...
Expected: <= 100
Received: 124

request-clone-leak.test.ts is RSS-based and already uses a delta, but RSS reflects process-wide allocator state: the step-1 Bun.gc(true) frees the previous file's global into ASAN quarantine right before the baseline read, and the async Bun.gc() in the inner loop falls behind on a larger JSC heap, so peak RSS overshoots.

The Windows-aarch64-only appearance is scheduling luck: workers pull files from a shared queue, so which file precedes these in a given worker depends on timing.

Fix

  • {request,response,fetch}-cyclic-reference.test.ts, server-response-stream-leak.test.ts: take a baseline count before the loop and assert on the delta (matches fetch-stream-cancel-leak.test.ts etc). Verified all four pass with a 120-stream polluter in front under --parallel=1 --isolate.
  • request-cyclic-reference.test.ts: scale to 2000 iterations under ASAN (the second test was already overrunning the 5s default under debug+ASAN on main at 10000).
  • request-clone-leak.test.ts: added to parallel-allowlist.json excludeFiles so it runs solo, matching the existing entries for fs-leak, fetch-leak, streams-leak, html-rewriter-leak, express-memory-leak, node-tls-getpeercert-leak.

Swept the rest of test/: every other objectTypeCounts check in a parallel-allowlisted directory already uses baseline/delta or runs in a spawned subprocess.


[stamp-90s] gate passed · iteration 3 · 5 files touched

passes on PR (with fix)
Test-only change.

Debug/ASAN (expected pass):
$ bun bd test 'test/js/web/fetch/fetch-cyclic-reference.test.ts' 'test/js/web/fetch/request-cyclic-reference.test.ts' 'test/js/web/fetch/response-cyclic-reference.test.ts' 'test/js/web/fetch/server-response-stream-leak.test.ts'
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/js/web/fetch/fetch-cyclic-reference.test.ts test/js/web/fetch/request-cyclic-reference.test.ts test/js/web/fetch/response-cyclic-reference.test.ts test/js/web/fetch/server-response-stream-leak.test.ts
bun test v1.4.0 (8eb9db560)

test/js/web/fetch/fetch-cyclic-reference.test.ts:
(pass) FetchTasklet cyclic reference > fetch with request body stream should not leak with cyclic reference [2615.09ms]
(pass) FetchTasklet cyclic reference > fetch with ReadableStream body should not leak streams [2094.14ms]

test/js/web/fetch/response-cyclic-reference.test.ts:
(pass) stream should not leak when response is cyclic reference to itself [1284.16ms]
(pass) stream should not leak when creating a stream contained in another response [1672.81ms]

test/js/web/fetch/server-response-stream-leak.test.ts:
(pass) Bun.serve response stream leak > proxy server forwarding streaming response should not leak [3299.29ms]

test/js/web/fetch/request-cyclic-reference.test.ts:
(pass) stream should not leak when request is cyclic reference to itself [827.57ms]
(pass) stream should not leak when creating a stream contained in another request [1374.68ms]

 7 pass
 0 fail
 9 expect() calls
Ran 7 tests across 4 files. [15.27s]
Exit: 0
diff hotspot
test/js/web/fetch/fetch-cyclic-reference.test.ts    | 18 ++++++++++++------
 test/js/web/fetch/request-cyclic-reference.test.ts  | 21 +++++++++++++++++----
 test/js/web/fetch/response-cyclic-reference.test.ts | 12 ++++++++++--
 .../web/fetch/server-response-stream-leak.test.ts   | 11 +++++++++--
 test/parallel-allowlist.json                        |  1 +
 5 files changed, 49 insertions(+), 14 deletions(-)

gate history · 2 passed · 1 rejected · iteration 3

evidence per changed file
file                                                   reads  edits  tests
test/js/web/fetch/fetch-cyclic-reference.test.ts           1      2      0
test/js/web/fetch/request-cyclic-reference.test.ts         2      3      0
test/js/web/fetch/response-cyclic-reference.test.ts        1      2      0
test/js/web/fetch/server-response-stream-leak.test.ts      1      2      0
test/parallel-allowlist.json                               1      1      0

…ctTypeCounts delta

These four tests assert on heapStats().objectTypeCounts.ReadableStream (or
Response/Request) against an absolute threshold. In CI they run under
'bun test --parallel --isolate', where each worker process runs several
test files against the same JSC VM and heapStats() is VM-wide, so the
count includes streams left over from whichever file the worker ran
immediately before. The previous file's global is unprotected after the
isolation swap, but a stale pointer on the runner loop's stack keeps it
pinned through conservative root scanning for one more file boundary.

This surfaced as a flaky failure on Windows 11 aarch64 (build 86911) with
'Received: 109..111' against the '<= 100' threshold, but only because
that lane's worker scheduling happened to place a stream-heavy file
directly before these; the mechanism is platform-independent.

Take a baseline count before the loop and assert on the delta, matching
the pattern already used in fetch-stream-cancel-leak.test.ts and others.
@coderabbitai

coderabbitai Bot commented Aug 2, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Fetch leak test baselines

Layer / File(s) Summary
Heap baseline setup
test/js/web/fetch/*cyclic-reference.test.ts, test/js/web/fetch/server-response-stream-leak.test.ts
The tests add heap-count helpers, record per-test ReadableStream, Request, and Response baselines, and adjust request iterations under ASAN.
Baseline-relative assertions
test/js/web/fetch/*cyclic-reference.test.ts, test/js/web/fetch/server-response-stream-leak.test.ts
Leak assertions compare post-GC object growth with recorded baselines. Existing limits of 100 and 50 remain in place.

Possibly related PRs

  • oven-sh/bun#33622: Both changes update web/fetch leak tests and GC-related handling for CI environments.
  • oven-sh/bun#36206: Both changes use post-GC heap counts and bounded leak assertions.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: deflaking cyclic-reference leak tests by using object-count deltas.
Description check ✅ Passed The description explains the problem, cause, fix, affected tests, and verification results, despite using different section headings from the template.

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the claude label Aug 2, 2026
@robobun

robobun commented Aug 2, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 2:05 AM PT - Aug 2nd, 2026

@robobun, your commit 8eb9db5 is building: #87694

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. node-net.test.ts: un-awaited expectMaxObjectTypeCount in named-pipe test fails unrelated subsequent tests on Windows #32092 - Same root cause: absolute objectTypeCounts assertions in one test file pollute results for subsequent files in a shared-VM parallel run on Windows; the baseline/delta pattern this PR applies is the exact fix strategy

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #32092

🤖 Generated with Claude Code

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — test-only deflake that switches four leak tests from absolute objectTypeCounts thresholds to baseline+delta.

What was reviewed:

  • Confirmed the baseline/delta pattern matches existing usage in fetch-stream-cancel-leak.test.ts and s3-stream-cancel-leak.test.ts.
  • Checked that assertion strength is preserved: loops create 500–10000 objects, so a real leak still blows past the unchanged 100/50 thresholds; only cross-file VM noise from --parallel --isolate is subtracted out.
  • Verified no production code is touched and the added Bun.gc(true) before each baseline read stabilizes the starting count.
Extended reasoning...

Overview

This PR modifies four test files in test/js/web/fetch/ to fix flakiness under bun test --parallel --isolate. Each test previously asserted an absolute heapStats().objectTypeCounts value against a threshold; now each records a baseline before the loop (after a forced GC) and asserts on the delta. No production code, thresholds, or loop iteration counts are changed.

Security risks

None. Test-only change with no effect on runtime behavior, no new inputs, no external network access.

Level of scrutiny

Low. This is a mechanical deflake applying an already-established pattern (fetch-stream-cancel-leak.test.ts, s3-stream-cancel-leak.test.ts, pipeTo-signal-leak.test.ts all use the same baseline/delta shape). The key REVIEW.md concern for deflakes — "keep asserting the property the original assertion protected" — is satisfied: the loops allocate 500–10000 objects, so a genuine leak produces a delta far above the unchanged 100/50 bounds. If prior-file objects happen to be collected mid-test the delta can go negative, which still correctly passes toBeLessThanOrEqual.

Other factors

The PR description gives a concrete cross-platform repro (--parallel=1 --isolate with a 120-stream polluter file) demonstrating the failure mode is real cross-file VM pollution rather than a masked regression. The author also swept test/ for the same pattern and reports the remaining objectTypeCounts checks are already delta-based or subprocess-isolated. No prior reviewer comments to address.

The extra heapStats() read for the baseline is cheap, so the leading full
GC is unnecessary; the delta assertion holds whether or not the prior
test's garbage has been swept yet.

request-cyclic-reference's second test (async pull + two Requests + .body
access per iteration) already overruns the 5s default timeout under
debug+ASAN on main at 10000 iterations. Scale to 2000 under ASAN; a real
cycle leak still leaves ~2000 streams against the 100 threshold.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/js/web/fetch/fetch-cyclic-reference.test.ts`:
- Around line 12-13: Run Bun.gc(true) immediately before every listed baseline
capture in fetch-cyclic-reference.test.ts (both Request and ReadableStream
baselines), request-cyclic-reference.test.ts, response-cyclic-reference.test.ts,
and server-response-stream-leak.test.ts. Preserve the existing countOf and
baseline logic while ensuring each heapStats objectTypeCounts baseline is
captured only after garbage collection.
- Line 8: Replace the seven heap-stat lookup fallbacks using || 0 with nullish
fallbacks ?? 0, preserving zero-valued counts. Apply this to countOf in
test/js/web/fetch/fetch-cyclic-reference.test.ts:8-8,
request-cyclic-reference.test.ts:9-9, response-cyclic-reference.test.ts:8-8, and
server-response-stream-leak.test.ts:10-11.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3fc06e3f-1b39-41de-b778-187b07578fd9

📥 Commits

Reviewing files that changed from the base of the PR and between 2569ae3 and 6123e5a.

📒 Files selected for processing (4)
  • test/js/web/fetch/fetch-cyclic-reference.test.ts
  • test/js/web/fetch/request-cyclic-reference.test.ts
  • test/js/web/fetch/response-cyclic-reference.test.ts
  • test/js/web/fetch/server-response-stream-leak.test.ts

Comment thread test/js/web/fetch/fetch-cyclic-reference.test.ts
Comment thread test/js/web/fetch/fetch-cyclic-reference.test.ts

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — test-only deflake switching absolute objectTypeCounts thresholds to baseline+delta.

What was reviewed:

  • Delta pattern matches the existing precedent in fetch-stream-cancel-leak.test.ts (baseline before loop, assert on after - baseline).
  • ASAN iteration reduction (10000→2000) in request-cyclic-reference.test.ts still leaves a ~20× margin over the 100 threshold if the cycle leak regresses, and isASAN is the harness export — this shrinks the workload rather than raising the timeout, per repo guidance.
  • Checked that a negative delta (GC frees pre-existing streams from the prior file) can't cause a false pass masking a real leak: a regression leaves ~iterations objects behind, which dwarfs any baseline.
Extended reasoning...

Overview

Test-only change to four leak-regression tests in test/js/web/fetch/. Each previously asserted heapStats().objectTypeCounts.ReadableStream <= N as an absolute; under bun test --parallel --isolate the VM is shared across files in a worker, so objects pinned by a conservatively-scanned stale global from the previous file inflate the count. The fix captures a baseline before the loop and asserts on the delta. request-cyclic-reference.test.ts additionally scales iterations to 2000 under ASAN via isASAN from harness.

Security risks

None. No production code, no inputs, no external I/O changes.

Level of scrutiny

Low. This is a mechanical deflake of test assertions following a pattern already established in the same directory (fetch-stream-cancel-leak.test.ts) and elsewhere. The PR description names the specific flaky build, gives a deterministic local repro (--parallel=1 --isolate with a 120-stream polluter file), and confirms the sweep found no other absolute-threshold objectTypeCounts checks in parallel-allowlisted dirs.

Other factors

REVIEW.md's de-flaking rule — "keep asserting the property the original assertion protected" — is satisfied: the property is "N iterations don't leave >threshold streams behind", and 2000 leaked streams vs a 100 threshold is still an unambiguous failure. The ASAN scaling follows "shrink the workload" over "raise the timeout". isASAN is confirmed exported from test/harness.ts:54. No prior reviews or unresolved comments on the PR.

@robobun

robobun commented Aug 2, 2026 •

Copy link
Copy Markdown
Collaborator Author

CI at 8eb9db5 (build 87694): all five changed files pass on every lane. request-clone-leak.test.ts is no longer in the flake list now that it runs solo.

Remaining reds are unrelated to this diff: bun-upgrade.test.ts pre-existing on win-aarch64, s3.test.ts Cloudflare R2 ServiceUnavailable (external, down to 2 lanes from 5 as R2 recovers), and assorted parallel-batch flakes that passed on retry. Ready to merge.

RSS-delta leak tests are sensitive to process-wide allocator state. In a
--parallel --isolate worker that has already run other files, the step-1
Bun.gc(true) frees the previous file's global into ASAN's quarantine
right before the baseline read, and the async Bun.gc() in the inner loop
falls behind on a larger JSC heap, pushing peak RSS past the threshold
(78MB vs 64 on debian x64-asan in build 87678; also flaking on both
Windows lanes against the 30MB non-ASAN threshold).

Matches the existing excludeFiles entries for fs-leak, fetch-leak,
streams-leak, html-rewriter-leak, express-memory-leak and
node-tls-getpeercert-leak.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — test-only deflake converting absolute objectTypeCounts assertions to baseline-delta, matching the established pattern in fetch-stream-cancel-leak.test.ts and siblings. The inline note on parallel-allowlist.json is a durability nit, not a blocker.

Extended reasoning...

Overview

This PR touches four leak-detection test files under test/js/web/fetch/ and adds one entry to test/parallel-allowlist.json. The core change replaces absolute heapStats().objectTypeCounts.X <= N assertions with baseline-captured-before-loop delta assertions, so VM-wide object counts carried over from a prior test file in the same --parallel --isolate worker no longer trip the threshold. request-cyclic-reference.test.ts additionally scales iterations from 10000 to 2000 under isASAN to stay under the default per-test timeout. No runtime/native code is touched.

Security risks

None. Test-only; no user-facing surface, no auth/crypto/permissions paths, no external inputs.

Level of scrutiny

Low. This is a mechanical deflake applying a pattern already used elsewhere in the same directory (fetch-stream-cancel-leak.test.ts, s3-stream-cancel-leak.test.ts). The key REVIEW.md concern for deflakes — "keep asserting the property the original assertion protected" — is satisfied: a real cycle-collection regression leaks on the order of iterations objects (500–10000), which dwarfs both the ~100-object cross-file carryover and the 50/100 thresholds, so the delta form still fails loudly on a real leak. The carryover objects are pinned by a conservative stack root through the next file (per the PR description), so they appear in both baseline and final reads and cancel out cleanly. The ASAN iteration reduction leaves ~20× headroom (2000 leaked vs 100 threshold).

Other factors

  • All four files were run under bun bd test per the PR evidence block (7 pass, 0 fail).
  • Both CodeRabbit threads (|| 0 vs ?? 0, and pre-baseline Bun.gc(true)) were addressed with sound reasoning by the author and withdrawn/resolved.
  • The one remaining finding — the hand-edited excludeFiles entry in the generated parallel-allowlist.json will be dropped on the next regeneration — is real but bounded: worst case, the excluded test re-enters the parallel bucket, flakes once, and the next regeneration re-excludes it via the flake-annotation path. It does not affect correctness of this PR's primary change and is self-correcting, so it does not block approval.
  • No prior claude[bot] reviews on this PR.

Comment thread test/parallel-allowlist.json
@Jarred-Sumner
Jarred-Sumner merged commit 8851953 into main Aug 2, 2026
48 of 50 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/d8bb3ef1/deflake-cyclic-reference-heapstats branch August 2, 2026 08:05
robobun added a commit that referenced this pull request Aug 17, 2026
… more in-process leak tests

The runner reads only parallel-allowlist.json, so a denylist entry keeps a
file out of the batch only once a regen has copied it into excludeFiles.
Assert that every denylisted file in a listed dir is in excludeFiles (and
that denylist entries are real files), so a by-hand denylist addition or a
table produced by an older script fails in CI instead of running the file
in the batch. Against the table from before #39373 the check names the 97
files that PR's body describes as batch-flaking.

Add the three in-process RSS/heapStats leak tests whose batch failures
predate the window the denylist was collected from (98887..99420), so the
next regen cannot promote them: shell/leak (#36585: failed in the batch in
340 of 400 builds), request-clone-leak (excluded by hand in #36759 after
batch failures on x64-asan and Windows) and zlib/leak (build 86483). All
three are already in excludeFiles; this only makes the exclusion stick.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants