Skip to content

fix(runtime): keepalive anchors as #[used(compiler)] — stop pinning the whole runtime into every binary (−15% typical, −21.7% auto-optimized) - #10382

Closed
proggeramlug wants to merge 1 commit into
PerryTS:mainfrom
proggeramlug:exp/used-compiler-anchors
Closed

proggeramlug wants to merge 1 commit into
PerryTS:mainfrom
proggeramlug:exp/used-compiler-anchors

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Problem

The ~490 keepalive anchor statics are declared with plain #[used]. On Mach-O that sets N_NO_DEAD_STRIP, and on ELF it puts the static in a section flagged SHF_GNU_RETAIN — i.e. every anchor is an unconditional linker root on both platforms.

[profile.release] sets codegen-units = 1, so perry-runtime compiles to one 17.4 MB archive member holding all 504 KEEP_* statics. Because an .a member 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.toml and optimized_libs/freshness.rs both argue the cost is bounded:

In a staticlib archive (.a) the linker only pulls in object files that resolve an undefined reference, so #[used] anchors only become -dead_strip roots when their object file is pulled in […] The size regression is therefore limited to the transitive callees of symbols the program uses (which would be kept anyway), not the entire runtime surface.

That holds for a multi-object archive. Under codegen-units = 1 there is only one object, so the bound degrades to "the entire runtime surface". Measured with ld64 -why_live, a console.log("yeah") program retains fs, child_process, dgram, tls, node_vm, bun_ffi, yoga/taffy and more, via:

_js_child_process_exec_sync
 ← object::native_module_dispatch::dispatch_a_c::nm_dispatch_child_process
  ← _js_nm_install_all
   ← module_require::dynamic_import_fallback_promise
    ← _js_module_dynamic_import_deferred
     ← module_require::KEEP_JS_MODULE_DYNAMIC_IMPORT_DEFERRED   (anchor = root)

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_func entry, and OHOS's .init_array entry. perry-ext-http's FORCE_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:

baseline this PR
externally-defined symbols in libperry_runtime.a 5611 5611 (0 missing, 0 added)
KEEP_* statics present in the object 504 504
[no dead strip] roots 553 2

