standalone: do not page the module graph out at idle
The second idle full collection (75 s after the heap stopped growing) also asks the kernel to reclaim the pages of a standalone
executable's embedded module graph (MADV_PAGEOUT, on a helper thread). This removes that: StandaloneModuleGraph::page_out, the
trait method, and the call in GarbageCollectionController::idle_tick. The idle collections themselves are unchanged.
Why
Those pages are clean and file-backed. They are not ours to evict: the kernel drops them by itself, at no cost, as soon as it
needs the memory, and until then they are a cache that makes the program's next action fast. Paging them out by hand only lowers
the process's RSS column, and the next thing the program does reads them back from the disk, one major fault at a time.
Deployments that run such executables in hosted sandboxes already set BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE=1 to turn it
off.
Measured
A large bundled CLI application as a standalone executable (release builds of main; 20 requests against a stub API, a pause, one
more request; every run on its own copy of the executable because MADV_PAGEOUT skips pages another process maps; faults from
/proc/<pid>/stat, instructions from perf stat; medians of 2-4 runs on a busy machine). A request in steady state is 224 ms,
0.9 G instructions and 4 major faults.
| the request after a pause of | wall | instructions | major faults | |
|---|---|---|---|---|
| 30 s (first idle collection only) | main | +19 ms | 0 | 3 |
| 90 s (second idle collection + page-out) | main | +600 ms | +1.55 G | 262 |
| 90 s | main, code aging off (BUN_JSC_forceCodeBlockLiveness=1): the page-out alone | +291 ms | +0.05 G | 217 |
| 90 s | main with BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE=1: the collection alone | +339 ms | +1.98 G | 0 |
| 3 min | main / page-out alone / collection alone | +671 / +241 / +216 ms | +2.5 / +0.1 / +2.5 G | 262 / 210 / 0 |
So the page-out costs the next request +240 to +290 ms and 210-220 major faults, for 57 MB of file-backed RSS (104 -> 46 MB;
anonymous memory is what the collection frees, 55 MB at that point, and is not affected by this change). The very first request
of a session that sat at its first prompt for 10 minutes: 1402 ms and 402 major faults with the page-out, 774 ms and 40 without.
What stays
BUN_FEATURE_FLAG_DISABLE_STANDALONE_MADVISE stays, because two other hints read it, both one-off and at start-up, both unchanged:
the read-ahead of the module graph's start-up pages (MADV_WILLNEED / F_RDADVISE, which overlaps the disk reads with JSC's
initialisation) and the MADV_DONTNEED hint for the embedded source text once the entry point has been evaluated. With this
change that flag gates only those two.
Test
gc-controller-cadence.test.ts: a standalone executable reads a 3 MB embedded file (so the module graph is resident), then sits
idle past both collections of BUN_IDLE_GC_SECONDS="1,1"; the mapping that holds the module graph keeps its pages (it is the test
that looks, at /proc/<pid>/smaps; Linux release lanes, disk-backed temp dir). A run in which the pages went is repeated up to
twice: the kernel takes freshly read pages now and then when memory is short, a page-out takes them every time. It fails on main
(752 KB left of 3444). The file: 13 tests, 9 s (the new one runs alongside the others). standalone-madvise-tla.test.ts unchanged. clippy on the three crates; cargo check for x86_64-pc-windows-msvc and aarch64-apple-darwin.