Skip to content

perf(standalone): madvise(MADV_DONTNEED) embedded source pages after entrypoint load - #29320

Merged
cirospaciari merged 5 commits into
mainfrom
ciro/madvise-bun-section-only
Apr 21, 2026
Merged

cirospaciari merged 5 commits into
mainfrom
ciro/madvise-bun-section-only

Conversation

@cirospaciari

Copy link
Copy Markdown
Member

What does this PR do?

For compiled standalone binaries (bun build --compile), call madvise(MADV_DONTNEED) on the embedded __BUN,__bun (Mach-O) / .bun (ELF) section once the entrypoint module has been parsed and the initial microtasks have drained. The section holds the bundled JS source text; after JSC has parsed it into bytecode the source pages are only re-read for stack-trace line lookup or lazy require() of modules not yet loaded, both of which fault back in transparently from the file-backed mapping.

Does nothing in the regular bun interpreter (no embedded section) or on Windows.

Why is this safe

MADV_DONTNEED is a hint to the kernel — it doesn't change the mapping, protection, or backing store. Pages remain valid and reads continue to work whether or not the kernel reclaims them. There is no window where the range is inaccessible, and the call cannot introduce a SIGSEGV/SIGBUS that wouldn't have happened anyway. How aggressively the kernel acts on the hint is platform- and pressure-dependent; the call is correct on every POSIX target regardless.

The pages are clean and file-backed, so a later read faults them back in from the executable on disk at page-fault cost.

Implementation

StandaloneModuleGraph.hintSourcePagesDontNeed() reuses the existing Macho.getData() / ELF.getData() section lookup, page-aligns the range, and calls std.posix.madvise(.., MADV.DONTNEED). Called from bun.js.zig immediately after the post-entrypoint releaseWeakRefs/runGC/vm.tick() block.

Relationship to #29313

This is the conservative variant — madvise only, on both macOS and Linux. #29313 additionally uses mmap(MAP_FIXED) overlay on macOS where madvise is treated as a hint that may not immediately reclaim file-backed pages. Both PRs are open so the tradeoff can be evaluated independently.

How did you verify your code works?

zig build check-all passes (macOS/Linux/Windows semantic analysis).

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. perf(standalone): release embedded source pages after entrypoint load (mmap MAP_FIXED on macOS, madvise on Linux) #29313 - Also reclaims embedded source pages in standalone binaries after entrypoint load; uses mmap(MAP_FIXED) on macOS + madvise on Linux (the more aggressive variant of this same optimization)

🤖 Generated with Claude Code

@cirospaciari
cirospaciari marked this pull request as ready for review April 17, 2026 16:50
@robobun

robobun commented Apr 17, 2026 •

Copy link
Copy Markdown
Collaborator
Updated 4:31 PM PT - Apr 21st, 2026

❌ @cirospaciari, your commit 4969ef7 has 2 failures in Build #46893 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 29320

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

bun-29320 --bun

@coderabbitai

coderabbitai Bot commented Apr 17, 2026 •

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fdb587b8-6129-4629-b240-c04c63ad12f2

📥 Commits

Reviewing files that changed from the base of the PR and between a439a0f and 4969ef7.

📒 Files selected for processing (2)
  • src/bun.js.zig
  • test/js/bun/compile/standalone-madvise-tla.test.ts

Walkthrough

Adds a non-Windows public StandaloneModuleGraph.hintSourcePagesDontNeed() that computes a page-aligned region for embedded blob bytes and issues a best-effort madvise(..., MADV_DONTNEED). Run.start now conditionally invokes it for non-watcher runs after entry-point loading. A test asserts the debug hint is emitted for a TLA standalone binary.

Changes

