Skip to content

gc: give back the large pages mimalloc keeps at the idle full collection, and test oven-sh/mimalloc#52 - #42832

Open
robobun wants to merge 2 commits into
mainfrom
robobun/2dc95571/mimalloc-floor-survives-collect
Open

robobun wants to merge 2 commits into
mainfrom
robobun/2dc95571/mimalloc-floor-survives-collect

Conversation

@robobun

@robobun robobun commented Sep 16, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Fix

Background

Notes

This PR had the pin bump too. #42853 merged the pin alone, so the branch is rebased and keeps what main does not have: the two tests and the idle collect. The commits are independent. Drop the second one to keep the idle cost that #42853 states.

Release builds, Linux x64. "pin only" is main since #42853.

Idle echo server (Bun.serve that answers with the request body, 100 requests at 5 a second, then idle; anonymous RSS in KiB at +10 s / +70 s / +310 s):

body before #42853 pin only this PR floor 0
128 KiB 2932 / 2036 / 2020 2808 / 2808 / 2792 2956 / 2060 / 2048 2032 / 2032 / 2020
512 KiB 2424 / 2424 / 2408 5680 / 5680 / 5668 2456 / 2456 / 2444 2420 / 2420 / 2408

(The "this PR" column was measured on a later main, so its baseline is about 25 KiB higher.)

What the forced collects cost a server that is not idle. The controller counts a heap as quiet when the JS heap does not grow, so a server that only moves request bodies is quiet while it serves requests. It loses the kept pages at the idle collections (at most three in a quiet period, each with two forced collects). Echo server, 512 KiB body, one request every 2 s, 40 requests, 3 runs, faults per request: before #42853 129.6 / 122.0 / 144.3, pin only 5.7 / 5.1 / 5.5, this PR 22.2 / 23.1 / 21.9. In each run of this PR one request of the 40 re-faults its page (321 faults). The others are as with the pin only.

Why two forced collects. collect_async_idle only requests the collection. A forced collect at that moment returns what is already free. The second one, at the next fire of the GC timer (1 s later in fast mode, up to 30 s in slow mode), returns what the collection has freed since. I did not shorten that fire: idle_tick counts quiet time in nominal tick intervals, and an extra short fire would move the later idle collections closer together than the CodeBlock aging lease.

The tests. Both are Linux only, skip under ASAN (malloc is not mimalloc there) and skip when a page is not 4 KiB, because they count faults in pages of that size. Bun.gc(false) is mi_collect(false), the same collect that JavaScriptCore runs after a full collection, without a dependency on when JavaScriptCore decides to collect. The idle test fails with the pin before #42853 (the page is gone while the process is active) and on main (the page stays after the idle collection).

Two older tests in gc-controller-cadence.test.ts ("idle release lets FTL code age out") time out under a debug ASAN build. They do the same with a debug build from before this branch.


no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/bun/jsc/heapStats-mimalloc.test.ts, test/js/bun/gc/gc-controller-cadence.test.ts

@coderabbitai

coderabbitai Bot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 0747eca4-a362-48a2-abd0-6c4da4679527

📥 Commits

Reviewing files that changed from the base of the PR and between b841a68 and 82a0db3.

📒 Files selected for processing (5)
  • scripts/build/deps/mimalloc.ts
  • src/jsc/GarbageCollectionController.rs
  • test/harness.ts
  • test/js/bun/gc/gc-controller-cadence.test.ts
  • test/js/bun/jsc/heapStats-mimalloc.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 0 remain after this review.


Walkthrough

Changes

Idle allocator cleanup

Layer / File(s) Summary
Allocator cleanup scheduling
scripts/build/deps/mimalloc.ts, src/jsc/GarbageCollectionController.rs
The mimalloc revision changes. Idle full collections force allocator cleanup and schedule a second cleanup on the next timer tick.
Linux page-size detection
test/harness.ts
The harness adds linuxPageSize(), which reads AT_PAGESZ on Linux and returns 0 when unavailable.
Allocator retention validation
test/js/bun/gc/gc-controller-cadence.test.ts, test/js/bun/jsc/heapStats-mimalloc.test.ts
Linux, non-ASAN tests measure page faults and RSS behavior across allocation bursts, non-forced collections, and idle cleanup.

Suggested reviewers: jarred-sumner

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to dfba9

No concrete merge-blocking regression was identified in the allocator cleanup scheduling or its Linux-specific test coverage.

🚥 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 describes the main change: returning large mimalloc pages during idle full garbage collection. It is specific and related to the changeset.
Description check ✅ Passed The description explains the problem, fix, background, test coverage, verification results, and platform scope. It does not use the exact template headings, but it provides the required information in…

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

@robobun

robobun commented Sep 16, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

#42853 merged the pin alone, so this PR is rebased and now carries what main does not have: the test for oven-sh/mimalloc#52 and the forced collect at the idle full collection.

How the idle cost reproduces on main (release build, Linux x64): start Bun.serve({ fetch: async req => new Response(await req.arrayBuffer()) }), send 100 requests with a 512 KiB body at 5 a second, then read RssAnon from /proc/<pid>/status. It is 5680 KiB at +10 s, +70 s and +310 s. Before #42853 it was 2424 KiB, with this branch it is 2456 KiB.

How the test for oven-sh/mimalloc#52 fails with the pin before #42853 (11 of 12 bursts fault all 512 pages in again, none with the current pin):

// MIMALLOC_PURGE_HOLES_MIN_INTERVAL=10 bun repro.mjs
import { readFileSync } from "node:fs";
const faults = () => {
  const stat = readFileSync("/proc/self/stat", "utf8");
  const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
  return Number(fields[7]) + Number(fields[9]);
};
const burst = () => {
  const buffers = [];
  for (let i = 0; i < 4; i++) buffers.push(new Uint8Array(512 * 1024).fill(1));
  for (const b of buffers) b.buffer.transfer(0);
};
for (let i = 0; i < 3; i++) { burst(); await Bun.sleep(30); }
const perBurst = [];
for (let i = 0; i < 12; i++) {
  const before = faults();
  burst();
  perBurst.push(faults() - before);
  await Bun.sleep(60);
  Bun.gc(false); // mi_collect(false), the collect JavaScriptCore runs after a full collection
  await Bun.sleep(40);
}
console.log(JSON.stringify(perBurst));
// before #42853: [6,513,512,512,512,512,512,512,512,512,512,512]
// now:           [4,0,0,0,0,0,0,0,0,0,0,0]

This PR: #42832

CI

The diff is green. The new tests pass on every lane that runs them (builds 116178, 116207, 116213 and 116395). The one red test in each build is test/js/bun/http/serve-pending-promise-abort-leak.test.ts on the debian 13 x64-asan lane ("client abort of a streaming Response releases the body stream it held"). This PR does not touch that code, and under ASAN the new mi_collect call compiles to nothing (USE_MIMALLOC is false there). The CI helper marks the failure as present on main too, it is red on about one build in five of unrelated branches, and it is reported to main-break triage. Every other failure passed on retry.

@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.

Beyond the inline findings, I also checked the new mimalloc_cleanup(true) call under ASAN builds — bun_core::Global::mimalloc_cleanup is gated on USE_MIMALLOC (cfg!(not(bun_asan))), so it is a no-op there rather than a call into a non-mimalloc allocator; and mi_collect acts on the calling thread's heap, so running it on the JS thread from the controller does not cross a thread boundary.

Extended reasoning...

Findings were posted inline covering the ordering of the forced collect relative to the async idle collection, the behavioral change of the mimalloc pin for threads without a GC controller, and the page-size and base-branch-validity concerns in the new tests. Separately I confirmed from src/bun_core/Global.rs:823 and src/bun_core/lib.rs:2323 that the new call is compiled out under ASAN and only touches the calling thread's mimalloc heap, so those two potential concerns (allocator mismatch under ASAN, cross-thread heap access) are not issues with this change. The dep bump itself and the semantics of the new upstream commit remain unverifiable from the diff alone and warrant a human look alongside the inline comments.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment thread src/jsc/GarbageCollectionController.rs
Comment thread scripts/build/deps/mimalloc.ts
Comment thread test/js/bun/gc/gc-controller-cadence.test.ts
Comment thread test/js/bun/gc/gc-controller-cadence.test.ts
Comment thread src/jsc/GarbageCollectionController.rs Outdated
Comment thread src/jsc/GarbageCollectionController.rs Outdated

@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.

Code review found no issues

No high-confidence issues detected in this change.

One verified lower-impact observation (a convention, logging or cleanup point) was not posted.

@robobun

robobun commented Sep 16, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 12:43 AM PT - Sep 16th, 2026

❌ @robobun, your commit dfba93c has 1 failures in Build #116395 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 42832

That installs a local version of the PR into your bun-42832 executable, so you can run:

bun-42832 --bun

@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.

Code review found no issues

No high-confidence issues detected in this change.

…e sweep kept (oven-sh/mimalloc#52)

Bursts of four 512 KiB buffers with Bun.gc(false) between them, which is
mi_collect(false), the collect that JavaScriptCore runs after each full
collection. With the pin before #42853, 11 of 12 bursts fault all 512
pages of their buffers in again. With the current pin none does.

Linux only, not ASAN, and only where a page is 4 KiB (the test counts
faults in pages of that size). linuxPageSize() in the harness reads
AT_PAGESZ from /proc/self/auxv.
…een quiet

Since oven-sh/mimalloc#52 (#42853) the collect at the end of a
JavaScriptCore collection leaves the large pages that mimalloc keeps for
the next burst (up to 4 MiB). That collect was what gave them back in an
idle process, so an idle server now holds them for as long as it is idle.

GarbageCollectionController runs mi_collect(true) when it requests an idle
full collection (after BUN_IDLE_GC_SECONDS in which the JS heap did not
grow, at most three times in a quiet period, main thread only), and once
more at the next timer fire, for what that collection has freed since.

Test: the page of six 512 KiB buffers stays while the process is active,
also across Bun.gc(false), and is gone after the idle collection. Linux
only, not ASAN, 4 KiB pages.
@robobun robobun changed the title mimalloc: a collect that is not forced leaves the large page that the idle sweep kept (oven-sh/mimalloc#52) gc: give back the large pages mimalloc keeps at the idle full collection, and test oven-sh/mimalloc#52 Sep 16, 2026
@robobun
robobun force-pushed the robobun/2dc95571/mimalloc-floor-survives-collect branch from 82a0db3 to dfba93c Compare September 16, 2026 07:20

@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.

Code review completed

Nothing new to post: everything this review found is already covered by existing comments on this pull request or didn't merit a separate one.

This branch has not been deployed

No deployments
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