fix(runtime): keepalive anchors as #[used(compiler)] — stop pinning the whole runtime into every binary (−15% typical, −21.7% auto-optimized) - #10382
Conversation
Plain #[used] on Mach-O sets N_NO_DEAD_STRIP, making every KEEP_* anchor an unconditional ld64 root. With codegen-units = 1 perry-runtime is a single archive member, so any runtime reference pulls all anchors and keeps the whole runtime surface alive. #[used(compiler)] keeps the anchor (and its target) in the archive for rustc/LTO without making it a linker root. Left as linker roots: mimalloc __mod_init_func, OHOS .init_array, build stamp. Local experiment branch; not for merge as-is.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (113)
Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review. 📝 WalkthroughWalkthroughThe pull request enables ChangesKeepalive anchor migration
Priority: ➖ Normal Estimated code review effort: 3 (Moderate) | ~25 minutes Change: Bug fix Merge Risk: ⚪ Minimal · up to The reported missing feature gates are already present, so no actionable regression remains from this change. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Landed via merge train #10401 (v0.5.1587). All source commits preserve authorship; merged main matches the validated train exactly. |
Problem
The ~490 keepalive anchor statics are declared with plain
#[used]. On Mach-O that setsN_NO_DEAD_STRIP, and on ELF it puts the static in a section flaggedSHF_GNU_RETAIN— i.e. every anchor is an unconditional linker root on both platforms.[profile.release]setscodegen-units = 1, soperry-runtimecompiles to one 17.4 MB archive member holding all 504KEEP_*statics. Because an.amember is pulled in whole, referencing any runtime symbol (e.g.console.log) drags in all of them, and each one roots whatever it points at.Cargo.tomlandoptimized_libs/freshness.rsboth argue the cost is bounded:That holds for a multi-object archive. Under
codegen-units = 1there is only one object, so the bound degrades to "the entire runtime surface". Measured withld64 -why_live, aconsole.log("yeah")program retainsfs,child_process,dgram,tls,node_vm,bun_ffi, yoga/taffy and more, via:Fix
Declare the anchors
#[used(compiler)]instead. This keeps the static (and its target) through rustc/LLVM — which is the reason the anchors exist, per #6917's revert — while leaving it strippable by the linker, so the program's own undefined references decide what survives.Deliberately left as plain
#[used]:PERRY_RUNTIME_BUILD_STAMP_EMBEDDED, mimalloc's__DATA,__mod_init_funcentry, and OHOS's.init_arrayentry.perry-ext-http'sFORCE_LINK_HTTP_SERVER(#1652) is untouched — it is intentional linker retention.The archive stays complete
This is the failure #6917 was reverted for, so it is checked directly rather than argued:
libperry_runtime.aKEEP_*statics present in the object[no dead strip]rootsPrograms that call the symbols named in those comments link and run:
js_box_release,js_bool_box_release,js_i32_box_release,js_closure_set_box_capture_ptr,js_link_path_module_parent(CJSrequire),js_module_dynamic_import_deferred(computedawait import(spec)).Measurements
13 programs on macOS/arm64, 12 on Linux/x86_64; stdout, stderr and exit code byte-identical to baseline in every case, provenance checked from each binary's embedded build stamp.
console.log, classes, closures, async, errors, generators, ESMevents/http/net/zlib)import()/ CJSrequirerich.tswith auto-optimize (the shipping path)perrycompiler binary itself (Linux)Linux is within a few tenths of a percent of macOS on every row.
Tests
perry --bin perry: 1129 passed, 0 failed — includesauto_optimize_always_includes_keepalive_anchors,auto_optimize_keepalive_anchors_not_bitcode_only,ext_crates_bundle_a_full_featured_perry_runtime.perry-runtime --lib/perry-stdlib --lib, single-threaded on both arms: identical results — 3952 passed and the same single pre-existing failure,gc::tests::heap_generation::a_free_or_move_outside_every_scope_is_caught_in_debug_builds, which fails the same way on unmodifiedfcd108bfb0. The extra failures seen in parallel runs are flaky and differ in membership between arms (baseline failscached_reads_preserve_identityandbuffered_stdin_is_delivered, which this branch passes).assert_lto_keepalive_anchormatches the literal text#[used], so it is widened to accept#[used(compiler)].Risks / not covered
#![feature(used_with_arg)]toperry-runtimeandperry-stdlib(both crates already require the pinned nightly).#[no_mangle]function in its staticlib, which suggests the anchors may no longer be needed at all. Not tested against perry itself, so this PR makes the conservative change.Summary by CodeRabbit