Cohort / File(s) Summary
Memory hinting function
src/StandaloneModuleGraph.zig
New pub fn hintSourcePagesDontNeed() void (non-Windows) that obtains platform-specific embedded blob bytes, computes a page-aligned memory range, and calls madvise(..., MADV_DONTNEED) if data exists. Returns early when blob data is missing/empty and logs debug warnings on success or madvise failure.
Function invocation
src/bun.js.zig
Added a conditional call to bun.StandaloneModuleGraph.hintSourcePagesDontNeed() in Run.start after entry-point loading and initial GC decision, guarded to skip when the VM watcher/hot-reload is enabled.
Test for hint emission
test/js/bun/compile/standalone-madvise-tla.test.ts
New test (skipped on Windows and non-debug builds) that builds and runs a standalone binary from a top-level-await entry, asserts expected stdout lines and exit code 0, and verifies stderr contains the hintSourcePagesDontNeed: debug hint.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding madvise(MADV_DONTNEED) optimization to standalone binary source pages after entrypoint load.
Description check ✅ Passed The description fully covers both required sections with clear explanations of what the PR does, the safety rationale, implementation details, and verification steps.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

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

This PR is technically correct and conservative — MADV_DONTNEED is a safe hint. However, since a competing PR (#29313) covers the same ground with an additional mmap(MAP_FIXED) overlay for macOS, a human should decide which variant (or combination) to land.

Extended reasoning...

Overview

The PR adds (~30 lines in StandaloneModuleGraph.zig) and calls it from after the entrypoint is loaded and initial microtasks drain. It uses to hint the kernel that the embedded source section pages are no longer hot. Windows is excluded at comptime; errors are caught and emitted as debug-only warnings (non-fatal).

Security risks

None. is a pure hint — it changes no mapping protections, cannot cause SIGSEGV/SIGBUS, and does not alter backing store. The implementation does not expose any user-controlled data to the syscall.

Level of scrutiny

Low-risk, focused change. The logic is self-contained, reuses existing helpers, and the madvise call is guarded against failure. The call site is appropriate (after the microtask drain, before the watcher loop).

Other factors

A bot flagged this as a potential duplicate of PR #29313, which implements the same optimization but additionally uses on macOS for a more aggressive reclaim. Both PRs are intentionally open so the tradeoff can be evaluated. That design decision — conservative madvise-only vs. more aggressive mmap overlay — requires a human call on which variant(s) to merge.

Comment thread src/StandaloneModuleGraph.zig Outdated
- std.posix.madvise hits `unreachable` on unexpected errnos; this is a
  best-effort hint, so call libc directly and just log on failure.
- Add a debug-only test that compiles a TLA entrypoint as a standalone
  binary and asserts the hintSourcePagesDontNeed debugWarn fires —
  verifying the call site at bun.js.zig:466 is reached even when the
  entrypoint promise is still pending. loadEntryPoint() returns without
  blocking, so control falls through to the madvise before the main
  event loop spins.

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/js/bun/compile/standalone-madvise-tla.test.ts`:
- Around line 41-42: The test currently asserts that stderr contains the exact
string "hintSourcePagesDontNeed: MADV_DONTNEED", which is brittle because
hintSourcePagesDontNeed() is best-effort; change the assertion to be tolerant by
checking that stderr indicates the callsite was reached rather than only the
success text — e.g., replace the strict
expect(stderr).toContain("hintSourcePagesDontNeed: MADV_DONTNEED") with a check
that stderr contains "hintSourcePagesDontNeed:" and then assert it either
includes "MADV_DONTNEED" or the failure variant (use an OR-style check or a
regex) so the test passes when the function logs success or a best-effort
failure; keep the exitCode assertion as-is.
🪄 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: b30a01e1-a0f9-456f-8f28-187c9043e39a

📥 Commits

Reviewing files that changed from the base of the PR and between be2213f and a439a0f.

📒 Files selected for processing (2)
  • src/StandaloneModuleGraph.zig
  • test/js/bun/compile/standalone-madvise-tla.test.ts

Comment thread test/js/bun/compile/standalone-madvise-tla.test.ts Outdated
Comment thread src/bun.js.zig Outdated
Comment thread src/bun.js.zig Outdated
…sertion

- Comment at the call site now says "initial synchronous evaluation is
  done (TLA may still be pending)" instead of claiming microtasks are
  drained, which is inaccurate when the entrypoint awaits.
- Skip the hint under --watch/--hot since those re-read source on every
  reload (degenerate for standalone, but cheap to be defensive).
- Test asserts the "hintSourcePagesDontNeed:" prefix so either the
  success or failure debugWarn variant proves the call site is reached.

@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 — all prior review feedback has been addressed in the latest commit.

Extended reasoning...

Overview

This PR adds StandaloneModuleGraph.hintSourcePagesDontNeed() in src/StandaloneModuleGraph.zig, calls it from src/bun.js.zig after entrypoint load, and adds a TLA-specific test. The change is scoped to standalone compiled binaries on macOS/Linux only.

Security risks

None. madvise(MADV_DONTNEED) is a kernel hint with no effect on mapping validity or protection bits. No sensitive data is exposed, no auth code is touched, and there is no window of inaccessibility. The call is purely advisory.

Level of scrutiny

Low-to-medium. This is a performance optimization that is safe by design (hint-only, file-backed pages fault back in transparently). The correctness risk is essentially zero.

Other factors

All three issues raised in prior reviews have been addressed in the latest commit: (1) the --watch/--hot guard is present, (2) the TLA comment is accurate, and (3) std.c.madvise replaces the std.posix call with a documented rationale. The test assertion is appropriately tolerant. No bugs were found by the automated system.

@cirospaciari
cirospaciari enabled auto-merge (squash) April 21, 2026 21:50
@cirospaciari
cirospaciari disabled auto-merge April 21, 2026 21:50
@cirospaciari
cirospaciari merged commit c14e37e into main Apr 21, 2026
62 of 65 checks passed
@cirospaciari
cirospaciari deleted the ciro/madvise-bun-section-only branch April 21, 2026 21:50
structwafel pushed a commit to structwafel/bun that referenced this pull request Apr 25, 2026
…entrypoint load (oven-sh#29320)

### What does this PR do?

For compiled standalone binaries (`bun build --compile`), call
`madvise(MADV_DONTNEED)` on the embedded `__BUN,__bun` (Mach-O) / `.bun`
(ELF) section once the entrypoint module has been parsed and the initial
microtasks have drained. The section holds the bundled JS source text;
after JSC has parsed it into bytecode the source pages are only re-read
for stack-trace line lookup or lazy `require()` of modules not yet
loaded, both of which fault back in transparently from the file-backed
mapping.

Does nothing in the regular `bun` interpreter (no embedded section) or
on Windows.

### Why is this safe

`MADV_DONTNEED` is a hint to the kernel — it doesn't change the mapping,
protection, or backing store. Pages remain valid and reads continue to
work whether or not the kernel reclaims them. There is no window where
the range is inaccessible, and the call cannot introduce a
SIGSEGV/SIGBUS that wouldn't have happened anyway. How aggressively the
kernel acts on the hint is platform- and pressure-dependent; the call is
correct on every POSIX target regardless.

The pages are clean and file-backed, so a later read faults them back in
from the executable on disk at page-fault cost.

### Implementation

`StandaloneModuleGraph.hintSourcePagesDontNeed()` reuses the existing
`Macho.getData()` / `ELF.getData()` section lookup, page-aligns the
range, and calls `std.posix.madvise(.., MADV.DONTNEED)`. Called from
`bun.js.zig` immediately after the post-entrypoint
`releaseWeakRefs/runGC/vm.tick()` block.

### Relationship to oven-sh#29313

This is the conservative variant — `madvise` only, on both macOS and
Linux. oven-sh#29313 additionally uses `mmap(MAP_FIXED)` overlay on macOS where
`madvise` is treated as a hint that may not immediately reclaim
file-backed pages. Both PRs are open so the tradeoff can be evaluated
independently.

### How did you verify your code works?

`zig build check-all` passes (macOS/Linux/Windows semantic analysis).
xhjkl pushed a commit to xhjkl/bun that referenced this pull request May 14, 2026
…entrypoint load (oven-sh#29320)

### What does this PR do?

For compiled standalone binaries (`bun build --compile`), call
`madvise(MADV_DONTNEED)` on the embedded `__BUN,__bun` (Mach-O) / `.bun`
(ELF) section once the entrypoint module has been parsed and the initial
microtasks have drained. The section holds the bundled JS source text;
after JSC has parsed it into bytecode the source pages are only re-read
for stack-trace line lookup or lazy `require()` of modules not yet
loaded, both of which fault back in transparently from the file-backed
mapping.

Does nothing in the regular `bun` interpreter (no embedded section) or
on Windows.

### Why is this safe

`MADV_DONTNEED` is a hint to the kernel — it doesn't change the mapping,
protection, or backing store. Pages remain valid and reads continue to
work whether or not the kernel reclaims them. There is no window where
the range is inaccessible, and the call cannot introduce a
SIGSEGV/SIGBUS that wouldn't have happened anyway. How aggressively the
kernel acts on the hint is platform- and pressure-dependent; the call is
correct on every POSIX target regardless.

The pages are clean and file-backed, so a later read faults them back in
from the executable on disk at page-fault cost.

### Implementation

`StandaloneModuleGraph.hintSourcePagesDontNeed()` reuses the existing
`Macho.getData()` / `ELF.getData()` section lookup, page-aligns the
range, and calls `std.posix.madvise(.., MADV.DONTNEED)`. Called from
`bun.js.zig` immediately after the post-entrypoint
`releaseWeakRefs/runGC/vm.tick()` block.

### Relationship to oven-sh#29313

This is the conservative variant — `madvise` only, on both macOS and
Linux. oven-sh#29313 additionally uses `mmap(MAP_FIXED)` overlay on macOS where
`madvise` is treated as a hint that may not immediately reclaim
file-backed pages. Both PRs are open so the tradeoff can be evaluated
independently.

### How did you verify your code works?

`zig build check-all` passes (macOS/Linux/Windows semantic analysis).
Jarred-Sumner pushed a commit that referenced this pull request Aug 11, 2026
…ce (#37357)

### What does this PR do?

Adds `BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE`, a runtime opt-out
for the behavior introduced in #29320: a `bun build --compile`
executable calls `madvise(MADV_DONTNEED)` on its embedded source section
once the entrypoint has loaded, so anything that touches the source
afterwards (a lazy `require()`, resolving a stack trace) pages it back
in from the executable on disk. Deployments where that re-read is
undesirable can set the flag in the environment of the running
executable to keep the pages resident:

```sh
BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE=1 ./myapp
```

The compiled binary reads it at runtime, nothing is baked in at compile
time. It follows the existing `BUN_FEATURE_FLAG_DISABLE_*` escape
hatches: registered in `env_var.rs`, checked in
`hint_source_pages_dont_need()` (which returns early, sharing the
existing `len == 0` return), falsy values such as `0` keep the default.
No effect on Windows or on the plain interpreter, which never issue the
hint. Like the other `BUN_FEATURE_FLAG_DISABLE_*` escape hatches it is
not documented.

#37356 is the alternative of removing the madvise call altogether; the
two are mutually exclusive.

### How did you verify your code works?

`test/js/bun/compile/standalone-madvise-tla.test.ts` now runs the same
compiled binary three times with `BUN_DEBUG_StandaloneModuleGraph=1`:
with the variable unset (explicitly removed from the child env so an
ambient value on the host cannot leak in), set to `0`, and set to `1`.
The first two must log a `hintSourcePagesDontNeed:` line, the last must
not log one at all, since the flag path returns before logging anything.
The test stays debug-only because it relies on the scoped logger.

Against a debug build without this change the `1` case fails as
expected:

```
Expected to not contain: "hintSourcePagesDontNeed:"
Received: "before-await\nafter-await\n[standalonemodulegraph] hintSourcePagesDontNeed: MADV_DONTNEED 4096 bytes\n"
```

With the change, `bun bd test
test/js/bun/compile/standalone-madvise-tla.test.ts` passes (22
assertions).

<!-- robobun:evidence:begin -->

---

**no test proof** · iteration 1 · Platform-specific test(s) that do not
run on this machine. Deferring to CI, which covers all platforms:
test/js/bun/compile/standalone-madvise-tla.test.ts

<!-- robobun:evidence:end -->

---------

Co-authored-by: robobun <117481402+robobun@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants