From 580720f6d3f4a605830b73dc094f82417ddb496d Mon Sep 17 00:00:00 2001 From: Ciro Spaciari MacBook Date: Mon, 10 Aug 2026 17:39:28 -0700 Subject: [PATCH 1/4] standalone: add BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE to keep embedded source resident Since #29320, a compiled executable calls madvise(MADV_DONTNEED) on its embedded source section once the entrypoint has loaded, so the pages are re-read from the executable on disk the next time something touches them (a lazy require(), resolving a stack trace). Give deployments where that re-read is undesirable a way out: setting the flag in the environment of the running executable skips the hint. It is read at runtime by the compiled binary, not at `bun build --compile` time, and follows the existing BUN_FEATURE_FLAG_DISABLE_* escape hatches (checked inside the implementing crate, falsy values keep the default). --- docs/bundler/executables.mdx | 12 +++++++ src/bun_core/env_var.rs | 4 +++ src/standalone_graph/StandaloneModuleGraph.rs | 14 +++++++- .../compile/standalone-madvise-tla.test.ts | 36 +++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) diff --git a/docs/bundler/executables.mdx b/docs/bundler/executables.mdx index 77cb6a6ba3c4..de2e7fe91507 100644 --- a/docs/bundler/executables.mdx +++ b/docs/bundler/executables.mdx @@ -398,6 +398,18 @@ BUN_OPTIONS="--heap-prof-md" ./myapp BUN_OPTIONS="--smol --cpu-prof-md" ./myapp ``` +### Keeping the embedded source resident + +Once a standalone executable has loaded its entrypoint, Bun tells the kernel (via `madvise`) that the pages holding the embedded JavaScript source are no longer needed, which lowers the executable's resident memory. The pages are file-backed, so anything that reads the source later (a lazy `require()`, or resolving a stack trace) transparently pages it back in from the executable on disk. + +If that re-read is a problem for your deployment (for example, the executable runs from slow or removable storage), set `BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE` when running the executable to keep the source pages resident: + +```bash terminal icon="terminal" +BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE=1 ./myapp +``` + +This is a runtime setting read by the compiled executable — it does not need to be set when running `bun build --compile`. It has no effect on Windows or when running scripts with `bun` directly, since neither releases source pages. + --- ## Automatic config loading diff --git a/src/bun_core/env_var.rs b/src/bun_core/env_var.rs index 9ca6d375d233..4c09b2c65718 100644 --- a/src/bun_core/env_var.rs +++ b/src/bun_core/env_var.rs @@ -252,6 +252,10 @@ pub mod feature_flag { new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_SOURCE_MAPS, "BUN_FEATURE_FLAG_DISABLE_SOURCE_MAPS", {}); new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_SPAWNSYNC_FAST_PATH, "BUN_FEATURE_FLAG_DISABLE_SPAWNSYNC_FAST_PATH", {}); new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_SQL_AUTO_PIPELINING, "BUN_FEATURE_FLAG_DISABLE_SQL_AUTO_PIPELINING", {}); + // Keep the embedded source section of a `bun build --compile` binary + // resident: skips the post-entrypoint madvise(MADV_DONTNEED) in + // bun_standalone_graph::Graph::hint_source_pages_dont_need. + new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE, "BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE", {}); new_feature_flag!(pub BUN_DISABLE_TRANSPILED_SOURCE_CODE_PREVIEW, "BUN_DISABLE_TRANSPILED_SOURCE_CODE_PREVIEW", {}); new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_UV_FS_COPYFILE, "BUN_FEATURE_FLAG_DISABLE_UV_FS_COPYFILE", {}); new_feature_flag!(pub BUN_DUMP_STATE_ON_CRASH, "BUN_DUMP_STATE_ON_CRASH", {}); diff --git a/src/standalone_graph/StandaloneModuleGraph.rs b/src/standalone_graph/StandaloneModuleGraph.rs index 6f427e1e48da..510048735dbc 100644 --- a/src/standalone_graph/StandaloneModuleGraph.rs +++ b/src/standalone_graph/StandaloneModuleGraph.rs @@ -2193,7 +2193,8 @@ impl StandaloneModuleGraph { /// The pages are clean file-backed COW, so any later read (lazy require, /// stack-trace source lookup) faults back in transparently from the /// executable on disk. Only applies when running as a compiled - /// standalone binary. + /// standalone binary; `BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE=1` + /// skips the hint. pub fn hint_source_pages_dont_need() { #[cfg(windows)] { @@ -2229,6 +2230,17 @@ impl StandaloneModuleGraph { return; } + if bun_core::env_var::feature_flag::BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE + .get() + .unwrap_or(false) + { + bun_core::scoped_log!( + StandaloneModuleGraph, + "hintSourcePagesDontNeed: skipped (BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE)" + ); + return; + } + let page: usize = bun_alloc::page_size(); let start = (base as usize) & !(page - 1); let end_unaligned = base as usize + len; diff --git a/test/js/bun/compile/standalone-madvise-tla.test.ts b/test/js/bun/compile/standalone-madvise-tla.test.ts index c3f499286dc4..cf6328ed67c3 100644 --- a/test/js/bun/compile/standalone-madvise-tla.test.ts +++ b/test/js/bun/compile/standalone-madvise-tla.test.ts @@ -2,6 +2,8 @@ // entrypoint has top-level await. loadEntryPoint() returns a promise without // blocking, so the call site at bun.js.zig:466 is hit synchronously before the // main event loop spins — TLA resolution happens later in that loop. +// BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE, read by the compiled binary at +// runtime, skips the hint. import { expect, test } from "bun:test"; import { bunEnv, bunExe, isDebug, isWindows, tempDir } from "harness"; import path from "node:path"; @@ -43,6 +45,40 @@ test.skipIf(isWindows || !isDebug)( // Scoped loggers write to the debug-writer stream (stdout by default). // Either the success or failure variant proves the call site is reached. expect(stdout).toContain("hintSourcePagesDontNeed:"); + expect(stdout).not.toContain("hintSourcePagesDontNeed: skipped"); + expect(stderr).toBe(""); + expect(exitCode).toBe(0); + } + + // BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE is read by the compiled + // executable at runtime (the binary above was built without it), and a + // falsy value leaves the hint enabled. + for (const [value, skipped] of [ + ["1", true], + ["0", false], + ] as const) { + await using proc = Bun.spawn({ + cmd: [out], + env: { + ...bunEnv, + BUN_DEBUG_StandaloneModuleGraph: "1", + BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE: value, + }, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stdout).toContain("before-await"); + expect(stdout).toContain("after-await"); + if (skipped) { + expect(stdout).toContain("hintSourcePagesDontNeed: skipped (BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE)"); + expect(stdout).not.toContain("hintSourcePagesDontNeed: MADV_DONTNEED"); + expect(stdout).not.toContain("hintSourcePagesDontNeed: madvise failed"); + } else { + expect(stdout).toContain("hintSourcePagesDontNeed:"); + expect(stdout).not.toContain("hintSourcePagesDontNeed: skipped"); + } expect(stderr).toBe(""); expect(exitCode).toBe(0); } From 0f2670566a3a8ec8a085b6c95e4efd83b05a339f Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:27:42 +0000 Subject: [PATCH 2/4] standalone: drop the comment on the madvise flag and the skipped log The flag check now returns silently alongside the len == 0 check, so the test asserts that no hintSourcePagesDontNeed line is logged when the flag is set. The unset baseline clears the variable explicitly so an ambient value on the host cannot leak into it. Docs name the platforms where the hint is issued. --- docs/bundler/executables.mdx | 4 +- src/bun_core/env_var.rs | 3 -- src/standalone_graph/StandaloneModuleGraph.rs | 15 ++----- .../compile/standalone-madvise-tla.test.ts | 44 ++++++------------- 4 files changed, 19 insertions(+), 47 deletions(-) diff --git a/docs/bundler/executables.mdx b/docs/bundler/executables.mdx index de2e7fe91507..fb45a0b283bc 100644 --- a/docs/bundler/executables.mdx +++ b/docs/bundler/executables.mdx @@ -400,7 +400,7 @@ BUN_OPTIONS="--smol --cpu-prof-md" ./myapp ### Keeping the embedded source resident -Once a standalone executable has loaded its entrypoint, Bun tells the kernel (via `madvise`) that the pages holding the embedded JavaScript source are no longer needed, which lowers the executable's resident memory. The pages are file-backed, so anything that reads the source later (a lazy `require()`, or resolving a stack trace) transparently pages it back in from the executable on disk. +On Linux and macOS, once a standalone executable has loaded its entrypoint, Bun tells the kernel (via `madvise`) that the pages holding the embedded JavaScript source are no longer needed, which lowers the executable's resident memory. The pages are file-backed, so anything that reads the source later (a lazy `require()`, or resolving a stack trace) transparently pages it back in from the executable on disk. If that re-read is a problem for your deployment (for example, the executable runs from slow or removable storage), set `BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE` when running the executable to keep the source pages resident: @@ -408,7 +408,7 @@ If that re-read is a problem for your deployment (for example, the executable ru BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE=1 ./myapp ``` -This is a runtime setting read by the compiled executable — it does not need to be set when running `bun build --compile`. It has no effect on Windows or when running scripts with `bun` directly, since neither releases source pages. +This is a runtime setting read by the compiled executable, so it does not need to be set when running `bun build --compile`. It has no effect on other platforms, or when running scripts with `bun` directly, because the source pages are never released there in the first place. --- diff --git a/src/bun_core/env_var.rs b/src/bun_core/env_var.rs index 4c09b2c65718..eb14e27b6a99 100644 --- a/src/bun_core/env_var.rs +++ b/src/bun_core/env_var.rs @@ -252,9 +252,6 @@ pub mod feature_flag { new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_SOURCE_MAPS, "BUN_FEATURE_FLAG_DISABLE_SOURCE_MAPS", {}); new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_SPAWNSYNC_FAST_PATH, "BUN_FEATURE_FLAG_DISABLE_SPAWNSYNC_FAST_PATH", {}); new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_SQL_AUTO_PIPELINING, "BUN_FEATURE_FLAG_DISABLE_SQL_AUTO_PIPELINING", {}); - // Keep the embedded source section of a `bun build --compile` binary - // resident: skips the post-entrypoint madvise(MADV_DONTNEED) in - // bun_standalone_graph::Graph::hint_source_pages_dont_need. new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE, "BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE", {}); new_feature_flag!(pub BUN_DISABLE_TRANSPILED_SOURCE_CODE_PREVIEW, "BUN_DISABLE_TRANSPILED_SOURCE_CODE_PREVIEW", {}); new_feature_flag!(pub BUN_FEATURE_FLAG_DISABLE_UV_FS_COPYFILE, "BUN_FEATURE_FLAG_DISABLE_UV_FS_COPYFILE", {}); diff --git a/src/standalone_graph/StandaloneModuleGraph.rs b/src/standalone_graph/StandaloneModuleGraph.rs index 510048735dbc..20e2851fcfa8 100644 --- a/src/standalone_graph/StandaloneModuleGraph.rs +++ b/src/standalone_graph/StandaloneModuleGraph.rs @@ -2226,18 +2226,11 @@ impl StandaloneModuleGraph { #[cfg(any(target_os = "macos", target_os = "linux", target_os = "android"))] { - if len == 0 { - return; - } - - if bun_core::env_var::feature_flag::BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE - .get() - .unwrap_or(false) + if len == 0 + || bun_core::env_var::feature_flag::BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE + .get() + .unwrap_or(false) { - bun_core::scoped_log!( - StandaloneModuleGraph, - "hintSourcePagesDontNeed: skipped (BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE)" - ); return; } diff --git a/test/js/bun/compile/standalone-madvise-tla.test.ts b/test/js/bun/compile/standalone-madvise-tla.test.ts index cf6328ed67c3..fe3cfd8017b5 100644 --- a/test/js/bun/compile/standalone-madvise-tla.test.ts +++ b/test/js/bun/compile/standalone-madvise-tla.test.ts @@ -31,38 +31,21 @@ test.skipIf(isWindows || !isDebug)( expect(build.stderr.toString()).not.toContain("error:"); expect(build.exitCode).toBe(0); - { - await using proc = Bun.spawn({ - cmd: [out], - env: { ...bunEnv, BUN_DEBUG_StandaloneModuleGraph: "1" }, - stdout: "pipe", - stderr: "pipe", - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - - expect(stdout).toContain("before-await"); - expect(stdout).toContain("after-await"); - // Scoped loggers write to the debug-writer stream (stdout by default). - // Either the success or failure variant proves the call site is reached. - expect(stdout).toContain("hintSourcePagesDontNeed:"); - expect(stdout).not.toContain("hintSourcePagesDontNeed: skipped"); - expect(stderr).toBe(""); - expect(exitCode).toBe(0); - } - // BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE is read by the compiled - // executable at runtime (the binary above was built without it), and a - // falsy value leaves the hint enabled. - for (const [value, skipped] of [ - ["1", true], - ["0", false], + // executable at runtime (the binary above was built without it); a falsy + // value leaves the hint enabled. With the flag set, the function returns + // before logging anything, so the only evidence is the missing line. + for (const [flag, hinted] of [ + [undefined, true], + ["0", true], + ["1", false], ] as const) { await using proc = Bun.spawn({ cmd: [out], env: { ...bunEnv, BUN_DEBUG_StandaloneModuleGraph: "1", - BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE: value, + BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE: flag, }, stdout: "pipe", stderr: "pipe", @@ -71,13 +54,12 @@ test.skipIf(isWindows || !isDebug)( expect(stdout).toContain("before-await"); expect(stdout).toContain("after-await"); - if (skipped) { - expect(stdout).toContain("hintSourcePagesDontNeed: skipped (BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE)"); - expect(stdout).not.toContain("hintSourcePagesDontNeed: MADV_DONTNEED"); - expect(stdout).not.toContain("hintSourcePagesDontNeed: madvise failed"); - } else { + // Scoped loggers write to the debug-writer stream (stdout by default). + // Either the success or failure variant proves the call site is reached. + if (hinted) { expect(stdout).toContain("hintSourcePagesDontNeed:"); - expect(stdout).not.toContain("hintSourcePagesDontNeed: skipped"); + } else { + expect(stdout).not.toContain("hintSourcePagesDontNeed:"); } expect(stderr).toBe(""); expect(exitCode).toBe(0); From 4840788bde8886ee6edcb36ece31a94930db2e60 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:33:02 +0000 Subject: [PATCH 3/4] standalone: drop the docs section for BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE --- docs/bundler/executables.mdx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/docs/bundler/executables.mdx b/docs/bundler/executables.mdx index fb45a0b283bc..77cb6a6ba3c4 100644 --- a/docs/bundler/executables.mdx +++ b/docs/bundler/executables.mdx @@ -398,18 +398,6 @@ BUN_OPTIONS="--heap-prof-md" ./myapp BUN_OPTIONS="--smol --cpu-prof-md" ./myapp ``` -### Keeping the embedded source resident - -On Linux and macOS, once a standalone executable has loaded its entrypoint, Bun tells the kernel (via `madvise`) that the pages holding the embedded JavaScript source are no longer needed, which lowers the executable's resident memory. The pages are file-backed, so anything that reads the source later (a lazy `require()`, or resolving a stack trace) transparently pages it back in from the executable on disk. - -If that re-read is a problem for your deployment (for example, the executable runs from slow or removable storage), set `BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE` when running the executable to keep the source pages resident: - -```bash terminal icon="terminal" -BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE=1 ./myapp -``` - -This is a runtime setting read by the compiled executable, so it does not need to be set when running `bun build --compile`. It has no effect on other platforms, or when running scripts with `bun` directly, because the source pages are never released there in the first place. - --- ## Automatic config loading From bd538c14c3f0d2b609cf623cebc2b6b73a49da80 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:53:52 +0000 Subject: [PATCH 4/4] ci: retrigger