Skip to content

test(fetch-leak): pin mimalloc purge state in the HiveRef Request-body test - #36981

Closed
robobun wants to merge 1 commit into
mainfrom
farm/cef242c7/fetch-leak-hiveref-threshold
Closed

test(fetch-leak): pin mimalloc purge state in the HiveRef Request-body test#36981
robobun wants to merge 1 commit into
mainfrom
farm/cef242c7/fetch-leak-hiveref-threshold

Conversation

@robobun

@robobun robobun commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

The "Request body HiveRef pool returns slot via Body.Value.deinit (does not leak)" String case in test/js/web/fetch/fetch-leak.test.ts gates on a 64 MB RSS delta and has been flaking on loaded CI lanes with marginal overages, always going green on retry:

error: Request body (String) leaked 74 MB over 32 cycles of 512 Requests

Observed in the last ~550 Buildkite builds: 88734 (macOS 13 x64, 74 MB), 88735 (Ubuntu 25.04 x64, 74 MB), 88783/88819/89090/89250 (Windows 11 aarch64, 68 MB), 89215 (Alpine 3.23 x64, 74 MB).

Cause

The overage is not memory growth. Bun.gc() runs mimalloc's collect before the JSC sweep (garbage_collect in VirtualMachine.rs), so the 64 MiB of bodies each measurement GC frees stays committed until a later purge, and mimalloc's purge delay defaults to 100ms, which the test's synchronous cycle loop may never service. Whether the baseline and final rss() reads each land on purged or unpurged state is timing dependent, and the mismatch shows up as a flat offset of about one cycle's working set, not as a ramp:

  • running the test's inner script on the release binary with per-cycle logging shows two discrete RSS levels and no growth
  • inserting a single 300ms sleep before the baseline read (simulating a descheduled runner) reproduces the CI band deterministically: delta jumps to 61-85 MB at cycle 1 and stays flat through all 32 cycles
  • with MIMALLOC_PURGE_DELAY=0, delta is -2 to 0 in every run, with or without the sleep

This also explains why the CI values cluster at exactly 68 and 74 MB per platform instead of varying with load.

Fix

Set MIMALLOC_PURGE_DELAY=0 in the spawned process env so frees decommit eagerly and rss() measures live memory. The 64 MB bound is unchanged. This mirrors the allocator-layer fix the same file already uses for ASAN retention (capping quarantine_size_mb in the readable-stream test) rather than widening thresholds over allocator behavior.

The env var cannot mask the regression the test guards: purge only decommits freed pages, and a skipped Body.Value.deinit keeps bodies resident. Retaining just 4 of the 32 cycles measures +260 MB against the 64 MB bound; the full leak is ~2 GiB.

An earlier revision of this PR raised the bound to 128 MB instead; review of the mechanism showed the noise band is purge timing, not an irreducible high-water, so this revision fixes the measurement and keeps the bound where it was.

How did you verify your code works?

  • USE_SYSTEM_BUN=1 bun test test/js/web/fetch/fetch-leak.test.ts -t 'Request body' passes 3/3 with deltaMB 0 (baseline RSS drops from ~100 MB to ~39 MB now that it reflects live memory)
  • bun bd test (debug+ASAN) passes with deltaMB 7 against the unchanged 320 MB ASAN bound
  • the release binary honors the env var (vendored mimalloc reads MIMALLOC_* via getenv on unix and windows; MIMALLOC_VERBOSE=1 shows option 'purge_delay': 0)

no test proof · iteration 0 · Platform-specific test-only change; deferring to CI.

@robobun

robobun commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Status: ready for review.

Reproduced the flake signature in Buildkite annotations for builds 88734, 88735, 88783, 88819, 89090, 89215, 89250 (68-74 MB against the 64 MB bound, every one green on retry), then reproduced the mechanism locally: a 300ms stall before the baseline read turns the delta into a flat 61-85 MB step (mimalloc purge-state mismatch between the two rss() reads), and MIMALLOC_PURGE_DELAY=0 pins it at 0 in every run. The bound stays at 64 MB.

CI: builds 89261 and 89296 both came back 195/196 jobs green with fetch-leak.test.ts passing on every lane. The one failed job in each (debian 13 x64-asan) is a pre-existing AsyncLocalStorage-tracking.test.ts leak report that also fails on main and is unrelated to this change.

@github-actions github-actions Bot added the claude label Aug 5, 2026
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 1 minute

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 81000a06-be62-44ac-8979-edf389c3e702

📥 Commits

Reviewing files that changed from the base of the PR and between 1f606e6 and f859853.

📒 Files selected for processing (1)
  • test/js/web/fetch/fetch-leak.test.ts

Walkthrough

The fetch request-body leak test now documents observed post-GC RSS values and increases the non-ASAN RSS threshold from 64 MB to 128 MB. The ASAN threshold remains 320 MB.

Changes

Fetch leak test

Layer / File(s) Summary
RSS threshold adjustment
test/js/web/fetch/fetch-leak.test.ts
The test documents 68–74 MB post-GC RSS on loaded CI runners. The non-ASAN limit changes to 128 MB, while the ASAN limit remains 320 MB.

Possibly related PRs

  • oven-sh/bun#36271: Adds compressed-response memory tests in the same fetch backpressure and leak-testing area.
  • oven-sh/bun#36429: Adjusts RSS leak-test thresholds in the same test file.

Suggested reviewers: jarred-sumner, ciropaciari

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title describes pinning mimalloc purge state, but the changeset only raises the RSS threshold and documents allocator behavior. Update the title to describe the actual threshold change, such as raising the non-ASAN RSS limit for the request-body leak test.
✅ Passed checks (3 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.
Description check ✅ Passed The description includes both required sections and provides detailed context, cause analysis, fix intent, and verification results.

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

@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 threshold bump backed by cited CI flakes.

What was reviewed:

  • The new 128 MB bound clears the observed 68-74 MB noise band while staying ~16× below the ~2 GiB signal a real skipped-deinit leak would produce, so the invariant the test guards is still asserted.
  • ASAN branch (320 MB) is unchanged; the updated comment records the CI lanes and observed noise band per the leak-test guidance in REVIEW.md.
  • No production code touched; only the String variant is affected (the only one in the loop).
Extended reasoning...

Overview

This PR touches a single test file, test/js/web/fetch/fetch-leak.test.ts, changing one integer constant (the non-ASAN RSS-delta threshold in the "Request body HiveRef pool returns slot via Body.Value.deinit" test) from 64 MB to 128 MB, and updating the adjacent comment to record why. No runtime, native, or built-in JS code is modified.

Security risks

None. This is a test-only threshold adjustment with no bearing on production code paths, auth, crypto, or user-facing behavior.

Level of scrutiny

Low. The change is mechanical and self-contained: a leak-test RSS bound is being widened to absorb documented CI allocator noise. The key review question per REVIEW.md's de-flaking guidance — "keep asserting the property the original assertion protected" — is satisfied: the test guards against a skipped Body.Value.deinit() on hive-pool return, which the test's own comment quantifies as ~2 GiB of retained bodies (32 × 512 × 128 KiB). The new 128 MB bound is ~16× below that, so a real regression still fails hard. The bound also remains branched on isASAN as the leak-test guidance requires.

Other factors

  • The PR description cites seven specific Buildkite builds (88734, 88735, 88783, 88819, 89090, 89215, 89250) across four platforms where the flake reproduced at 68-74 MB and went green on retry, giving concrete evidence this is allocator high-water noise on loaded runners rather than a masked leak.
  • The rationale is sound: each cycle holds 64 MiB of live bodies simultaneously (512 × 128 KiB), so a 64 MB post-GC RSS delta was sitting right at the working-set size with zero headroom. 128 MB gives ~73% headroom over the worst observed noise.
  • The comment update records the observed noise band and affected lanes, which is exactly what REVIEW.md asks for when adjusting leak thresholds.
  • No prior reviewer comments to address; no CODEOWNER-gated paths.

…y test

The String case flaked on loaded CI lanes with 68-74 MB against its
64 MB RSS bound, always green on retry. The overage is not growth:
Bun.gc() runs mimalloc's collect before the JSC sweep, so the 64 MiB of
bodies a measurement GC frees stays committed until a purge (default
delay 100ms) that the synchronous cycle loop may never reach, and
whether each rss() read lands on purged or unpurged state produces a
flat one-working-set offset between baseline and final.

Set MIMALLOC_PURGE_DELAY=0 for the spawned process so frees decommit
eagerly and rss() measures live memory: the delta settles at 0 on every
run, including with a simulated 300ms descheduling before the baseline
read, which reliably reproduced the 61-85 MB CI band without the env
var. Purge only affects freed pages, so a skipped Body.Value.deinit
still presents at full size (retaining 4 of the 32 cycles measures
+260 MB). The 64 MB bound is unchanged.
@robobun
robobun force-pushed the farm/cef242c7/fetch-leak-hiveref-threshold branch from d01afb8 to f859853 Compare August 5, 2026 19:04
@robobun robobun changed the title test(fetch-leak): raise the HiveRef Request-body RSS bound above CI noise test(fetch-leak): pin mimalloc purge state in the HiveRef Request-body test Aug 5, 2026
@robobun

robobun commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Note for reviewers: the walkthrough and pre-merge warnings above describe the first revision of this PR (d01afb8, which raised the bound to 128 MB). The current revision (f859853) does not change the threshold; it keeps the 64 MB bound and adds MIMALLOC_PURGE_DELAY=0 to the spawned process env, as the PR description says. The summary was generated while the incremental review was rate limited, so it reflects the old diff.

@robobun

robobun commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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 de-flake that pins mimalloc purge timing instead of widening the threshold.

What was reviewed

  • The 64 MB non-ASAN bound is unchanged, so the assertion the test protects is not weakened.
  • { ...bunEnv, MIMALLOC_PURGE_DELAY: "0" } matches the harness convention and the same pattern already used in test/napi/napi.test.ts and test/cli/install/bun-create.test.ts.
  • Purge only decommits freed pages, so a real Body.Value.deinit skip would still keep the ~2 GiB of retained bodies resident and blow past 64 MB — the env var cannot mask the regression.
Extended reasoning...

Overview

This PR touches a single test file, test/js/web/fetch/fetch-leak.test.ts, adding MIMALLOC_PURGE_DELAY: "0" to the env of the spawned subprocess in the "Request body HiveRef pool returns slot via Body.Value.deinit" test, plus an explanatory comment. No runtime code is changed and the leak threshold stays at 64 MB (320 MB under ASAN).

Security risks

None. This is a test-only environment-variable change to a spawned child process. No auth, crypto, or user-facing surface is touched.

Level of scrutiny

Low. The change is a targeted de-flake of a leak-detection test. The key REVIEW.md concern for de-flaking — "keep asserting the property the original assertion protected" — is satisfied: the bound is unchanged, and the fix operates at the allocator layer (purge only decommits freed pages), so a genuine leak where bodies are never freed would still manifest at full size (~2 GiB against a 64 MB bound). This is the same style of allocator-layer fix the file already uses for ASAN quarantine (quarantine_size_mb).

Other factors

  • bunEnv is correctly spread before the override, per harness convention.
  • MIMALLOC_PURGE_DELAY is an established pattern in this repo (test/napi/napi.test.ts:608,622, test/cli/install/bun-create.test.ts:457), so the vendored mimalloc is known to honor it.
  • The PR description documents a deterministic local repro (300ms stall before the baseline read → flat 61-85 MB offset) and verification that the env var pins delta to 0 on release and 7 MB on debug+ASAN.
  • The earlier revision that raised the bound to 128 MB was replaced with this measurement fix, which is the right call per the review guide (fix the measurement, don't widen thresholds over allocator behavior).
  • No prior reviewer comments to address; no CODEOWNERS on test files.

@robobun

robobun commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #37209, which fixes the same HiveRef flake by shrinking the per-Request bodies to 16 KiB so the purge-timing offset this PR diagnosed (one cycle of freed pages still committed) drops to 8 MiB against the 64 MiB bound. The diagnosis here is what confirmed that margin, and #37209 credits it. Closing.

@robobun robobun closed this Aug 8, 2026
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.

1 participant