Programs 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 (CJS require), js_module_dynamic_import_deferred (computed await 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.

program baseline this PR
console.log, classes, closures, async, errors, generators, ESM ~14.7 MB ~12.5 MB −13…−15%
ext-routed (events/http/net/zlib) 29.77 MB 27.60 MB −7.3%
dynamic import() / CJS require ~14.8 MB ~14.3 MB −3.3% (these genuinely need the module surface)
rich.ts with auto-optimize (the shipping path) 9.86 MB 7.72 MB −21.7%
the perry compiler binary itself (Linux) 100 MB 87 MB it links the runtime too

Linux is within a few tenths of a percent of macOS on every row.

Tests

  • perry --bin perry: 1129 passed, 0 failed — includes auto_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 unmodified fcd108bfb0. The extra failures seen in parallel runs are flaky and differ in membership between arms (baseline fails cached_reads_preserve_identity and buffered_stdin_is_delivered, which this branch passes).
  • assert_lto_keepalive_anchor matches the literal text #[used], so it is widened to accept #[used(compiler)].

Risks / not covered

  • Windows/COFF is untested. Only macOS arm64 and Linux x86_64 were measured. Worth a CI check before merge.
  • Adds #![feature(used_with_arg)] to perry-runtime and perry-stdlib (both crates already require the pinned nightly).
  • A follow-up worth exploring: a scratch crate on this toolchain kept an unanchored #[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

  • Build and Runtime Reliability
    • Updated internal symbol-retention handling across runtime and standard-library components to improve preservation of compiler-generated functionality during optimized builds.
    • Standardized retention behavior for array, object, promise, stream, buffer, worker, database, compression, and other native integrations.
    • No user-facing API, runtime logic, or feature behavior changes.

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

coderabbitai Bot commented Sep 16, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 9e7963e2-3fbe-42fb-b793-49bd82350386

📥 Commits

Reviewing files that changed from the base of the PR and between 33690c5 and d77cef8.

📒 Files selected for processing (113)
  • crates/perry-runtime/src/array/element_shape.rs
  • crates/perry-runtime/src/array/fill_extend.rs
  • crates/perry-runtime/src/array/from_concat.rs
  • crates/perry-runtime/src/array/generic.rs
  • crates/perry-runtime/src/array/generic_object.rs
  • crates/perry-runtime/src/array/header.rs
  • crates/perry-runtime/src/array/immutable.rs
  • crates/perry-runtime/src/array/indexing.rs
  • crates/perry-runtime/src/array/iter_methods.rs
  • crates/perry-runtime/src/array/join.rs
  • crates/perry-runtime/src/array/numeric_range.rs
  • crates/perry-runtime/src/array/push_pop.rs
  • crates/perry-runtime/src/array/sort.rs
  • crates/perry-runtime/src/array/subclass_loop_guard.rs
  • crates/perry-runtime/src/array/subclass_packed_index.rs
  • crates/perry-runtime/src/bigint/convert.rs
  • crates/perry-runtime/src/box.rs
  • crates/perry-runtime/src/buffer/access.rs
  • crates/perry-runtime/src/buffer/dataview.rs
  • crates/perry-runtime/src/buffer/encode.rs
  • crates/perry-runtime/src/buffer/query.rs
  • crates/perry-runtime/src/buffer/u8_codec.rs
  • crates/perry-runtime/src/builtins/arithmetic.rs
  • crates/perry-runtime/src/bun_compat/jsc.rs
  • crates/perry-runtime/src/child_process/validate.rs
  • crates/perry-runtime/src/closure/alloc.rs
  • crates/perry-runtime/src/closure/dispatch/bound.rs
  • crates/perry-runtime/src/closure/registry.rs
  • crates/perry-runtime/src/closure/unbox.rs
  • crates/perry-runtime/src/collection_iter_object.rs
  • crates/perry-runtime/src/date.rs
  • crates/perry-runtime/src/disposable.rs
  • crates/perry-runtime/src/eh.rs
  • crates/perry-runtime/src/embedded.rs
  • crates/perry-runtime/src/embedded/compressed.rs
  • crates/perry-runtime/src/error.rs
  • crates/perry-runtime/src/error_subclass_stack.rs
  • crates/perry-runtime/src/event_target.rs
  • crates/perry-runtime/src/fs/validate.rs
  • crates/perry-runtime/src/gc/barrier/mod.rs
  • crates/perry-runtime/src/gc/layout/typed_shape.rs
  • crates/perry-runtime/src/intl/segments_view.rs
  • crates/perry-runtime/src/iterator_helpers.rs
  • crates/perry-runtime/src/json/parse_api.rs
  • crates/perry-runtime/src/json/raw_json.rs
  • crates/perry-runtime/src/lib.rs
  • crates/perry-runtime/src/map.rs
  • crates/perry-runtime/src/module_require.rs
  • crates/perry-runtime/src/module_require/import_meta_resolve.rs
  • crates/perry-runtime/src/native_abi.rs
  • crates/perry-runtime/src/node_sea.rs
  • crates/perry-runtime/src/node_stream_keepalive.rs
  • crates/perry-runtime/src/node_submodules/zlib.rs
  • crates/perry-runtime/src/node_v8.rs
  • crates/perry-runtime/src/object/alloc.rs
  • crates/perry-runtime/src/object/class_constructors.rs
  • crates/perry-runtime/src/object/class_meta_registry.rs
  • crates/perry-runtime/src/object/class_registry/construct/class_return.rs
  • crates/perry-runtime/src/object/class_registry/registration.rs
  • crates/perry-runtime/src/object/descriptors.rs
  • crates/perry-runtime/src/object/global_this/builtin_thunks.rs
  • crates/perry-runtime/src/object/global_this/fetch_globals.rs
  • crates/perry-runtime/src/object/groupby.rs
  • crates/perry-runtime/src/object/native_module.rs
  • crates/perry-runtime/src/object/native_this_alias.rs
  • crates/perry-runtime/src/object/object_ops/has_own.rs
  • crates/perry-runtime/src/object/shapes.rs
  • crates/perry-runtime/src/object/this_binding.rs
  • crates/perry-runtime/src/object/with_env.rs
  • crates/perry-runtime/src/os.rs
  • crates/perry-runtime/src/os/os_process_emitter.rs
  • crates/perry-runtime/src/param_type_guard.rs
  • crates/perry-runtime/src/path.rs
  • crates/perry-runtime/src/path/value_args.rs
  • crates/perry-runtime/src/process/env_misc.rs
  • crates/perry-runtime/src/promise/async_step.rs
  • crates/perry-runtime/src/promise/combinators.rs
  • crates/perry-runtime/src/promise/microtasks.rs
  • crates/perry-runtime/src/promise/mod.rs
  • crates/perry-runtime/src/promise/rejection.rs
  • crates/perry-runtime/src/proxy.rs
  • crates/perry-runtime/src/proxy/put_value.rs
  • crates/perry-runtime/src/regex/escape.rs
  • crates/perry-runtime/src/set.rs
  • crates/perry-runtime/src/string/locale.rs
  • crates/perry-runtime/src/string/pad.rs
  • crates/perry-runtime/src/string/raw.rs
  • crates/perry-runtime/src/string/slice_ops.rs
  • crates/perry-runtime/src/string/suffix_cursor.rs
  • crates/perry-runtime/src/symbol/constructors.rs
  • crates/perry-runtime/src/symbol/iterator.rs
  • crates/perry-runtime/src/text.rs
  • crates/perry-runtime/src/tls.rs
  • crates/perry-runtime/src/typed_feedback.rs
  • crates/perry-runtime/src/typed_feedback/guards.rs
  • crates/perry-runtime/src/typed_feedback/tests.rs
  • crates/perry-runtime/src/typed_feedback/trace.rs
  • crates/perry-runtime/src/typedarray/access.rs
  • crates/perry-runtime/src/typedarray_props.rs
  • crates/perry-runtime/src/url/abort.rs
  • crates/perry-runtime/src/url/search_params.rs
  • crates/perry-runtime/src/validators.rs
  • crates/perry-runtime/src/value/dyn_index.rs
  • crates/perry-runtime/src/value/dynamic_arith.rs
  • crates/perry-runtime/src/value/nanbox.rs
  • crates/perry-runtime/src/yoga.rs
  • crates/perry-stdlib/src/events/constructors.rs
  • crates/perry-stdlib/src/lib.rs
  • crates/perry-stdlib/src/sqlite/better.rs
  • crates/perry-stdlib/src/tls.rs
  • crates/perry-stdlib/src/worker_threads.rs
  • crates/perry-stdlib/src/worker_threads/direct_message.rs
  • crates/perry-stdlib/src/zlib.rs

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


📝 Walkthrough

Walkthrough

The pull request enables #[used(compiler)] and updates keepalive anchors across perry-runtime and perry-stdlib. Several previously ungated anchors also gain the keepalive-anchors feature gate. One typed-feedback test accepts both attribute forms.

Changes

Keepalive anchor migration

Layer / File(s) Summary
Compiler support and runtime anchor migration
crates/perry-runtime/src/array/*, crates/perry-runtime/src/buffer/*, crates/perry-runtime/src/closure/*, crates/perry-runtime/src/error*, crates/perry-runtime/src/lib.rs
The runtime enables used_with_arg and changes many keepalive attributes from #[used] to #[used(compiler)].
Remaining runtime anchors and validation
crates/perry-runtime/src/map.rs, crates/perry-runtime/src/object/*, crates/perry-runtime/src/promise/*, crates/perry-runtime/src/typed_feedback/*, crates/perry-runtime/src/value/*, crates/perry-runtime/src/yoga.rs
Additional runtime anchors use compiler-scoped retention. Selected anchors gain or retain the keepalive-anchors feature gate. The typed-feedback test accepts either retention attribute.
Standard-library anchor migration
crates/perry-stdlib/src/events/*, crates/perry-stdlib/src/lib.rs, crates/perry-stdlib/src/sqlite/*, crates/perry-stdlib/src/tls.rs, crates/perry-stdlib/src/worker_threads*, crates/perry-stdlib/src/zlib.rs
The standard library enables used_with_arg and updates its keepalive anchors to #[used(compiler)].

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to d77ce

The reported missing feature gates are already present, so no actionable regression remains from this change.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: replacing keepalive anchors with #[used(compiler)] to reduce unnecessary runtime retention and binary size.
Description check ✅ Passed The description is detailed, relevant, and covers the problem, fix, preserved linker roots, measurements, tests, and risks. It does not use the template headings exactly and omits explicit Related iss…
Docstring Coverage ✅ Passed Docstring coverage is 93.75% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 32 functions across 50 files. (63 skipped: …
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

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

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Landed via merge train #10401 (v0.5.1587). All source commits preserve authorship; merged main matches the validated train exactly.

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.

1 participant