diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml index d5d9aa3645..01183f480a 100644 --- a/.github/workflows/release-packages.yml +++ b/.github/workflows/release-packages.yml @@ -50,6 +50,14 @@ concurrency: group: release-packages-${{ github.event.release.tag_name || inputs.existing_tag || (inputs.cut_release && inputs.candidate_sha) || github.run_id }} cancel-in-progress: false +# Shipped runtime archives carry no GC/diagnostic instruments: perry-runtime's +# build script compiles them out when this is set (see its +# `emit_instrument_cfgs`). Every cargo build in these jobs sees it, including +# the per-ext-crate invocations that rebuild the runtime, so all shipped +# archives share one feature set (#6303). The container builds pass it through. +env: + PERRY_RELEASE_STRIP_INSTRUMENTS: "1" + jobs: # --------------------------------------------------------------------------- # Resolve the run mode + release tag ONCE, up front. Every downstream job @@ -764,6 +772,7 @@ jobs: --env CARGO_TARGET_DIR=/work/target-glibc231 \ --env PERRY_ABORT_TARGET_DIR=/work/target-glibc231-abort \ --env PERRY_CLI_UPDATE_PUBLIC_KEYS \ + --env PERRY_RELEASE_STRIP_INSTRUMENTS \ --volume "$HOME/.cargo:/tmp/cargo-home" \ --volume "$HOME/.rustup:/tmp/rustup-home" \ --volume "$GITHUB_WORKSPACE:/work" \ @@ -810,6 +819,7 @@ jobs: --env CARGO_TARGET_DIR=/work/target-musl \ --env PERRY_ABORT_TARGET_DIR=/work/target-musl-abort \ --env PERRY_CLI_UPDATE_PUBLIC_KEYS \ + --env PERRY_RELEASE_STRIP_INSTRUMENTS \ --volume "$HOME/.cargo:/tmp/cargo-home" \ --volume "$HOME/.rustup:/tmp/rustup-home" \ --volume "$GITHUB_WORKSPACE:/work" \ diff --git a/changelog.d/11629-release-runtime-no-instruments.md b/changelog.d/11629-release-runtime-no-instruments.md new file mode 100644 index 0000000000..bff6fb00c7 --- /dev/null +++ b/changelog.d/11629-release-runtime-no-instruments.md @@ -0,0 +1,3 @@ +- **perf(size): release packages build their runtime archives without the GC/diagnostic instruments.** Follow-up to #11605. perry-runtime's `diagnostics`, `gc-instruments` and `hot-diag` features are in its `default` set, so the prebuilt full-feature `libperry_runtime.a` that an installed perry links carried every census, verifier, trace and hot-path diagnostic into every program. Their code is now gated on build-script cfgs (`perry_diagnostics`, `perry_gc_instruments`, `perry_hot_diag`) that `crates/perry-runtime/build.rs` sets from the matching features unless `PERRY_RELEASE_STRIP_INSTRUMENTS=1`; `.github/workflows/release-packages.yml` sets it for every package build (including both Linux docker builds). An env var rather than a feature change keeps one feature union across every shipped archive (#6303/#7358: the ext crates and the stdlib bundle their own perry-runtime copies), and a workspace build, `cargo test` and auto-optimized links (`PERRY_GC_INSTRUMENTS=1`, or an instrument knob set while compiling) are unchanged. A stripped binary behaves as a build without the features always has: an instrument knob refuses at startup with the existing "built without the GC instruments" message. A new compile-time note explains that a prebuilt link cannot honor an instrument request and names `PERRY_WORKSPACE_ROOT`. Installed-mode sizes (Linux x86_64, release recipe, on top of #11605): the `node:net` + `fetch` + TLS backend 15.05 → 14.71 MB, a runtime-only fs program 8.76 → 8.39 MB, `node:net` 11.23 → 10.87 MB, `node:sqlite` 11.00 → 10.64 MB, fetch 12.66 → 12.29 MB; the prebuilt core runtime (already built without the instruments) and auto-optimized links unchanged. + +Retain declared Windows Inkwell0.9 lock entries alongside non-Windows0.10 after integrating current dependency updates. diff --git a/changelog.d/11629-stack-parent-refresh.md b/changelog.d/11629-stack-parent-refresh.md new file mode 100644 index 0000000000..6c180b0174 --- /dev/null +++ b/changelog.d/11629-stack-parent-refresh.md @@ -0,0 +1 @@ +Preserve both the parent feature-installer and child release-instrument snapshot audits while refreshing the stacked parent. The five snapshot source files and pins are unchanged from the reviewed child; no runtime code or collector behavior changes. diff --git a/crates/perry-runtime/build.rs b/crates/perry-runtime/build.rs index 74fe51afce..60dd4da182 100644 --- a/crates/perry-runtime/build.rs +++ b/crates/perry-runtime/build.rs @@ -543,7 +543,35 @@ fn generate_single_byte_encodings(out_dir: &str) { .expect("write single_byte_encodings.rs"); } +/// The GC/diagnostic instruments (`diagnostics`, `gc-instruments`, `hot-diag`) +/// compile under the `perry_diagnostics` / `perry_gc_instruments` / +/// `perry_hot_diag` cfgs, set here when the feature is on — unless +/// `PERRY_RELEASE_STRIP_INSTRUMENTS=1`. The release workflow sets that for +/// every build in its jobs, so the shipped archives carry no instruments while +/// every other build (dev, CI, auto-optimize) is unchanged. An env var rather +/// than a feature: a feature can only be added by Cargo's unification, and the +/// release co-builds each ext crate with the runtime (#6303/#7358), whose +/// `default` would switch the instruments back on; the build script sees the +/// variable in every one of those invocations alike. A shipped runtime asked +/// for an instrument knob refuses it with the existing +/// "instruments not compiled in" diagnostic. +fn emit_instrument_cfgs() { + println!("cargo:rerun-if-env-changed=PERRY_RELEASE_STRIP_INSTRUMENTS"); + let strip = std::env::var("PERRY_RELEASE_STRIP_INSTRUMENTS").is_ok_and(|v| v == "1"); + for (feature_env, cfg) in [ + ("CARGO_FEATURE_DIAGNOSTICS", "perry_diagnostics"), + ("CARGO_FEATURE_GC_INSTRUMENTS", "perry_gc_instruments"), + ("CARGO_FEATURE_HOT_DIAG", "perry_hot_diag"), + ] { + println!("cargo:rustc-check-cfg=cfg({cfg})"); + if std::env::var_os(feature_env).is_some() && !strip { + println!("cargo:rustc-cfg={cfg}"); + } + } +} + fn main() { + emit_instrument_cfgs(); println!("cargo:rerun-if-changed=src/ffi/perry_memory_profile.c"); if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("linux") && std::env::var("CARGO_CFG_TARGET_POINTER_WIDTH").as_deref() == Ok("64") diff --git a/crates/perry-runtime/src/arena/alloc_sample.rs b/crates/perry-runtime/src/arena/alloc_sample.rs index 904ee31173..5fa4ed48f3 100644 --- a/crates/perry-runtime/src/arena/alloc_sample.rs +++ b/crates/perry-runtime/src/arena/alloc_sample.rs @@ -38,7 +38,7 @@ // constant "off" (inlined, so every caller's guarded branch folds away and the // instrument links nothing); the rest of the module stays compiled so it // cannot rot, hence the allow. -#![cfg_attr(not(feature = "gc-instruments"), allow(dead_code, unused_imports))] +#![cfg_attr(not(perry_gc_instruments), allow(dead_code, unused_imports))] use std::cell::{Cell, RefCell}; use std::collections::HashMap; @@ -77,22 +77,22 @@ crate::perry_thread_local! { /// unparsable value selects the default interval. /// The sampling interval, or 0 when off. A constant 0 without the /// `gc-instruments` feature, so the allocation fast paths drop the check. -#[cfg(feature = "gc-instruments")] +#[cfg(perry_gc_instruments)] #[inline(always)] fn current_interval() -> usize { INTERVAL.load(Ordering::Relaxed) } -#[cfg(not(feature = "gc-instruments"))] +#[cfg(not(perry_gc_instruments))] #[inline(always)] fn current_interval() -> usize { 0 } -#[cfg(not(feature = "gc-instruments"))] +#[cfg(not(perry_gc_instruments))] pub(crate) fn init_from_env() {} -#[cfg(feature = "gc-instruments")] +#[cfg(perry_gc_instruments)] pub(crate) fn init_from_env() { let raw = std::env::var("PERRY_ALLOC_SITE_SAMPLE").ok(); let interval = parse_interval(raw.as_deref()); diff --git a/crates/perry-runtime/src/arena/mod.rs b/crates/perry-runtime/src/arena/mod.rs index e26b675b49..84879dd35e 100644 --- a/crates/perry-runtime/src/arena/mod.rs +++ b/crates/perry-runtime/src/arena/mod.rs @@ -98,7 +98,7 @@ pub(crate) use allocators::{ }; // walk.rs -#[cfg(feature = "diagnostics")] +#[cfg(perry_diagnostics)] pub(crate) use walk::ArenaRegionTelemetry; pub use walk::{ arena_block_count, arena_in_use_bytes, arena_total_bytes, arena_walk_objects, diff --git a/crates/perry-runtime/src/arena/quarantine.rs b/crates/perry-runtime/src/arena/quarantine.rs index 0746d4240e..bbbdad9235 100644 --- a/crates/perry-runtime/src/arena/quarantine.rs +++ b/crates/perry-runtime/src/arena/quarantine.rs @@ -101,7 +101,7 @@ // constant "off" (inlined, so every caller's guarded branch folds away and the // instrument links nothing); the rest of the module stays compiled so it // cannot rot, hence the allow. -#![cfg_attr(not(feature = "gc-instruments"), allow(dead_code, unused_imports))] +#![cfg_attr(not(perry_gc_instruments), allow(dead_code, unused_imports))] use super::*; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering as AtomicOrdering}; @@ -149,13 +149,13 @@ thread_local! { static MODE_OVERRIDE: Cell> = const { Cell::new(None) }; } -#[cfg(not(feature = "gc-instruments"))] +#[cfg(not(perry_gc_instruments))] #[inline(always)] pub(crate) fn fromspace_protection_mode() -> FromSpaceProtection { FromSpaceProtection::Off } -#[cfg(feature = "gc-instruments")] +#[cfg(perry_gc_instruments)] pub(crate) fn fromspace_protection_mode() -> FromSpaceProtection { #[cfg(test)] if let Some(mode) = MODE_OVERRIDE.with(Cell::get) { diff --git a/crates/perry-runtime/src/arena/walk.rs b/crates/perry-runtime/src/arena/walk.rs index 4cf650834d..0156ecbd95 100644 --- a/crates/perry-runtime/src/arena/walk.rs +++ b/crates/perry-runtime/src/arena/walk.rs @@ -439,7 +439,7 @@ pub(crate) struct ArenaRegionTelemetry { } #[derive(Clone, Copy, Default)] -#[cfg_attr(not(feature = "diagnostics"), allow(dead_code))] +#[cfg_attr(not(perry_diagnostics), allow(dead_code))] pub(crate) struct ArenaTelemetrySnapshot { pub(crate) arena: ArenaRegionTelemetry, pub(crate) survivor0: ArenaRegionTelemetry, diff --git a/crates/perry-runtime/src/bun_compat/cli_utils.rs b/crates/perry-runtime/src/bun_compat/cli_utils.rs index 8fc3d69dc8..2e3de5a99c 100644 --- a/crates/perry-runtime/src/bun_compat/cli_utils.rs +++ b/crates/perry-runtime/src/bun_compat/cli_utils.rs @@ -626,7 +626,7 @@ pub extern "C" fn js_bun_generate_heap_snapshot(format: f64, encoding: f64) -> f if value_to_string(format) != "v8" { throw_type_error("Bun.generateHeapSnapshot format must be 'v8'"); } - #[cfg(feature = "diagnostics")] + #[cfg(perry_diagnostics)] { let json = crate::gc::gc_build_v8_heap_snapshot_json(); if !is_undefined_or_null(encoding) && value_to_string(encoding) == "arraybuffer" { @@ -637,7 +637,7 @@ pub extern "C" fn js_bun_generate_heap_snapshot(format: f64, encoding: f64) -> f } boxed_str(json.as_bytes()) } - #[cfg(not(feature = "diagnostics"))] + #[cfg(not(perry_diagnostics))] { let _ = encoding; throw_type_error("Heap snapshot diagnostics are not enabled in this Perry runtime") diff --git a/crates/perry-runtime/src/error_stack_frames.rs b/crates/perry-runtime/src/error_stack_frames.rs index 2429a6445b..b4fb90481c 100644 --- a/crates/perry-runtime/src/error_stack_frames.rs +++ b/crates/perry-runtime/src/error_stack_frames.rs @@ -416,7 +416,7 @@ fn dladdr_info(ip: usize) -> Option { /// `gc::instruments::INSTRUMENT_KNOBS`). #[cfg(unix)] fn stack_symbols_enabled() -> bool { - if !cfg!(feature = "gc-instruments") { + if !cfg!(perry_gc_instruments) { return false; } static ENABLED: OnceLock = OnceLock::new(); @@ -1011,7 +1011,7 @@ mod tests { /// `PERRY_STACK_SYMBOLS`; no in-process test mutates the cached flag. /// The table is served by `gc-instruments` (#11541): without the feature /// the knob aborts at startup, so there is no opted-in state to test. - #[cfg(all(unix, feature = "gc-instruments"))] + #[cfg(all(unix, perry_gc_instruments))] #[test] fn describe_ip_names_a_kept_runtime_symbol_when_nm_is_opted_in() { const CHILD_ENV: &str = "PERRY_TEST_STACK_SYMBOLS_NM_CHILD"; diff --git a/crates/perry-runtime/src/gc/barrier_arming.rs b/crates/perry-runtime/src/gc/barrier_arming.rs index 405b2211ea..2ecf738729 100644 --- a/crates/perry-runtime/src/gc/barrier_arming.rs +++ b/crates/perry-runtime/src/gc/barrier_arming.rs @@ -124,7 +124,7 @@ thread_local! { // which is itself `allow(dead_code)` without the `diagnostics` feature — so a // product build (`cargo check -p perry --bins`, `-D warnings`) sees this as // unused. Same `cfg_attr` the three sibling sites in `telemetry.rs` use. -#[cfg_attr(not(feature = "diagnostics"), allow(dead_code))] +#[cfg_attr(not(perry_diagnostics), allow(dead_code))] pub(super) fn remembered_reconstruct_census() -> RememberedReconstructCensus { RECONSTRUCT_CENSUS.with(Cell::get) } diff --git a/crates/perry-runtime/src/gc/census.rs b/crates/perry-runtime/src/gc/census.rs index 410b479542..7e8ab22a8d 100644 --- a/crates/perry-runtime/src/gc/census.rs +++ b/crates/perry-runtime/src/gc/census.rs @@ -38,7 +38,7 @@ // collection passes below compile to nothing; the walk, the classifier and // the JSON writer then have no caller. `allow` rather than a cascade of cfgs: // they stay compiled, so they cannot rot in the default build either. -#![cfg_attr(not(feature = "gc-instruments"), allow(dead_code, unused_imports))] +#![cfg_attr(not(perry_gc_instruments), allow(dead_code, unused_imports))] use super::*; use std::sync::atomic::{AtomicBool, Ordering}; @@ -86,9 +86,9 @@ pub(crate) fn census_path() -> Option<&'static str> { } // Built without the instruments: never enabled (`gc_init` aborted if the // knob was set). - #[cfg(not(feature = "gc-instruments"))] + #[cfg(not(perry_gc_instruments))] return None; - #[cfg(feature = "gc-instruments")] + #[cfg(perry_gc_instruments)] CENSUS_PATH .get_or_init(|| { std::env::var("PERRY_GC_CENSUS") @@ -211,11 +211,11 @@ fn header_is_marked(header: *const GcHeader) -> bool { /// the reachable set so the sweep-entry pass can tell reachability from /// block-persistence retention. No-op unless armed. pub(super) fn census_pass1_if_armed() { - #[cfg(feature = "gc-instruments")] + #[cfg(perry_gc_instruments)] census_pass1_if_armed_impl(); } -#[cfg(feature = "gc-instruments")] +#[cfg(perry_gc_instruments)] fn census_pass1_if_armed_impl() { if !ARMED.with(|c| c.get()) { return; @@ -240,11 +240,11 @@ fn census_pass1_if_armed_impl() { /// Pass 2: sweep entry of the same synchronous full cycle (all marks final, /// nothing swept, block persistence already applied). Consumes the arm. pub(super) fn census_take_if_armed_at_full_sweep_start() { - #[cfg(feature = "gc-instruments")] + #[cfg(perry_gc_instruments)] census_take_if_armed_at_full_sweep_start_impl(); } -#[cfg(feature = "gc-instruments")] +#[cfg(perry_gc_instruments)] fn census_take_if_armed_at_full_sweep_start_impl() { if !ARMED.with(|c| c.replace(false)) { return; diff --git a/crates/perry-runtime/src/gc/cycle.rs b/crates/perry-runtime/src/gc/cycle.rs index d6d817adff..175d27331a 100644 --- a/crates/perry-runtime/src/gc/cycle.rs +++ b/crates/perry-runtime/src/gc/cycle.rs @@ -17,7 +17,7 @@ pub(super) enum GcCyclePhase { } impl GcCyclePhase { - #[cfg(feature = "diagnostics")] + #[cfg(perry_diagnostics)] #[inline] pub(super) const fn as_str(self) -> &'static str { match self { diff --git a/crates/perry-runtime/src/gc/forwarding.rs b/crates/perry-runtime/src/gc/forwarding.rs index d6c73026f7..7e6ec18b58 100644 --- a/crates/perry-runtime/src/gc/forwarding.rs +++ b/crates/perry-runtime/src/gc/forwarding.rs @@ -216,11 +216,7 @@ pub(super) fn accept_forwarding_target(user_addr: usize) -> bool { /// Resolve a live field's closure while collector traversal may still see a /// forwarding stub. Use the same validated source/target gates as the copying /// rewrite walk; never read closure payload metadata from a forwarded stub. -#[cfg(any( - debug_assertions, - feature = "field-rep-assert", - feature = "gc-instruments" -))] +#[cfg(any(debug_assertions, feature = "field-rep-assert", perry_gc_instruments))] pub(crate) fn field_rep_live_address(mut addr: usize) -> Option { for _ in 0..64 { let Some(header) = forwarding_walk_header(addr) else { diff --git a/crates/perry-runtime/src/gc/fromspace_scan.rs b/crates/perry-runtime/src/gc/fromspace_scan.rs index 89b0dd28ae..a4288778e6 100644 --- a/crates/perry-runtime/src/gc/fromspace_scan.rs +++ b/crates/perry-runtime/src/gc/fromspace_scan.rs @@ -49,7 +49,7 @@ // constant "off" (inlined, so every caller's guarded branch folds away and the // instrument links nothing); the rest of the module stays compiled so it // cannot rot, hence the allow. -#![cfg_attr(not(feature = "gc-instruments"), allow(dead_code, unused_imports))] +#![cfg_attr(not(perry_gc_instruments), allow(dead_code, unused_imports))] use super::*; @@ -142,13 +142,13 @@ pub(super) fn resolve_scan_knobs(scan: Option<&str>, abort: Option<&str>) -> (bo (truthy(scan) || abort, abort) } -#[cfg(not(feature = "gc-instruments"))] +#[cfg(not(perry_gc_instruments))] #[inline(always)] pub(super) fn fromspace_scan_enabled() -> bool { false } -#[cfg(feature = "gc-instruments")] +#[cfg(perry_gc_instruments)] pub(super) fn fromspace_scan_enabled() -> bool { use std::sync::OnceLock; static CACHED: OnceLock = OnceLock::new(); diff --git a/crates/perry-runtime/src/gc/instruments.rs b/crates/perry-runtime/src/gc/instruments.rs index 96165de1f3..124dad20cf 100644 --- a/crates/perry-runtime/src/gc/instruments.rs +++ b/crates/perry-runtime/src/gc/instruments.rs @@ -496,7 +496,7 @@ impl Drop for FinalRemarkTimer { /// built on the knob pass having exercised nothing, so the process aborts at /// startup instead — see CLAUDE.md "Four ways a gate can be unable to fail". // Read only by the feature-off startup check below. -#[cfg_attr(feature = "gc-instruments", allow(dead_code))] +#[cfg_attr(perry_gc_instruments, allow(dead_code))] pub(crate) const INSTRUMENT_KNOBS: &[&str] = &[ "PERRY_GC_CENSUS", "PERRY_GC_PROTECT_FROMSPACE", @@ -511,7 +511,7 @@ pub(crate) const INSTRUMENT_KNOBS: &[&str] = &[ ]; /// The first instrument knob set (non-empty) in the environment, if any. -#[cfg_attr(feature = "gc-instruments", allow(dead_code))] +#[cfg_attr(perry_gc_instruments, allow(dead_code))] pub(crate) fn requested_instrument_knob() -> Option<&'static str> { INSTRUMENT_KNOBS .iter() @@ -520,14 +520,14 @@ pub(crate) fn requested_instrument_knob() -> Option<&'static str> { } /// Startup check for binaries built without the instruments (see above). -#[cfg(not(feature = "gc-instruments"))] +#[cfg(not(perry_gc_instruments))] pub(crate) fn refuse_instrument_knobs_without_instruments() { if let Some(knob) = requested_instrument_knob() { instruments_unavailable(knob); } } -#[cfg(not(feature = "gc-instruments"))] +#[cfg(not(perry_gc_instruments))] #[cold] #[inline(never)] fn instruments_unavailable(knob: &str) -> ! { diff --git a/crates/perry-runtime/src/gc/malloc.rs b/crates/perry-runtime/src/gc/malloc.rs index b774e55e8e..7a66c4632b 100644 --- a/crates/perry-runtime/src/gc/malloc.rs +++ b/crates/perry-runtime/src/gc/malloc.rs @@ -30,7 +30,7 @@ impl MallocKindTelemetry { } } - #[cfg(feature = "diagnostics")] + #[cfg(perry_diagnostics)] pub(super) fn reset_cycle_deltas(&mut self) { self.allocated_count = 0; self.allocated_bytes = 0; @@ -435,7 +435,7 @@ impl MallocState { counters.copied_minor_validation_lookups.saturating_add(1); } - #[cfg(feature = "diagnostics")] + #[cfg(perry_diagnostics)] pub(super) fn take_kind_telemetry( &mut self, ) -> [MallocKindTelemetry; MALLOC_KIND_BUCKET_COUNT] { diff --git a/crates/perry-runtime/src/gc/mod.rs b/crates/perry-runtime/src/gc/mod.rs index 67c1f6186a..ebe3a95cf4 100644 --- a/crates/perry-runtime/src/gc/mod.rs +++ b/crates/perry-runtime/src/gc/mod.rs @@ -184,11 +184,7 @@ use copying_first_cycle::*; // Named rather than glob-imported: a glob does not propagate through the // transitive re-exports the gc submodules reach these through. use copying_pointer_set::{plausible_gc_header, CopyingPointer, CopyingPointerKind}; -#[cfg(any( - debug_assertions, - feature = "field-rep-assert", - feature = "gc-instruments" -))] +#[cfg(any(debug_assertions, feature = "field-rep-assert", perry_gc_instruments))] pub(crate) use forwarding::field_rep_live_address; use forwarding::*; use sticky_remembered::*; @@ -285,12 +281,12 @@ pub use verify::*; /// Env-gated heap census (`PERRY_GC_CENSUS`); off by default. pub(crate) mod census; mod census_field_repr; -#[cfg(feature = "diagnostics")] +#[cfg(perry_diagnostics)] mod heap_snapshot; mod heap_stats; mod regex_census; pub use census::{census_poll_signal, gc_census_enabled}; -#[cfg(feature = "diagnostics")] +#[cfg(perry_diagnostics)] pub use heap_snapshot::gc_build_v8_heap_snapshot_json; pub(crate) use heap_stats::heap_stats; @@ -984,9 +980,9 @@ pub fn gc_init() { return; } crate::perf_hooks::init_time_origin(); - #[cfg(not(feature = "gc-instruments"))] + #[cfg(not(perry_gc_instruments))] instruments::refuse_instrument_knobs_without_instruments(); - #[cfg(not(feature = "hot-diag"))] + #[cfg(not(perry_hot_diag))] crate::hot_diag::refuse_knobs_without_hot_diag(); // `PERRY_GC_CENSUS`: remember the main thread and install the SIGUSR2 // trigger. No-op (one OnceLock read) when the env var is unset. diff --git a/crates/perry-runtime/src/gc/oldgen.rs b/crates/perry-runtime/src/gc/oldgen.rs index 15f84faf08..14a45a4aed 100644 --- a/crates/perry-runtime/src/gc/oldgen.rs +++ b/crates/perry-runtime/src/gc/oldgen.rs @@ -110,7 +110,7 @@ impl Default for EvacuationPolicyDecision { } #[derive(Clone, Copy, Default)] -#[cfg_attr(not(feature = "diagnostics"), allow(dead_code))] +#[cfg_attr(not(perry_diagnostics), allow(dead_code))] pub(super) struct SweepTraceStats { pub(super) dead_bytes: u64, // Compatibility alias for dead_bytes. diff --git a/crates/perry-runtime/src/gc/pin.rs b/crates/perry-runtime/src/gc/pin.rs index 6514052319..2d7930465c 100644 --- a/crates/perry-runtime/src/gc/pin.rs +++ b/crates/perry-runtime/src/gc/pin.rs @@ -598,7 +598,7 @@ pub(super) fn pinned_young_move_report( /// Human-readable name for a `GcHeader::obj_type`. /// -/// `types::gc_type_name` is `#[cfg(feature = "diagnostics")]`, and this abort +/// `types::gc_type_name` is `#[cfg(perry_diagnostics)]`, and this abort /// has to print the same text in every build — a fault report that degrades /// with the feature set is a fault report nobody can compare against. fn gc_type_label(obj_type: u8) -> &'static str { diff --git a/crates/perry-runtime/src/gc/policy.rs b/crates/perry-runtime/src/gc/policy.rs index 7a7ba38d96..2780661d71 100644 --- a/crates/perry-runtime/src/gc/policy.rs +++ b/crates/perry-runtime/src/gc/policy.rs @@ -1125,7 +1125,7 @@ pub(super) enum GcCollectionKind { } impl GcCollectionKind { - #[cfg(feature = "diagnostics")] + #[cfg(perry_diagnostics)] #[inline] pub(super) fn as_str(self) -> &'static str { match self { @@ -1164,7 +1164,7 @@ pub(super) enum GcTriggerKind { } impl GcTriggerKind { - #[cfg(feature = "diagnostics")] + #[cfg(perry_diagnostics)] #[inline] pub(super) fn as_str(self) -> &'static str { match self { @@ -1243,7 +1243,7 @@ impl DeferredGcRequest { } #[derive(Clone, Copy)] -#[cfg_attr(not(feature = "diagnostics"), allow(dead_code))] +#[cfg_attr(not(perry_diagnostics), allow(dead_code))] pub(super) struct GcStepSnapshot { pub(super) arena_step_bytes: usize, pub(super) next_arena_trigger_bytes: usize, @@ -2325,7 +2325,7 @@ pub(super) fn note_copying_minor_young_survival(survival_permille: u64) { /// Whether the last copying minor measured a retaining heap. Trace/test /// observability — a gate that cannot see this cannot prove which arm paced a /// given run. -#[cfg(any(feature = "diagnostics", test))] +#[cfg(any(perry_diagnostics, test))] pub(super) fn major_pacing_retaining() -> bool { GC_MAJOR_PACING_RETAINING.with(|c| c.get()) } @@ -2576,7 +2576,7 @@ thread_local! { } /// Current arena-growth escalation backoff shift. -#[cfg(any(feature = "diagnostics", test))] +#[cfg(any(perry_diagnostics, test))] pub(super) fn major_pacing_backoff_shift() -> u32 { GC_MAJOR_PACING_BACKOFF_SHIFT.with(|shift| shift.get()) } @@ -2595,7 +2595,7 @@ pub(super) fn major_pacing_backoff_shift() -> u32 { // `test` as well as `diagnostics` (matching `major_pacing_backoff_shift`), so // the test that pins snapshot-vs-predicate agreement still builds under // `--no-default-features`, where the trace itself is compiled out. -#[cfg(any(feature = "diagnostics", test))] +#[cfg(any(perry_diagnostics, test))] pub(super) fn major_pacing_snapshot() -> (usize, u32, Option) { let baseline = GC_LAST_FULL_ARENA_IN_USE_BYTES.with(|bytes| bytes.get()); let shift = major_pacing_backoff_shift(); diff --git a/crates/perry-runtime/src/gc/roots/scan_mode.rs b/crates/perry-runtime/src/gc/roots/scan_mode.rs index 305a51dea6..ba17cf1584 100644 --- a/crates/perry-runtime/src/gc/roots/scan_mode.rs +++ b/crates/perry-runtime/src/gc/roots/scan_mode.rs @@ -32,7 +32,7 @@ pub(crate) enum ConservativeStackScanDecision { } impl ConservativeStackScanDecision { - #[cfg(feature = "diagnostics")] + #[cfg(perry_diagnostics)] #[inline] pub(crate) const fn as_str(self) -> &'static str { match self { diff --git a/crates/perry-runtime/src/gc/schedule.rs b/crates/perry-runtime/src/gc/schedule.rs index 1960135fdb..858c2d0c31 100644 --- a/crates/perry-runtime/src/gc/schedule.rs +++ b/crates/perry-runtime/src/gc/schedule.rs @@ -106,7 +106,7 @@ // constant "off" (inlined, so every caller's guarded branch folds away and the // instrument links nothing); the rest of the module stays compiled so it // cannot rot, hence the allow. -#![cfg_attr(not(feature = "gc-instruments"), allow(dead_code, unused_imports))] +#![cfg_attr(not(perry_gc_instruments), allow(dead_code, unused_imports))] use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; @@ -245,13 +245,13 @@ thread_local! { /// Resolved `(seed, threshold)`, or `None` when the mode is off. Cached: the /// environment is read exactly once per process. -#[cfg(not(feature = "gc-instruments"))] +#[cfg(not(perry_gc_instruments))] #[inline(always)] fn resolved() -> Option<(u64, u64)> { None } -#[cfg(feature = "gc-instruments")] +#[cfg(perry_gc_instruments)] fn resolved() -> Option<(u64, u64)> { #[cfg(test)] if let Some(over) = SCHEDULE_OVERRIDE.with(std::cell::Cell::get) { diff --git a/crates/perry-runtime/src/gc/telemetry.rs b/crates/perry-runtime/src/gc/telemetry.rs index aae95562ad..2347e4bd63 100644 --- a/crates/perry-runtime/src/gc/telemetry.rs +++ b/crates/perry-runtime/src/gc/telemetry.rs @@ -62,9 +62,9 @@ impl Drop for GcDiagTestGuard { /// constant `false`, so the mark verifiers behind it (~28 KiB) are not linked, /// and `gc_init` aborts if the knob is set (`instruments::INSTRUMENT_KNOBS`). pub(crate) fn gc_verify_mark_enabled() -> bool { - #[cfg(not(feature = "gc-instruments"))] + #[cfg(not(perry_gc_instruments))] return false; - #[cfg(feature = "gc-instruments")] + #[cfg(perry_gc_instruments)] { static ENABLED: std::sync::OnceLock = std::sync::OnceLock::new(); *crate::once_init::get_or_init(&ENABLED, || env_flag_enabled("PERRY_GC_VERIFY_MARK")) @@ -123,7 +123,7 @@ thread_local! { } #[derive(Clone, Copy, Default)] -#[cfg_attr(not(feature = "diagnostics"), allow(dead_code))] +#[cfg_attr(not(perry_diagnostics), allow(dead_code))] pub(super) struct RememberedSetTraceStats { pub(super) entries_scanned: usize, pub(super) valid_roots: usize, @@ -251,7 +251,7 @@ pub(super) enum CopiedMinorFallbackReason { } impl CopiedMinorFallbackReason { - #[cfg(feature = "diagnostics")] + #[cfg(perry_diagnostics)] #[inline] pub(super) const fn as_str(self) -> &'static str { match self { @@ -433,7 +433,7 @@ impl RootSourceSlotTraceStats { } #[derive(Clone, Copy, Default)] -#[cfg_attr(not(feature = "diagnostics"), allow(dead_code))] +#[cfg_attr(not(perry_diagnostics), allow(dead_code))] pub(super) struct NativeStackFallbackTraceStats { pub(super) decision: ConservativeStackScanDecision, pub(super) scanned: bool, @@ -827,7 +827,7 @@ pub(super) struct GcPauseStepTrace { } #[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[cfg_attr(not(feature = "diagnostics"), allow(dead_code))] +#[cfg_attr(not(perry_diagnostics), allow(dead_code))] pub(super) enum AllocatorMaintenanceStatus { Skipped, Executed, @@ -835,7 +835,7 @@ pub(super) enum AllocatorMaintenanceStatus { } impl AllocatorMaintenanceStatus { - #[cfg(feature = "diagnostics")] + #[cfg(perry_diagnostics)] #[inline] pub(super) const fn as_str(self) -> &'static str { match self { @@ -847,7 +847,7 @@ impl AllocatorMaintenanceStatus { } #[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[cfg_attr(not(feature = "diagnostics"), allow(dead_code))] +#[cfg_attr(not(perry_diagnostics), allow(dead_code))] pub(super) enum AllocatorMaintenanceReason { OrdinaryBudgeted, NotSupported, @@ -860,7 +860,7 @@ pub(super) enum AllocatorMaintenanceReason { } impl AllocatorMaintenanceReason { - #[cfg(feature = "diagnostics")] + #[cfg(perry_diagnostics)] #[inline] pub(super) const fn as_str(self) -> &'static str { match self { @@ -874,7 +874,7 @@ impl AllocatorMaintenanceReason { } #[derive(Clone, Copy, Debug, Eq, PartialEq)] -#[cfg_attr(not(feature = "diagnostics"), allow(dead_code))] +#[cfg_attr(not(perry_diagnostics), allow(dead_code))] pub(super) struct AllocatorMaintenanceEvent { pub(super) status: AllocatorMaintenanceStatus, pub(super) reason: AllocatorMaintenanceReason, @@ -882,7 +882,7 @@ pub(super) struct AllocatorMaintenanceEvent { } #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -#[cfg_attr(not(feature = "diagnostics"), allow(dead_code))] +#[cfg_attr(not(perry_diagnostics), allow(dead_code))] pub(super) struct AllocatorMaintenanceTrace { pub(super) malloc_trim: Option, /// #9612: the mimalloc purge, which is the primitive that actually @@ -891,7 +891,7 @@ pub(super) struct AllocatorMaintenanceTrace { pub(super) allocator_purge: Option, } -#[cfg_attr(not(feature = "diagnostics"), allow(dead_code))] +#[cfg_attr(not(perry_diagnostics), allow(dead_code))] pub(super) struct GcCycleTrace { pub(super) collection_kind: GcCollectionKind, pub(super) trigger_kind: GcTriggerKind, @@ -1068,7 +1068,7 @@ impl GcCycleTrace { } } - #[cfg(feature = "diagnostics")] + #[cfg(perry_diagnostics)] pub(super) fn into_json(mut self, steps_after: GcStepSnapshot) -> serde_json::Value { self.capture_layout_scans(); self.debt.record(GcDebtSnapshot::current()); @@ -1374,7 +1374,7 @@ impl GcCycleTrace { }) } - #[cfg(feature = "diagnostics")] + #[cfg(perry_diagnostics)] pub(super) fn emit(self, steps_after: GcStepSnapshot) { let event = self.into_json(steps_after); #[cfg(test)] @@ -1384,7 +1384,7 @@ impl GcCycleTrace { } } - #[cfg(not(feature = "diagnostics"))] + #[cfg(not(perry_diagnostics))] pub(super) fn emit(self, _steps_after: GcStepSnapshot) { eprintln!( "[gc] cycle (diagnostics feature disabled — rebuild without --no-default-features for JSON trace)" @@ -1392,7 +1392,7 @@ impl GcCycleTrace { } } -#[cfg(feature = "diagnostics")] +#[cfg(perry_diagnostics)] pub(super) fn debt_snapshot_json(snapshot: GcDebtSnapshot) -> serde_json::Value { serde_json::json!({ "arena_debt_bytes": snapshot.arena_debt_bytes, @@ -1401,7 +1401,7 @@ pub(super) fn debt_snapshot_json(snapshot: GcDebtSnapshot) -> serde_json::Value }) } -#[cfg(feature = "diagnostics")] +#[cfg(perry_diagnostics)] pub(super) fn pause_budget_json( progress_kind: GcProgressKind, progress_budget: GcPauseBudget, @@ -1419,7 +1419,7 @@ pub(super) fn pause_budget_json( }) } -#[cfg(feature = "diagnostics")] +#[cfg(perry_diagnostics)] pub(super) fn pause_step_json(step: GcPauseStepTrace) -> serde_json::Value { let progress_budget = gc_progress_contract().budget_for(step.progress_kind); let within_soft_pause_target = progress_budget @@ -1447,7 +1447,7 @@ pub(super) fn pause_step_json(step: GcPauseStepTrace) -> serde_json::Value { }) } -#[cfg(feature = "diagnostics")] +#[cfg(perry_diagnostics)] pub(super) fn allocator_maintenance_json( trace: AllocatorMaintenanceTrace, progress_kind: GcProgressKind, @@ -1477,7 +1477,7 @@ pub(super) fn allocator_maintenance_json( }) } -#[cfg(feature = "diagnostics")] +#[cfg(perry_diagnostics)] fn default_malloc_trim_maintenance(progress_kind: GcProgressKind) -> AllocatorMaintenanceEvent { if progress_kind.is_budgeted() { return AllocatorMaintenanceEvent { @@ -1573,7 +1573,7 @@ pub(super) fn malloc_object_count() -> usize { MALLOC_STATE.with(|s| s.borrow().objects.len()) } -#[cfg(feature = "diagnostics")] +#[cfg(perry_diagnostics)] pub(super) fn malloc_kind_telemetry_row( obj_type: u8, counters: MallocKindTelemetry, @@ -1594,7 +1594,7 @@ pub(super) fn malloc_kind_telemetry_row( }) } -#[cfg(feature = "diagnostics")] +#[cfg(perry_diagnostics)] pub(super) fn root_source_slot_json(stats: RootSourceSlotTraceStats) -> serde_json::Value { serde_json::json!({ "registered_scanners": stats.registered_scanners, @@ -1605,7 +1605,7 @@ pub(super) fn root_source_slot_json(stats: RootSourceSlotTraceStats) -> serde_js }) } -#[cfg(feature = "diagnostics")] +#[cfg(perry_diagnostics)] pub(super) fn root_sources_json(stats: RootSourcesTraceStats) -> serde_json::Value { serde_json::json!({ "compiled_shadow": root_source_slot_json(stats.compiled_shadow), @@ -1634,7 +1634,7 @@ pub(super) fn root_sources_json(stats: RootSourcesTraceStats) -> serde_json::Val }) } -#[cfg(feature = "diagnostics")] +#[cfg(perry_diagnostics)] pub(super) fn malloc_kind_telemetry_json_from_snapshot( snapshot: [MallocKindTelemetry; MALLOC_KIND_BUCKET_COUNT], ) -> serde_json::Value { @@ -1653,13 +1653,13 @@ pub(super) fn malloc_kind_telemetry_json_from_snapshot( serde_json::Value::Array(rows) } -#[cfg(feature = "diagnostics")] +#[cfg(perry_diagnostics)] pub(super) fn take_malloc_kind_telemetry_json() -> serde_json::Value { let snapshot = MALLOC_STATE.with(|s| s.borrow_mut().take_kind_telemetry()); malloc_kind_telemetry_json_from_snapshot(snapshot) } -#[cfg(feature = "diagnostics")] +#[cfg(perry_diagnostics)] pub(super) fn arena_region_json(region: crate::arena::ArenaRegionTelemetry) -> serde_json::Value { serde_json::json!({ "in_use_bytes": region.in_use_bytes, @@ -1668,7 +1668,7 @@ pub(super) fn arena_region_json(region: crate::arena::ArenaRegionTelemetry) -> s }) } -#[cfg(feature = "diagnostics")] +#[cfg(perry_diagnostics)] pub(super) fn arena_snapshot_json( snapshot: crate::arena::ArenaTelemetrySnapshot, ) -> serde_json::Value { @@ -1685,7 +1685,7 @@ pub(super) fn arena_snapshot_json( }) } -#[cfg(feature = "diagnostics")] +#[cfg(perry_diagnostics)] pub(super) fn steps_json(before: GcStepSnapshot, after: GcStepSnapshot) -> serde_json::Value { serde_json::json!({ "arena_step_bytes": { diff --git a/crates/perry-runtime/src/gc/trace.rs b/crates/perry-runtime/src/gc/trace.rs index 0f234464a0..3bb4d99af8 100644 --- a/crates/perry-runtime/src/gc/trace.rs +++ b/crates/perry-runtime/src/gc/trace.rs @@ -14,7 +14,7 @@ crate::perry_thread_local! { /// unclassifiable in that synthetic state, so the differential verifier /// must stand down for the rest of the thread's test. Read only by the /// verifier, which `gc-instruments` serves. - #[cfg_attr(not(feature = "gc-instruments"), allow(dead_code))] + #[cfg_attr(not(perry_gc_instruments), allow(dead_code))] pub(crate) static CLASSIFIER_VERIFY_SUPPRESSED: std::cell::Cell = const { std::cell::Cell::new(false) }; } @@ -89,11 +89,11 @@ pub(super) fn classifier_valid_object_start(addr: usize) -> bool { /// #6179: differential-verification mode for the page-metadata classifier. /// A `gc-instruments` knob (#10572): constant `false` without the feature. pub(super) fn classifier_verify_enabled() -> bool { - #[cfg(not(feature = "gc-instruments"))] + #[cfg(not(perry_gc_instruments))] return false; // The cached process-wide switch first: this runs on every census hit, and // the suppression flag is a thread-local (#10182). - #[cfg(feature = "gc-instruments")] + #[cfg(perry_gc_instruments)] { static CACHED: std::sync::OnceLock = std::sync::OnceLock::new(); *crate::once_init::get_or_init(&CACHED, || { diff --git a/crates/perry-runtime/src/gc/types.rs b/crates/perry-runtime/src/gc/types.rs index 9add6e76c6..84266a12cb 100644 --- a/crates/perry-runtime/src/gc/types.rs +++ b/crates/perry-runtime/src/gc/types.rs @@ -1117,7 +1117,7 @@ pub(crate) unsafe fn gc_type_finalize_unmarked_payload(obj_type: u8, user_ptr: * } } -#[cfg(feature = "diagnostics")] +#[cfg(perry_diagnostics)] #[inline] pub(super) fn gc_type_name(obj_type: u8) -> &'static str { gc_type_info(obj_type).map_or("unknown", |info| info.name) diff --git a/crates/perry-runtime/src/hot_diag.rs b/crates/perry-runtime/src/hot_diag.rs index 173f9f9a9c..e887a866d7 100644 --- a/crates/perry-runtime/src/hot_diag.rs +++ b/crates/perry-runtime/src/hot_diag.rs @@ -24,7 +24,7 @@ // Without `hot-diag` the probes below have no armed caller. `allow` rather // than a cascade of cfgs keeps them compiled, so they cannot rot unbuilt. -#![cfg_attr(not(feature = "hot-diag"), allow(dead_code, unused_imports))] +#![cfg_attr(not(perry_hot_diag), allow(dead_code, unused_imports))] use std::cell::RefCell; use std::collections::HashMap; @@ -53,7 +53,7 @@ pub(crate) fn sink_from_env(name: &str) -> Option { /// `HOT_DIAG_KNOBS` in the compiler's `optimized_libs/freshness.rs` (pinned by /// `hot_diag_knobs_match_the_runtime`). // Read only by the feature-off startup check below. -#[cfg_attr(feature = "hot-diag", allow(dead_code))] +#[cfg_attr(perry_hot_diag, allow(dead_code))] pub(crate) const HOT_DIAG_KNOBS: &[&str] = &[ "PERRY_REGEX_DIAG", "PERRY_IC_DIAG", @@ -65,7 +65,7 @@ pub(crate) const HOT_DIAG_KNOBS: &[&str] = &[ /// Startup check for binaries built without the instruments: a knob that /// would arm one (same spelling rules as [`sink_from_env`]) aborts. -#[cfg(not(feature = "hot-diag"))] +#[cfg(not(perry_hot_diag))] pub(crate) fn refuse_knobs_without_hot_diag() { if let Some(knob) = HOT_DIAG_KNOBS .iter() @@ -76,7 +76,7 @@ pub(crate) fn refuse_knobs_without_hot_diag() { } } -#[cfg(not(feature = "hot-diag"))] +#[cfg(not(perry_hot_diag))] #[cold] #[inline(never)] fn hot_diag_unavailable(knob: &str) -> ! { @@ -147,9 +147,9 @@ fn regex_sink() -> &'static Option { /// Is the regex instrument armed? One relaxed load once initialised. #[inline] pub fn regex_on() -> bool { - #[cfg(not(feature = "hot-diag"))] + #[cfg(not(perry_hot_diag))] return false; - #[cfg(feature = "hot-diag")] + #[cfg(perry_hot_diag)] { if REGEX_SINK.get().is_none() { regex_sink(); @@ -566,9 +566,9 @@ pub fn layout_on() -> bool { if let Some(armed) = LAYOUT_TEST_ARMED.with(std::cell::Cell::get) { return armed; } - #[cfg(not(feature = "hot-diag"))] + #[cfg(not(perry_hot_diag))] return false; - #[cfg(feature = "hot-diag")] + #[cfg(perry_hot_diag)] { if LAYOUT_SINK.get().is_none() { layout_sink(); @@ -813,9 +813,9 @@ impl Drop for LayoutDiagTestGuard { /// Is the IC-miss instrument armed? One relaxed load once initialised. #[inline] pub fn ic_on() -> bool { - #[cfg(not(feature = "hot-diag"))] + #[cfg(not(perry_hot_diag))] return false; - #[cfg(feature = "hot-diag")] + #[cfg(perry_hot_diag)] { if IC_SINK.get().is_none() { ic_sink(); @@ -1220,9 +1220,9 @@ fn enum_sink() -> &'static Option { /// Is the enumeration/concat execution counter armed? #[inline] pub fn enum_on() -> bool { - #[cfg(not(feature = "hot-diag"))] + #[cfg(not(perry_hot_diag))] return false; - #[cfg(feature = "hot-diag")] + #[cfg(perry_hot_diag)] { if ENUM_SINK.get().is_none() { enum_sink(); @@ -1392,9 +1392,9 @@ fn buffer_sink() -> &'static Option { /// Is the buffer-probe instrument armed? One relaxed load once initialised. #[inline] pub fn buffer_on() -> bool { - #[cfg(not(feature = "hot-diag"))] + #[cfg(not(perry_hot_diag))] return false; - #[cfg(feature = "hot-diag")] + #[cfg(perry_hot_diag)] { if BUFFER_SINK.get().is_none() { buffer_sink(); diff --git a/crates/perry-runtime/src/hot_diag/receiver_repr.rs b/crates/perry-runtime/src/hot_diag/receiver_repr.rs index 3cc96e5f96..7ec49d7101 100644 --- a/crates/perry-runtime/src/hot_diag/receiver_repr.rs +++ b/crates/perry-runtime/src/hot_diag/receiver_repr.rs @@ -5,7 +5,7 @@ //! relaxed load and enters none of the range, registry, or ownership probes. // See the parent module: without `hot-diag` nothing arms these probes. -#![cfg_attr(not(feature = "hot-diag"), allow(dead_code, unused_imports))] +#![cfg_attr(not(perry_hot_diag), allow(dead_code, unused_imports))] use super::{sink_from_env, write_sink, Sink}; use std::fmt::Write as _; @@ -125,9 +125,9 @@ pub fn receiver_repr_on() -> bool { if TEST_FORCE_ON.load(Ordering::Relaxed) { return true; } - #[cfg(not(feature = "hot-diag"))] + #[cfg(not(perry_hot_diag))] return false; - #[cfg(feature = "hot-diag")] + #[cfg(perry_hot_diag)] { if RECEIVER_REPR_SINK.get().is_none() { receiver_repr_sink(); diff --git a/crates/perry-runtime/src/node_v8.rs b/crates/perry-runtime/src/node_v8.rs index 24090a4284..e3e7ac3920 100644 --- a/crates/perry-runtime/src/node_v8.rs +++ b/crates/perry-runtime/src/node_v8.rs @@ -360,11 +360,11 @@ pub extern "C" fn js_v8_cached_data_version_tag() -> f64 { #[no_mangle] pub extern "C" fn js_v8_get_heap_snapshot(options: f64) -> f64 { validate_heap_snapshot_options(options); - #[cfg(feature = "diagnostics")] + #[cfg(perry_diagnostics)] let json = crate::gc::gc_build_v8_heap_snapshot_json(); // OFF stub: the compiler enables `diagnostics` whenever a program uses the // v8 heap-snapshot APIs, so this branch is unreachable in practice. - #[cfg(not(feature = "diagnostics"))] + #[cfg(not(perry_diagnostics))] let json = String::from("{}"); snapshot_readable_stream(&json) } @@ -383,11 +383,11 @@ pub extern "C" fn js_v8_write_heap_snapshot(filename: f64, options: f64) -> f64 } }; validate_heap_snapshot_options(options); - #[cfg(feature = "diagnostics")] + #[cfg(perry_diagnostics)] let json = crate::gc::gc_build_v8_heap_snapshot_json(); // OFF stub: unreachable in practice (compiler enables `diagnostics` when a // program uses the v8 heap-snapshot APIs). - #[cfg(not(feature = "diagnostics"))] + #[cfg(not(perry_diagnostics))] let json = String::from("{}"); match std::fs::write(&path, json.as_bytes()) { Ok(()) => string_value(&path), diff --git a/crates/perry-runtime/src/object/field_rep_store.rs b/crates/perry-runtime/src/object/field_rep_store.rs index 4017c4a47c..78013fb3f9 100644 --- a/crates/perry-runtime/src/object/field_rep_store.rs +++ b/crates/perry-runtime/src/object/field_rep_store.rs @@ -535,11 +535,7 @@ pub(crate) fn shape_slot_is_f64(id: u32, slot: u32) -> bool { /// without either feature compiles no check, and the knob is one of /// `gc::instruments::INSTRUMENT_KNOBS`, so setting it there aborts at startup /// instead of passing having checked nothing. -#[cfg(any( - debug_assertions, - feature = "field-rep-assert", - feature = "gc-instruments" -))] +#[cfg(any(debug_assertions, feature = "field-rep-assert", perry_gc_instruments))] #[inline] pub(crate) fn field_rep_verify_enabled() -> bool { #[cfg(any(debug_assertions, feature = "field-rep-assert"))] @@ -595,11 +591,7 @@ pub(crate) unsafe fn birth_fill_f64_lanes(obj: *mut ObjectHeader) { /// deprecation changes future admission, never an existing carrier's body fact. /// Collector traversal may precede closure-slot rewriting, so SPECIAL checks /// resolve validated forwarding before examining closure payload metadata. -#[cfg(any( - debug_assertions, - feature = "field-rep-assert", - feature = "gc-instruments" -))] +#[cfg(any(debug_assertions, feature = "field-rep-assert", perry_gc_instruments))] pub(crate) unsafe fn assert_field_rep_lanes( obj: *const ObjectHeader, record: Option, @@ -634,11 +626,7 @@ pub(crate) unsafe fn assert_field_rep_lanes( /// Also used at the existing cold method-prime refusal: an unchecked store /// must be diagnosed even if no collection follows before generic dispatch. -#[cfg(any( - debug_assertions, - feature = "field-rep-assert", - feature = "gc-instruments" -))] +#[cfg(any(debug_assertions, feature = "field-rep-assert", perry_gc_instruments))] #[cold] #[inline(never)] pub(crate) unsafe fn assert_constfn_slot_body( diff --git a/crates/perry-runtime/src/object/gc_slots.rs b/crates/perry-runtime/src/object/gc_slots.rs index 15372af550..0b07f01dbd 100644 --- a/crates/perry-runtime/src/object/gc_slots.rs +++ b/crates/perry-runtime/src/object/gc_slots.rs @@ -60,11 +60,7 @@ pub(crate) unsafe fn gc_field_slot_range( if field_count > 1_000_000 { return None; } - #[cfg(any( - debug_assertions, - feature = "field-rep-assert", - feature = "gc-instruments" - ))] + #[cfg(any(debug_assertions, feature = "field-rep-assert", perry_gc_instruments))] if super::field_rep_store::field_rep_verify_enabled() { super::field_rep_store::assert_field_rep_lanes(obj, record, field_count); } diff --git a/crates/perry-runtime/src/object/method_site.rs b/crates/perry-runtime/src/object/method_site.rs index f597df61da..47af3e6883 100644 --- a/crates/perry-runtime/src/object/method_site.rs +++ b/crates/perry-runtime/src/object/method_site.rs @@ -652,11 +652,7 @@ unsafe fn prime(slot: *mut MethodSiteSlot, recv: f64, name: &[u8], argc: usize) if body != Some(info as *const crate::closure::JsFunctionInfo as u64) || slot_word & METHOD_SITE_SPILL != 0 { - #[cfg(any( - debug_assertions, - feature = "field-rep-assert", - feature = "gc-instruments" - ))] + #[cfg(any(debug_assertions, feature = "field-rep-assert", perry_gc_instruments))] if super::field_rep_store::field_rep_verify_enabled() { if let Some(record) = super::shapes::shape_record_by_id(super::shapes::object_shape_stamp(obj)) diff --git a/crates/perry-runtime/src/process/report.rs b/crates/perry-runtime/src/process/report.rs index c638375e20..16a7eaa67d 100644 --- a/crates/perry-runtime/src/process/report.rs +++ b/crates/perry-runtime/src/process/report.rs @@ -69,9 +69,9 @@ extern "C" fn process_report_function_write_report( .unwrap_or_else(process_report_default_filename); // OFF stub: unreachable in practice (the compiler enables `diagnostics` // whenever a program references `process.report`). - #[cfg(feature = "diagnostics")] + #[cfg(perry_diagnostics)] let report_json = process_report_json_string("API", Some(&filename)); - #[cfg(not(feature = "diagnostics"))] + #[cfg(not(perry_diagnostics))] let report_json = String::from("{}"); if let Err(err) = std::fs::write(&filename, report_json) { crate::fs::validate::throw_type_error_with_code( @@ -356,7 +356,7 @@ fn process_report_unix_time_ms() -> f64 { .unwrap_or(0.0) } -#[cfg(feature = "diagnostics")] +#[cfg(perry_diagnostics)] fn process_report_json_string(trigger: &str, filename: Option<&str>) -> String { let args: Vec = super::process_args_lossy().collect(); let command_line = if args.is_empty() { diff --git a/crates/perry-runtime/src/typed_feedback.rs b/crates/perry-runtime/src/typed_feedback.rs index 88ccc84736..a918271988 100644 --- a/crates/perry-runtime/src/typed_feedback.rs +++ b/crates/perry-runtime/src/typed_feedback.rs @@ -5,9 +5,9 @@ //! has actually seen at runtime. use std::collections::{BTreeMap, HashMap}; -#[cfg(any(feature = "diagnostics", test))] +#[cfg(any(perry_diagnostics, test))] use std::sync::atomic::AtomicBool; -#[cfg(any(feature = "diagnostics", test))] +#[cfg(any(perry_diagnostics, test))] use std::sync::atomic::Ordering; use std::sync::{LazyLock, Mutex}; @@ -22,7 +22,7 @@ const POLYMORPHIC_CAP: usize = 4; static REGISTRY: LazyLock> = LazyLock::new(|| Mutex::new(TypedFeedbackRegistry::default())); -#[cfg(any(feature = "diagnostics", test))] +#[cfg(any(perry_diagnostics, test))] static TRACE_DUMPED: AtomicBool = AtomicBool::new(false); #[cfg(not(test))] @@ -420,7 +420,7 @@ fn registry() -> crate::gc::NonCollectingRootRegistryGuard<'static, TypedFeedbac /// already compile-gated. Now it produces nothing, which is the same amount of /// information and looks far more like success. The trace dump uses this to say /// so out loud rather than writing an empty file. -#[cfg(feature = "diagnostics")] +#[cfg(perry_diagnostics)] pub(crate) fn no_sites_were_instrumented() -> bool { registry().sites.is_empty() } @@ -1169,7 +1169,7 @@ pub use guards::{ #[path = "typed_feedback/trace.rs"] mod trace; pub use trace::typed_feedback_snapshot; -#[cfg(feature = "diagnostics")] +#[cfg(perry_diagnostics)] pub use trace::{js_typed_feedback_maybe_dump_trace, typed_feedback_trace_json}; fn hash_bytes(bytes: &[u8]) -> u64 { diff --git a/crates/perry-runtime/src/typed_feedback/trace.rs b/crates/perry-runtime/src/typed_feedback/trace.rs index c6b5019ce2..1b3baba6f3 100644 --- a/crates/perry-runtime/src/typed_feedback/trace.rs +++ b/crates/perry-runtime/src/typed_feedback/trace.rs @@ -1,4 +1,4 @@ -#[cfg(feature = "diagnostics")] +#[cfg(perry_diagnostics)] use std::path::{Path, PathBuf}; use super::*; @@ -226,7 +226,7 @@ pub fn typed_feedback_snapshot() -> TypedFeedbackSnapshot { snapshot } -#[cfg(feature = "diagnostics")] +#[cfg(perry_diagnostics)] pub fn typed_feedback_trace_json() -> serde_json::Value { let snapshot = typed_feedback_snapshot(); serde_json::json!({ @@ -285,7 +285,7 @@ pub fn typed_feedback_trace_json() -> serde_json::Value { }) } -#[cfg(feature = "diagnostics")] +#[cfg(perry_diagnostics)] fn typed_feedback_trace_path_from_env() -> Option { let value = std::env::var("PERRY_TYPED_FEEDBACK_TRACE").ok()?; if value.is_empty() || value == "0" { @@ -298,7 +298,7 @@ fn typed_feedback_trace_path_from_env() -> Option { } } -#[cfg(feature = "diagnostics")] +#[cfg(perry_diagnostics)] fn ensure_parent_dir(path: &Path) -> std::io::Result<()> { if let Some(parent) = path.parent() { if !parent.as_os_str().is_empty() { @@ -317,7 +317,7 @@ fn ensure_parent_dir(path: &Path) -> std::io::Result<()> { // binaries). #[no_mangle] pub extern "C" fn js_typed_feedback_maybe_dump_trace() { - #[cfg(feature = "diagnostics")] + #[cfg(perry_diagnostics)] { let Some(path) = typed_feedback_trace_path_from_env() else { return; diff --git a/crates/perry/src/commands/compile/optimized_libs/driver.rs b/crates/perry/src/commands/compile/optimized_libs/driver.rs index 13d5512541..c49193068a 100644 --- a/crates/perry/src/commands/compile/optimized_libs/driver.rs +++ b/crates/perry/src/commands/compile/optimized_libs/driver.rs @@ -567,6 +567,23 @@ pub(crate) fn build_optimized_libs( let workspace_root = match find_perry_workspace_root() { Some(p) => p, None => { + // Release packages build their runtime archives with + // PERRY_RELEASE_STRIP_INSTRUMENTS=1, so a prebuilt link cannot + // honor an instrument request, and the knob then refuses at + // startup with advice to recompile. Say here why recompiling + // alone does not help. Not verbose-gated: it answers a request + // the user just made. + if matches!(format, OutputFormat::Text) + && (super::freshness::gc_instruments_requested() + || super::freshness::hot_diag_requested()) + { + eprintln!( + " note: GC instruments / hot-path diagnostics were requested, but \ + Perry workspace source was not found, so the prebuilt runtime is \ + linked; release builds of it carry no instruments. Set \ + PERRY_WORKSPACE_ROOT to a perry source checkout to build them in." + ); + } if super::prebuilt_core::eligible(ctx, cli_features) { if let Some(runtime) = super::super::library_search::find_runtime_core_library(target) diff --git a/scripts/gc_runtime_root_holders.json b/scripts/gc_runtime_root_holders.json index 20d52deb54..d08a319a59 100644 --- a/scripts/gc_runtime_root_holders.json +++ b/scripts/gc_runtime_root_holders.json @@ -343,7 +343,7 @@ "file": "crates/perry-runtime/src/gc/census.rs", "name": "PASS1_MARKED", "verdict": "non_moving_snapshot", - "why": "Real GC header addresses, deliberately untraced so the diagnostic does not keep its observed objects alive. Populated only at the end of mark propagation of a synchronous full cycle; consumed at sweep entry in the same run_to_completion invocation. The intervening full-cycle phases do not relocate or run JS callbacks. The Vec is used for membership comparisons and dropped with the census before sweep. Budgeted and minor cycles skip both boundaries. Pin re-audited 2026-09-05 after #9760 touched `gc/mod.rs`: that change is `mod heap_stats;` plus a `pub(crate) use` re-export and alters no mark/sweep control flow. `heap_stats()` is reached only from `js_bun_jsc_heap_stats` (the JS-facing `bun:jsc.heapStats()`), i.e. from mutator code, never inside a cycle, and its own module contract forbids allocation or collection during its walk. The mark-complete \u2192 sweep-entry window is unchanged. Re-audited 2026-09-05 (train125) after #9769 and #9771 touched pinned files. #9769 adds one `reg_scanner!` registration to `gc/mod.rs`; #9771 adds a feature-gated `alloc_census_init()` there and a feature-gated Rust-heap dump inside `take_census`. `alloc-census` is not in the default feature set, and decisively: `census_take_if_armed_at_full_sweep_start` does `PASS1_MARKED.with(|p| p.borrow_mut().take())` BEFORE calling `take_census`, so the snapshot has already left the thread-local by the time #9771's code runs \u2014 it cannot affect the window. Neither change alters mark/sweep control flow. Re-audited 2026-09-06 after #9831 touched `gc/policy.rs`. Its hunks are (a) the tiny-parse pressure guard's pricing (`tiny_parse_pressure_headroom_bytes`, `tiny_parse_pressure_due*`, a `Cell` byte-count base) consulted from JSON.parse's mutator-side boundaries (`gc_bump_malloc_trigger`, `gc_collect_pending_suppressed_parse`, `gc_schedule_parse_boundary_collection_if_pressure`), none of which is reachable from inside a cycle, and (b) one extra `Cell` store in `note_collection_finished_arena_occupancy`, which runs from `publish_reclaim_outcome` in the Publish subphase \u2014 after `step_sweep` has already consumed the snapshot. Mark/sweep control flow between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start` is untouched. Re-audited 2026-09-05 (train126) after #9755 restructured `gc/cycle.rs`. Its hunks are all root-scan machinery (`RootScanSubphase`, `RootScanCycleState`, the mutable-scanner iteration state), which runs BEFORE mark propagation completes; `gc/mod.rs` gains only a `mod young_log;` declaration. The bracketing is unchanged \u2014 `census_pass1_if_armed` is still inside `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` inside `step_sweep` \u2014 and a synchronous full mark-sweep still moves nothing between them. Re-pinned 2026-09-05 for the #9740 hot-TLS conversion of this file: the sole change is `thread_local!` \u2192 `crate::perry_thread_local!`, a macro-name swap with identical declaration syntax and `.with()` call sites. No control flow, no phase boundary, and no storage semantics change. Re-audited 2026-09-06 (train128) after #9794's GC diagnostics touched `gc/mod.rs` and `gc/policy.rs`: both gain diagnostic module declarations and counters only \u2014 no mark/sweep control flow, and the census bracketing in `step_mark_propagation` / `step_sweep` is unchanged. Re-audited for #9794's GC diagnostics: `gc/mod.rs` gains `mod diag_sites;` / `mod survival_diag;`, a re-export, a `diag_sites::full_started(...)` call at TRIGGER time (before mark propagation begins), and exit-time reporting. Nothing executes between mark-complete and sweep-entry, so the window is unchanged. Re-audited 2026-09-06 for the retained array-growth verifier fix: the cycle.rs change passes the existing non-copying evacuation verifier an explicit all-forwarded policy. That call remains in minor finalization, outside the synchronous full-cycle census window; its root and heap reads do not allocate GC objects, move objects, or invoke JS callbacks. The mark-complete and sweep-entry boundaries are unchanged. Re-audited 2026-09-05 after #9830 touched `gc/policy.rs`. That change is (a) six `thread_local! {` blocks rewritten as `crate::perry_thread_local! {` and (b) one `#[cfg(test)]` accessor listing the trigger path's hot-slot indices. The macro keeps the same storage, the same `.with()` at every read and write, and the same destructor registration (the teardown guard exists exactly when `needs_drop` holds, which is what `std::thread_local!` already decided); no value, predicate or branch in the file changes, so no mark or sweep control flow does. The one new behaviour is on a declaration's FIRST read: `HotKey::resolve_and_cache` takes a mutex and allocates a key through the GLOBAL allocator. Even if a first read landed inside this window it would be sound \u2014 the window's contract is that nothing relocates and no JS callback runs, and a mimalloc allocation does neither. `census_pass1_if_armed` is still inside `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` inside `step_sweep`; the bracketing is untouched. Re-audited 2026-09-06 (train132) after #9860 and #9845 touched `gc/mod.rs`. Both hunks are re-export lists and nothing else: #9860 adds `idle_reclaim_elapsed_starts` / `IDLE_RECLAIM_REARM_MS`, and #9845 adds `owner_is_dead_copied_minor_from_space_of_type`. No mark or sweep control flow changes. #9845's substantive work sits in `gc/oldgen.rs` and `gc/copying.rs`, neither pinned: the copying-minor arm (`finalize_dead_copied_minor_from_space_regexps`) runs on a MINOR, which skips both census boundaries; the full-cycle arm (`collect_dead_registered_regexps_post_trace`, from `with_dead_collection_finalize`) walks the RegExp registry building a Vec of addresses \u2014 no GC allocation, no JS callback, so it cannot relocate the snapshot's subjects \u2014 and it is reached from the sweep body, i.e. AFTER `census_take_if_armed_at_full_sweep_start` has already `take()`n the snapshot out of the thread-local. The mark-complete -> sweep-entry window is unchanged. Re-audited 2026-09-07 for #9965 after 1ec9e0e8a touched `gc/cycle.rs` and `gc/mod.rs`: `gc/mod.rs:216-217` only declares and imports the failure-attribution module, while `gc/cycle.rs:1414-1417` reads the trigger and diagnostic counters immediately before evacuation verification inside `atomic_finalize_minor_prelude`. Full cycles bypass `MinorPrelude` at `gc/cycle.rs:1192-1196`; evacuation remains guarded by the minor-only context at `gc/cycle.rs:1330-1372`. The snapshot store remains at `gc/cycle.rs:963-964` after synchronous full marking, and its take remains at `gc/cycle.rs:1454-1457` before sweep. No new write, relocation, collection, or JS callback was added to that full-cycle interval, so the PASS1_MARKED window is unaffected. Re-audited 2026-09-07 for the regex census rows: all new work is in `take_census` after `census_take_if_armed_at_full_sweep_start` has taken PASS1_MARKED out of TLS; neither boundary nor the intervening cycle control flow changed. Re-audited 2026-09-08 (train144) after #9976 and #9977 touched pinned files. `gc/mod.rs` gains exactly three lines: `mod copying_phase;` and `mod regex_census;` (declarations) and one `reg_scanner!(regex::site_test::scan_roots_mut)` registration. A scanner registration adds a root SOURCE for the mutable-root walks; it does not move either census boundary and runs nowhere between them. `gc/census.rs` widens `side_tables()` to `pub(super)`, extends it with regex rows and adds a test module \u2014 all census REPORTING, which runs from the diagnostic dump, not inside a cycle. Mark/sweep control flow between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start` is untouched. Re-audited 2026-09-08 for #9849 JSON construction deferral. `gc/mod.rs` adds the `json_defer` module/re-export and a trusted-header layout helper used only by already-validated JSON emitters; neither changes or runs in collector phase control flow. `gc/policy.rs` adds JSON completion scheduling, construction-grace checks, and safepoint deferral predicates. These are called from mutator-side JSON allocation/output boundaries and ordinary safepoint entry; they do not alter `step_mark_propagation`, `step_sweep`, or invoke callbacks or relocation between the census boundaries. The mark-complete to sweep-entry window is unchanged. The follow-up adds a cfg(test)-only one-shot boolean for deterministic explicit-pressure fixtures; it is absent from production builds and cannot affect the census window. The first predicate read consumes it, so post-parse accounting exercises normal pricing. Re-audited 2026-09-09 for bounded tiny-JSON completion polling. The policy.rs changes split the mutator-side pending-parse check into an inlined empty fast path plus an outlined debt-service path, and amortize the mutator-side arena-pressure read across 64 bounded parse completions. Neither function is reachable from step_mark_propagation or step_sweep; neither census boundary nor the synchronous full-cycle interval between them changes. Re-audited 2026-09-09 for lazy JSON record batches: policy.rs only widens gc_budgeted_cycle_active visibility from pub(super) to pub(crate). Its body remains a read-only Cell query. The new caller is lazy_get materialization in the mutator; run_to_completion, step_mark_propagation, census snapshot consumption at step_sweep, and the synchronous non-moving window are unchanged. Re-audited 2026-09-09 for completed JSON-output debt: the added gc_service_json_output_sweep function calls the existing trigger check from a rooted mutator boundary and reports whether its malloc-count request remains due. It is not called from any census or collector phase; the synchronous mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-09 for the JSON byte-debt carry: the same mutator-only service helper now distinguishes requests satisfied before its call from those satisfied by its trigger check. The added enum contains no payload, both count reads are scalar, and no census boundary or collector phase changed. Re-audited 2026-09-11 for #10055: gc/mod.rs only registers the weak UTF-16 index scanner during gc_init. It neither marks strings nor allocates GC objects or runs JS; offset vectors use the Rust allocator. The mark-complete to sweep-entry census window and cycle control flow are unchanged. Re-audited 2026-09-11 for #10054: gc/mod.rs adds only the trim-cache mutable-root scanner registration in gc_init. Its scanner visits two existing string slots without allocating or invoking JS. Root scanning still precedes mark completion, and neither census boundary nor the synchronous mark-complete to sweep-entry window changes. Re-audited 2026-09-11 for #10060: the census array classifier now reads the logical element start and bounds its scan by the remaining capacity. The helper only reads the existing GC/header words and performs pointer arithmetic; it cannot allocate, collect, or call JS. This classifier runs in take_census after PASS1_MARKED has been taken out of TLS. Neither census boundary nor the mark-complete to sweep-entry control flow changed. Re-audited for #8512: gc/mod.rs only enables the existing PTY mutable-root scanner on Windows; it changes no mark/sweep phase or census boundary. The scanner visits NaN-boxed slots without running JS callbacks. Re-audited 2026-09-12 for the single regular-expression engine: `gc/mod.rs` changes `mod prefetch;` to `pub(crate) mod prefetch;` so the RegExp owner-table walks can prefetch headers, a visibility change with no new call in collector control flow; `gc/census.rs` changes only its `#[cfg(test)]` `regex_census_tests` module, dropping assertions for the previous engine's cache rows. Neither boundary (`census_pass1_if_armed` in `step_mark_propagation`, `census_take_if_armed_at_full_sweep_start` in `step_sweep`) nor the synchronous mark-complete to sweep-entry interval changes. Re-audited 2026-09-13 after the #10169 fix touched `gc/mod.rs` and `gc/policy.rs`. `gc/mod.rs` gains only `pub(crate) use` re-exports (`policy::note_young_leaf_born_old`, `policy::young_generation_holds_a_nursery`, `promote_in_place::{young_generation_measured_dying, young_generation_measured_retained}`, and cfg(test) survival seeders). `gc/policy.rs` gains a `Cell` thread-local (`GC_YOUNG_LEAF_BORN_OLD`, no pointer), its setter, a pure predicate over `copying_from_space_in_use_bytes` vs the base nursery cap, and a consumed-once branch at the top of `gc_budgeted_due_trigger` that may answer `YoungScavengeCap` ahead of `OldReclaim`. That branch decides WHICH collection a safepoint starts (a minor instead of a full); it runs before any cycle begins and never inside one, so the mark-complete \u2192 sweep-entry window of a synchronous full \u2014 where PASS1_MARKED is populated and consumed within one `run_to_completion` \u2014 is unchanged, and neither hunk adds an allocation, a JS callback, or a relocation to it. Re-audited 2026-09-13 for the heap generation (#10164 cross-call search positions): `gc/mod.rs` only declares `pub(crate) mod heap_generation;`. `gc/cycle.rs` wraps the `Sweep` and `Reclaim` arms of `GcCycleState::step` in a `HeapChange` scope and opens one inside `atomic_finalize_minor_prelude`'s evacuation branch (with a nested one around old-page defrag). Opening and closing a scope only increments two thread-local integer cells (`HEAP_GENERATION`, `OPEN_HEAP_CHANGES`); a first thread-local read may allocate a key through the global allocator, which neither relocates nor runs JS. The `Sweep` scope opens immediately before `step_sweep`, i.e. before `census_take_if_armed_at_full_sweep_start` takes PASS1_MARKED out of TLS, and adds no relocation, collection or JS callback to the synchronous mark-complete to sweep-entry window; the minor-prelude scope is unreachable from a full cycle, which bypasses `MinorPrelude`. Neither boundary nor the intervening control flow changed. Re-audited 2026-09-13 for #10182 block-granular reclamation, which touched `gc/cycle.rs`. Two hunks: (a) in the `RememberedSetRebuild` subphase of AtomicFinalize \u2014 INSIDE the window \u2014 the require-marked old-to-young rebuild is now constructed with `OldToYoungRememberedRebuildState::new_skipping`, whose cursor never enters blocks the census recorded as holding no reached, pinned or pre-marked object (`BlockCensus::unmarked_blocks`); computing that list reads `arena_block_snapshots()` and allocates one `Vec` through the global allocator. It visits a subset of the same objects the rebuild already walked (every skipped object would have been rejected as unmarked), and it neither allocates a GC object, relocates anything, nor runs a JS callback. (b) In `step_sweep`, `IncrementalSweepState::with_block_skip` runs after `census_take_if_armed_at_full_sweep_start` has already taken PASS1_MARKED out of TLS. Neither boundary moved and the synchronous mark-complete to sweep-entry interval gains no relocation, collection or callback. Re-audited 2026-09-11 for the startup memory profile: gc/mod.rs only retains the pre-main allocator-policy constructor in js_gc_init. The constructor applies process allocation options, without invoking GC or JS. No census boundary, collector phase, or mark-complete to sweep-entry control flow changed. Re-audited 2026-09-13 for #10179: census.rs only adds a native regex cache metadata row and its unit assertion; snapshot consumption and the full-cycle window are unchanged. Re-audited 2026-09-14 for the GC due-check fast path, which touched `gc/mod.rs` and `gc/policy.rs`. `gc/mod.rs` only changes the safepoint re-exports: `gc_runtime_safepoint` becomes cfg(test) and `gc_runtime_safepoint_poll` is added. `gc/policy.rs`: the budgeted step returns a debt-free `GcStepReport` (debt is attached by the FFI and test entry points after the step returns) and moves cycle start/step into an out-of-line `gc_budgeted_start_or_step`; `gc_check_trigger` reuses a repeatable due-trigger answer through `DueTriggerMemo`, placed after its `GC_FLAG_IN_ALLOC` and suppression early returns; the young scavenge cap reuses the old-gen pressure value the due trigger already read and checks the census-seeded flag first. All of it runs from mutator safepoints, allocation-point trigger checks and the host step API, before a cycle starts or between budgeted steps. None of it is reachable between `census_pass1_if_armed` in `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` in `step_sweep` of a synchronous full: an allocation inside that window reaches `gc_check_trigger` with `GC_FLAG_IN_ALLOC` set and returns before the changed code. No allocation, relocation, collection or JS callback is added to the window. Re-audited 2026-09-14 for the tiny-parse nursery-cap boundary, which touched `gc/policy.rs`. It adds `tiny_parse_generational_collection_due`, a pure predicate (the existing `tiny_parse_pressure_due` OR the existing `young_scavenge_cap_due` read), and calls it instead of `tiny_parse_pressure_due` from `gc_bump_malloc_trigger_inner` and `gc_collect_pending_suppressed_parse_slow` (generational branch only) and from `gc_schedule_parse_boundary_collection_if_pressure`. All three are JSON.parse mutator-side boundaries, none reachable from `step_mark_propagation` or `step_sweep`; the predicate reads counters and allocates nothing. Neither census boundary nor the synchronous mark-complete to sweep-entry interval changed. Re-audited 2026-09-13 for #10182's full-collection throughput follow-up, which touched `gc/cycle.rs` in one hunk, INSIDE the window: the `RememberedSetRebuild` subphase of a synchronous full now first asks `verify::full_remembered_rebuild_provably_empty` and, when it holds, installs `OldToYoungRememberedRebuildState::provably_empty()` (an empty sticky set, no walk) instead of the require-marked rebuild. The predicate reads `arena_block_snapshots()` (one `Vec` through the global allocator), the census's per-block reached/pre-marked facts and the malloc registry's length; the constructor bumps a `Cell` counter and prints one line under `PERRY_GC_DIAG`. None of it allocates a GC object, relocates anything, collects, or runs a JS callback, and both census boundaries stay where they were. Re-audited 2026-09-14 for #10182's pacing-full work, which touched `gc/cycle.rs`, `gc/mod.rs` and `gc/policy.rs`. `gc/cycle.rs`: `GcCycleState::new_full` no longer calls `materialize_all_promoted_page_runs`; that call ran in the constructor, before the census and far before `census_pass1_if_armed`, and removing it adds nothing to the window. `gc/mod.rs`: one `mod promoted_cohort;` declaration. `gc/policy.rs`: (a) `credit_promoted_bytes_to_old_baseline` also credits a `Cell` cohort counter (it runs after a copying minor completes); (b) `finish_full_old_reclaim_baseline` also records the verified old live bytes and resets that counter (Publish, after `step_sweep` consumed the snapshot); (c) `gc_safepoint_moving_minor` arms and disarms the promotion-census record around its nursery minor and calls `run_promoted_cohort_full_if_due`, which starts a synchronous full through the same `gc_collect_full_mark_sweep_with_trigger` entry and reads byte counters before and after it. All of it runs before a cycle starts or after it completes; none of it runs between mark completion and sweep entry, allocates a GC object, relocates anything, or calls into JS. The census the promoted-cohort full may adopt from the promotion walk is built in `BuildValidPointerSet`, before either boundary. Both boundaries are unchanged. Re-audited 2026-09-14 for the #10182 dead-stack scrub in `gc/cycle.rs`: `step_build_valid_pointer_set` now calls `scrub_dead_stack_below`, which zeroes a local array in its own frame (dead stack below the caller), right after the census finishes \u2014 in `BuildValidPointerSet`, before the root scan and far before `census_pass1_if_armed`. It writes no heap memory, allocates nothing, relocates nothing and calls no JS; both boundaries are unchanged. Re-audited 2026-09-14 for #10241 (cohort survival), which touched `gc/cycle.rs` and `gc/policy.rs`. `gc/cycle.rs`: one call, `promoted_cohort::survival::check_minor_view_at_full_sweep_start()`, in `step_sweep` immediately AFTER `census_take_if_armed_at_full_sweep_start` has taken PASS1_MARKED out of TLS, i.e. outside the window. It is a no-op unless a promoted-cohort full armed its survival probe; when armed it walks the old page index over the preceding minor's dirty pages (`old_arena_walk_objects_on_pages`, Rust-allocator Vecs), reads GC headers' mark flags and the slots of unmarked ones, and records one enum. It writes no heap memory, allocates no GC object, relocates nothing and calls no JS. `gc/policy.rs`: `run_promoted_cohort_full_if_due` arms the probe before `gc_collect_full_mark_sweep_with_trigger` and takes it after the full returns (feeding `note_full_measured_promotion_survival` and one diagnostic line); both run before a cycle starts or after it completes. Both boundaries are unchanged. Re-audited 2026-09-14 for #10241's in-place-only cohort: `gc/policy.rs` drops the `promoted_cohort::note_promoted` call from `credit_promoted_bytes_to_old_baseline` (the copying minor now calls `promoted_cohort::note_minor_promotion` itself, after the credit). Both run at the end of a copying minor, outside any full cycle; the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-14 for the parse-boundary side-allocation band (medium-parse pacing), which touched `gc/policy.rs`. Three hunks: (a) a `Cell` thread-local (`GC_LAST_COLLECTION_EXTERNAL_SIDE_BYTES`, a byte COUNT, no pointer) plus three pure predicates over it and `external_side_live_bytes()`; (b) that predicate added as a third disjunct of `tiny_parse_generational_collection_due`, which is read only from the three JSON.parse mutator-side boundaries (`gc_bump_malloc_trigger_inner`, `gc_collect_pending_suppressed_parse_slow`, `gc_schedule_parse_boundary_collection_if_pressure`), none of them reachable from `step_mark_propagation` or `step_sweep`; and (c) one extra `Cell` store in `note_collection_finished_arena_occupancy` plus two extra reads in the `PERRY_GC_DIAG` tiny-parse line. `note_collection_finished_arena_occupancy` runs from `publish_reclaim_outcome` in the Publish subphase, i.e. AFTER `step_sweep` has already `take()`n the snapshot out of the thread-local, exactly as #9831's store on the same line does. Nothing added allocates a GC object, relocates anything, or runs a JS callback, and neither census boundary moved. Re-audited 2026-09-14 for the drained-bytes counterweight to that band, which touched `gc/policy.rs` again. Four hunks: a second `Cell` thread-local (`GC_EXTERNAL_SIDE_DRAINED_SINCE_FULL`, a byte COUNT); one increment of it inside `gc_note_external_side_free`; a pure read (`external_side_old_reclaim_pressure_bytes`) substituted for `external_side_live_bytes()` at the four old-reclaim pressure sites; and one `Cell` store at the top of `finish_full_old_reclaim_baseline`. None of it can run between the census boundaries. `gc_note_external_side_free` is also reached by mutator-side tape materialization, regex scratch teardown, native-addon adjustments and buffer replacement. Its added operation is only a saturating increment of a scalar Cell, with no GC allocation, relocation, collection or JS callback, so this wider caller set does not invalidate the census window. `finish_full_old_reclaim_baseline` runs from `publish_reclaim_outcome` in the Publish subphase, the same place #9831's store already sits. The pressure reads happen at trigger decisions, before a cycle starts. No allocation, relocation, collection or JS callback is added to the mark-complete -> sweep-entry window, and neither boundary moved. Re-audited 2026-09-15 for turnloop P0, which touched `gc/mod.rs` with one added call: `crate::event_pump::shutdown_wait_driver()` inside `js_gc_release_current_thread_collection_side_allocations`, the process-exit funnel. That function runs once no more JavaScript can run on the thread, never from inside a collection cycle; the added call drops the thread's turnloop wait loop (closing its kqueue/epoll descriptor) and may print a diagnostic line. It allocates no GC object, relocates nothing, starts no collection and runs no JS callback. The census boundaries and the mark-complete -> sweep-entry window are untouched.. Re-audited 2026-09-16 for the copying minor's per-parent weak-holder fact: `gc/mod.rs` gains exactly one line, `mod copying_parent_facts;`, a module declaration. The module it declares holds `weak_holder_fact` (a read of the parent's `obj_type`/`class_id` via `weakref::is_weak_holder_header`) and the copying minor's `visit_slot_with_parent`, moved verbatim out of `gc/copying.rs` for the 2000-line lint. Both run only inside a COPYING MINOR, which skips both census boundaries (`census_pass1_if_armed` in `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` in `step_sweep` are synchronous-full only). Nothing was added to any full-cycle phase, and the declaration itself executes no code. Neither boundary moved and the synchronous mark-complete to sweep-entry window gains no allocation, relocation, collection or JS callback. Re-audited 2026-09-18 for the #10532 follow-up argument-list rooting fix, which touched `gc/mod.rs`. The only change there is `mod collection_points;` plus a `pub(crate) use collection_points::collection_point;` re-export (and, under `#[cfg(test)]`, `arm_collection_point`). `collection_point` is an inline no-op outside `cfg(test)`; under test it only runs a copying minor when called from ordinary MUTATOR code (`proxy.rs`'s `Reflect.apply` rebind path and `registry.rs`'s rest-array bundler), never from inside `step_mark_propagation` or `step_sweep`. Neither `census_pass1_if_armed` nor `census_take_if_armed_at_full_sweep_start` is reachable from it, so the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-18 (same PR, round 2) for the added `arm_collection_point_after` re-export in `gc/mod.rs`: another pure re-export line, same as the `collection_point`/`arm_collection_point` one already covered above. `arm_collection_point_after` only changes test-only arming state in `collection_points.rs` (which named site fires and on which hit); it still runs no mark/sweep control flow. Re-audited 2026-09-19 for #10735 (require.main threading): gc/mod.rs gains exactly one line, `reg_scanner!(crate::module_require::scan_cjs_main_module_root_mut);`, registering the new CJS_MAIN_MODULE thread-local's mutable-root scanner beside the existing `scan_module_path_roots_mut` registration. A scanner registration adds a root SOURCE for the mutable-root walks; it runs during root scanning, before mark propagation completes, and does not execute between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start`. Neither census boundary moved and the synchronous mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-20 for #10834 (inherited-property read cache). `gc/mod.rs` gains exactly one line: `reg_scanner!(crate::object::inherited_read_cache::scan_inherited_read_cache_roots_mut);` in `gc_init()`. A scanner registration adds a root SOURCE for the mutable-root walks. The walk runs inside `RootScanCycleState::step_current_subphase`, i.e. entirely within the RootScan phase: `step_root_scan` only sets `self.phase = GcCyclePhase::MarkPropagation` once that loop reports done (`gc/cycle.rs:958-961`), and `census_pass1_if_armed()` fires at the END of `step_mark_propagation` (`gc/cycle.rs:982`). The scanner therefore runs strictly BEFORE the window opens and can never execute between the boundaries. Its body is a bounded walk of a fixed 512-entry thread-local array calling `visit_tagged_usize_slot` / `visit_usize_slot`; it allocates nothing, relocates nothing and runs no JS callback. Same shape as #9769, #9976/#9977, #10054, #10055 and #10735, all previously cleared. The PR also adds an `INHERITED_READ_CACHE` entry to `DEAD_KEY_PRUNES` in `gc/dead_owner.rs` (not a pinned source). That registry is consumed by `IncrementalSweepState::with_dead_collection_finalize` at `gc/cycle.rs:1548`, which is AFTER `census_take_if_armed_at_full_sweep_start` at `gc/cycle.rs:1505` has already `take()`n the snapshot out of the thread-local -- the same argument that cleared #9845's `collect_dead_registered_regexps_post_trace`. The prune reads addresses and zeroes entries; no GC allocation, relocation or callback. Both additions sit outside the window, on opposite sides of it. Neither boundary moved and the synchronous mark-complete to sweep-entry interval is unchanged. Re-audited 2026-09-22 for #10399 (per-thread module init), which touched `gc/mod.rs`. Two hunks, both init-time: a new free function `raise_default_thread_stack_floor()` and one call to it at the top of `js_gc_init`, before `enter_current_thread_image`'s successor statements. The function reads `RUST_MIN_STACK` from the environment and, only when it is unset, sets it to 32 MiB so a thread spawned against a multi-megabyte static TLS block still has usable stack (glibc carves static TLS out of the thread's stack mapping). It touches no heap object, allocates no GC object, relocates nothing and runs no JS callback. `js_gc_init` is the first runtime call of a compiled `main`, so it runs once before any cycle exists, and it is not reachable from `step_mark_propagation` or `step_sweep`. Same shape as the 2026-09-11 startup-memory-profile re-audit, which cleared the pre-main allocator-policy constructor in the same function. Neither census boundary moved and the synchronous mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-23 (size/runtime-decouple, #11135) after the binary-size branch touched `gc/census.rs`, `gc/mod.rs` and `gc/policy.rs`. census.rs: `census_pass1_if_armed` / `census_take_if_armed_at_full_sweep_start` keep their bodies verbatim, moved into `_impl` functions compiled only with the new `gc-instruments` feature (without it both are empty and `census_path()` is `None`, so nothing is ever armed); the take still empties PASS1_MARKED before `take_census`. gc/mod.rs: `gc_init` gains a startup env check that aborts when an instrument knob is set without the feature, before any cycle exists. gc/policy.rs: env-knob OnceLock caches now initialize through `crate::once_init::get_or_init` (same closures, same values). No mark/sweep control flow between the two census boundaries changed; the window is unchanged. Re-audited for Fetch handle reclamation: cycle.rs only redirects the incomplete-cycle Drop cancellation hook to also cancel the Fetch trace. The full-trace finish hook removes native records and cached slots without allocating GC objects or invoking JS; it cannot relocate the census addresses before sweep entry. Re-audited 2026-09-22 for #10928 (one proportional old-reclaim rule), which touched `gc/policy.rs`. Six hunks. (a) Two new thread-locals, `GC_OLD_RECLAIM_PRE_IN_USE_BYTES` (`Cell`) and `GC_OLD_RECLAIM_BACKOFF_SHIFT` (`Cell`): both are byte/shift COUNTS, neither holds a pointer. (b) `gc_old_reclaim_growth_band_bytes` gains a `Cell` read and a left shift -- pure arithmetic over byte counts. (c) `old_reclaim_pressure_due` loses the #7937 absolute first-crossing arm, splits its pure form out as `old_reclaim_pressure_due_inner`, and calls `note_old_reclaim_cycle_started()` when the answer is true. That predicate is read at TRIGGER decisions only -- the allocation-point `gc_check_trigger` and `gc_budgeted_due_trigger` at safepoints -- i.e. before a cycle starts, never between the boundaries; an allocation inside the window reaches `gc_check_trigger` with `GC_FLAG_IN_ALLOC` set and returns before this code, the same argument the 2026-09-14 due-check fast-path re-audit made for the same function. Even if it did run there it would be sound: `note_old_reclaim_cycle_started` stores one scalar `Cell` from `pacing_arena_in_use_bytes()` (a read of `arena_live_allocated_bytes`), which allocates no GC object, relocates nothing and runs no JS callback -- the window's contract. (d) `update_old_reclaim_backoff` is called only from `finish_full_old_reclaim_baseline`, which runs from `publish_reclaim_outcome` in the Publish subphase, AFTER `step_sweep` has already `take()`n the snapshot out of the thread-local -- exactly where #9831's store and the medium-parse pacing store already sit. (e) `gc_old_reclaim_debt_bytes` drops the absolute arm it mirrored; it remains pure arithmetic read at debt/trigger decisions. (f) `#[cfg(test)]` seams, absent from production builds. `census_pass1_if_armed` is still inside `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` inside `step_sweep`. Nothing added allocates a GC object, relocates anything, collects, or invokes a JS callback between them; the change alters only WHEN a collection is scheduled, never what runs inside one. Neither boundary moved and the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-24 for #10960 (growth-aware old-reclaim backoff), which touched `gc/policy.rs` again. One new thread-local, `GC_OLD_RECLAIM_LAST_POST_IN_USE_BYTES` (`Cell`), a byte COUNT that holds no pointer. It is written only by `update_old_reclaim_backoff`, which runs from `finish_full_old_reclaim_baseline` in the Publish subphase, after `step_sweep` has already taken the snapshot out of the thread-local; the change there is pure integer arithmetic deciding whether to widen the band. Nothing added allocates a GC object, relocates anything, collects, or invokes a JS callback, and neither window boundary moved. Re-audited 2026-09-24 after the class-capture environment added one `reg_scanner!` registration (`scan_class_env_roots_mut`, visiting each guarded class environment's owner class object) to `gc/mod.rs`: a root-scanner registration alters no mark/sweep control flow and runs nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-25 after the inherited-access lane touched `gc/mod.rs`: the change is one `reg_scanner!` registration for `object::chain_store::scan_chain_store_roots_mut`, a root scanner that visits store-site chain verdicts (one interned key pointer each) during root scanning. It runs at the start of a cycle, never between mark completion and sweep entry, relocates nothing and runs no JS callback. The synchronous mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-25 for #10698 (allocation-point trigger watermark), which touched `gc/mod.rs` and `gc/policy.rs`. `gc/mod.rs` gains `pub(crate) mod trigger_watermark;` and a `pub(crate) use` re-export -- no control flow. In `gc/policy.rs`: (a) `gc_check_trigger` becomes an out-of-line wrapper over an inlined fast path that reads the watermark cell, the malloc registry's length through an unguarded shared borrow and the inline allocator's offset, then either returns or runs the previous body unchanged (`gc_check_trigger_evaluate`). An allocation inside the window reaches it with `GC_FLAG_IN_ALLOC` set: the fast path returns without acting, which is the outcome of the `GC_FLAG_IN_ALLOC` early return it would otherwise reach, and the slow path still takes that early return. (b) The due-trigger evaluation also returns a watermark -- integer arithmetic over values it already read -- published only on the no-action return past every entry guard. (c) The ladder's thresholds and flags are retyped `Cell` -> `TriggerInput`, whose writes add one store retiring the watermark; heap-generation advances and `Arena::set_current` retire it too. (d) `#[cfg(test)]` seams. Nothing added allocates a GC object, relocates anything, collects, or invokes a JS callback; `census_pass1_if_armed` is still inside `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` inside `step_sweep`, so the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-25 for #10498 (class-accessor cache), which adds one `reg_scanner!` registration (`scan_class_accessor_cache_roots_mut`, marking and rewriting the cache's key strings) to `gc/mod.rs`: again a root-scanner registration, altering no mark/sweep control flow and running nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-26 for #10572: `gc/mod.rs` gains one `#[cfg(not(feature = \"hot-diag\"))]` call to `hot_diag::refuse_knobs_without_hot_diag()` in `gc_init`, a startup-only knob check that runs before any cycle and alters no mark/sweep control flow; the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-27 for #11416: `gc/mod.rs` loses the `reg_scanner!` registration for `class_accessor_cache::scan_class_accessor_cache_roots_mut` (the cache is deleted); removing a root-scanner registration alters no mark/sweep control flow and runs nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-27 for the method-calls lane, which adds one `reg_scanner!` registration (`scan_method_site_roots_mut`, marking and rewriting the method closures inherited method-site entries hold) to `gc/mod.rs`: a root-scanner registration, altering no mark/sweep control flow and running nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-28 (charter step 5, P0): the field-representation census adds `mod census_field_repr;` to `gc/mod.rs` and, in `census.rs`, one Rust-owned accumulator fed from `visit_object` inside `take_census`, which runs after `census_take_if_armed_at_full_sweep_start` has already taken PASS1_MARKED; it reads slots and the per-object layout maps (try_borrow), allocates nothing on the JS heap and alters no mark/sweep control flow. The window is unchanged. Re-audited 2026-09-27 for #11416: `gc/mod.rs` loses the `reg_scanner!` registration for `class_accessor_cache::scan_class_accessor_cache_roots_mut` (the cache is deleted); removing a root-scanner registration alters no mark/sweep control flow and runs nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-27 for the method-calls lane, which adds one `reg_scanner!` registration (`scan_method_site_roots_mut`, marking and rewriting the method closures inherited method-site entries hold) to `gc/mod.rs`: a root-scanner registration, altering no mark/sweep control flow and running nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-27 (class constructors as function objects, #11414): `gc/mod.rs` gains one `reg_scanner!` registration (`object::class_value::scan_class_value_roots_mut`, the per-agent class function-object table, visited and rewritten) \u2014 a root-scanner registration that alters no mark/sweep control flow and runs nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-29 after #11659 added `verify::verify_array_hole_tails_at_collection()` to `gc/mod.rs` and `gc/policy.rs`. Every call sits at a collection or budgeted-cycle ENTRY, beside `roots::ensure_stack_maps_built()`, i.e. before mark begins and so before `census_pass1_if_armed` opens the window. The call is a read-only walk of array headers (debug builds, or release with PERRY_GC_VERIFY_ARRAY_HOLES): it neither allocates GC memory, relocates, runs JS, nor reads or writes PASS1_MARKED, and it panics rather than continuing on a violation. Mark/sweep control flow between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start` is untouched. Pin re-audited 2026-09-29 after PR #11646 added one call inside that window, in `cycle.rs` beside `census_take_if_armed_at_full_sweep_start`: `object::shapes::store_kind::audit_heap_at_full_sweep_start()`. It compiles to nothing unless the `shape-fact-audit` feature is on; when on it walks the arena read-only (`gc::for_each_live_object_at_sweep_start`, in `gc/verify.rs`) and reads each marked object header and its shape record. It allocates no GC object, moves nothing, runs no JS callback and holds no address past the walk, so PASS1_MARKED stays valid across it; a disagreement panics rather than continuing. Re-audited 2026-09-29 after the pinned-roots fix touched `gc/cycle.rs` and `gc/mod.rs`: the block-persistence live-block predicate now reads GC_FLAG_MARKED alone (pinned objects are marked as roots, so the set of live blocks is unchanged), and `gc/mod.rs` adds one `reg_scanner!` registration. Neither relocates, runs JS, or moves the mark-complete to sweep-entry window. Re-audited again 2026-09-29 (same fix, copying-minor follow-up): `BlockPersistCycleState`'s force-mark in `gc/cycle.rs` no longer skips an unmarked pinned header, so it marks and pushes it like any other object of a live block. That is marking inside the budgeted cycle's persistence step: nothing relocates, no JS runs, and the budgeted path still skips both census boundaries. The window is unchanged. Re-audited 2026-09-29 for this-as-a-parameter stage 3: `gc/mod.rs` registers `scan_dispatch_binding_roots_mut` in place of `scan_implicit_this_roots_mut` (the implicit-`this` cell is deleted; the same scanner body keeps `new.target`, the static-`this` override and the static private-owner stack) and rewords its comment; a root-scanner registration, altering no mark/sweep control flow and running nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-28 for the JsFunctionInfo lane: `gc/census.rs`'s `side_tables()` loses its `closure_registry_census()` row (the closure-body registry is deleted); a census report row, altering no mark/sweep control flow and running nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-29 for the read-site holder entry: `gc/mod.rs` adds one `reg_scanner!` registration, `read_holder::scan_read_holder_roots_mut`, which visits the holder and hop words of registered read-site caches during ROOT SCAN, before mark propagation completes. It rewrites root slots only through the visitor (as every registered scanner does), runs no JS, and nothing it does executes between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start`. The window is unchanged. Re-audited 2026-09-30 for Step 5 P4: census.rs only removes the typed-layout count, and gc/mod.rs removes shape-install registration/re-export and an init-time typed-intact verifier gate. The snapshot remains stored after mark propagation and taken at sweep entry; none of these edits relocates objects or invokes callbacks in that interval. Re-audited 2026-09-29 (#11549 trace cost): `gc/mod.rs` gains one `mod copying_object_scan;` declaration, the copying minor's plain-object slot walk. It is reached only from the copying drain, never from a full cycle, and changes no mark or sweep control flow. The window is unchanged. Re-audited 2026-09-30 after rebasing P4 over #11549/#11676: gc/mod.rs retains the copying_object_scan declaration and P4 removes typed-layout registration; both are module wiring outside the synchronous full mark-complete to sweep-entry window. The census boundaries and the window owner in cycle.rs are unchanged. Re-audited 2026-09-30 for the scope-context change: gc/mod.rs only removes the box root-scanner registration and exit-time box statistics. The former ran during root scan, before mark completion, and the latter runs at process exit. Neither changes the synchronous mark-complete to sweep-entry window. Re-audited for A2 inherited-read-cache deletion: gc/mod.rs removes only its old root-scanner registration and adjacent comments. The method-site and read-holder scanners still run during root scan before mark completion; no collection phase or callback was added inside the mark-complete to sweep-entry window. Re-audited 2026-09-30 for the setter-site root scanner: gc/mod.rs adds only its reg_scanner! registration in the root-scan setup. That scanner visits per-site key and holder slots before mark propagation completes; it runs no JS and adds no relocation or callback between census pass1 and sweep entry. The synchronous-full snapshot window and both boundaries remain unchanged. Re-audited 2026-10-02 for the SPECIAL ConstFn verifier: gc/mod.rs adds only a feature-gated forwarding-helper re-export. The helper follows existing validated forwarding and runs no JS, allocation or collection. No holder, scanner registration or mark/sweep control flow is added; both census boundaries and their synchronous window remain unchanged. Re-audited 2026-09-28 for #11605 (link-time runtime feature installs): `gc/mod.rs` registers the interpreter root scanner through its always-present forwarder (`dyn_eval_hooks::scan_dyn_eval_roots_mut`, which calls the real scanner through a slot the `dyn-eval` install fills) instead of a feature-gated direct registration, and `js_gc_init` ends by running the program\u2019s feature installer, which only stores fn pointers into `Hook` slots at startup, before any user code or collection. Neither alters mark/sweep control flow or runs inside the mark-complete to sweep-entry window. Re-audited 2026-10-01 after main integration: the pinned census, cycle, policy and progress files are byte-identical to current main. The only gc/mod.rs delta replaces the interpreter scanner registration with its installed-slot forwarder and invokes the installer at js_gc_init startup, before user code and any collection. No change executes between mark completion and sweep entry. Re-audited 2026-10-01 while integrating main 7b5912d4e7 into #11605: census.rs, cycle.rs, policy.rs and progress.rs are byte-identical to that main. The only gc/mod.rs differences register the existing interpreter root scanner through its installed-slot forwarder and run the selected feature installer at js_gc_init startup before user code. The current main method/read-holder/setter-site scanners are preserved. No work was added between mark completion and sweep entry; the synchronous non-moving snapshot window is unchanged.", + "why": "Real GC header addresses, deliberately untraced so the diagnostic does not keep its observed objects alive. Populated only at the end of mark propagation of a synchronous full cycle; consumed at sweep entry in the same run_to_completion invocation. The intervening full-cycle phases do not relocate or run JS callbacks. The Vec is used for membership comparisons and dropped with the census before sweep. Budgeted and minor cycles skip both boundaries. Pin re-audited 2026-09-05 after #9760 touched `gc/mod.rs`: that change is `mod heap_stats;` plus a `pub(crate) use` re-export and alters no mark/sweep control flow. `heap_stats()` is reached only from `js_bun_jsc_heap_stats` (the JS-facing `bun:jsc.heapStats()`), i.e. from mutator code, never inside a cycle, and its own module contract forbids allocation or collection during its walk. The mark-complete \u2192 sweep-entry window is unchanged. Re-audited 2026-09-05 (train125) after #9769 and #9771 touched pinned files. #9769 adds one `reg_scanner!` registration to `gc/mod.rs`; #9771 adds a feature-gated `alloc_census_init()` there and a feature-gated Rust-heap dump inside `take_census`. `alloc-census` is not in the default feature set, and decisively: `census_take_if_armed_at_full_sweep_start` does `PASS1_MARKED.with(|p| p.borrow_mut().take())` BEFORE calling `take_census`, so the snapshot has already left the thread-local by the time #9771's code runs \u2014 it cannot affect the window. Neither change alters mark/sweep control flow. Re-audited 2026-09-06 after #9831 touched `gc/policy.rs`. Its hunks are (a) the tiny-parse pressure guard's pricing (`tiny_parse_pressure_headroom_bytes`, `tiny_parse_pressure_due*`, a `Cell` byte-count base) consulted from JSON.parse's mutator-side boundaries (`gc_bump_malloc_trigger`, `gc_collect_pending_suppressed_parse`, `gc_schedule_parse_boundary_collection_if_pressure`), none of which is reachable from inside a cycle, and (b) one extra `Cell` store in `note_collection_finished_arena_occupancy`, which runs from `publish_reclaim_outcome` in the Publish subphase \u2014 after `step_sweep` has already consumed the snapshot. Mark/sweep control flow between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start` is untouched. Re-audited 2026-09-05 (train126) after #9755 restructured `gc/cycle.rs`. Its hunks are all root-scan machinery (`RootScanSubphase`, `RootScanCycleState`, the mutable-scanner iteration state), which runs BEFORE mark propagation completes; `gc/mod.rs` gains only a `mod young_log;` declaration. The bracketing is unchanged \u2014 `census_pass1_if_armed` is still inside `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` inside `step_sweep` \u2014 and a synchronous full mark-sweep still moves nothing between them. Re-pinned 2026-09-05 for the #9740 hot-TLS conversion of this file: the sole change is `thread_local!` \u2192 `crate::perry_thread_local!`, a macro-name swap with identical declaration syntax and `.with()` call sites. No control flow, no phase boundary, and no storage semantics change. Re-audited 2026-09-06 (train128) after #9794's GC diagnostics touched `gc/mod.rs` and `gc/policy.rs`: both gain diagnostic module declarations and counters only \u2014 no mark/sweep control flow, and the census bracketing in `step_mark_propagation` / `step_sweep` is unchanged. Re-audited for #9794's GC diagnostics: `gc/mod.rs` gains `mod diag_sites;` / `mod survival_diag;`, a re-export, a `diag_sites::full_started(...)` call at TRIGGER time (before mark propagation begins), and exit-time reporting. Nothing executes between mark-complete and sweep-entry, so the window is unchanged. Re-audited 2026-09-06 for the retained array-growth verifier fix: the cycle.rs change passes the existing non-copying evacuation verifier an explicit all-forwarded policy. That call remains in minor finalization, outside the synchronous full-cycle census window; its root and heap reads do not allocate GC objects, move objects, or invoke JS callbacks. The mark-complete and sweep-entry boundaries are unchanged. Re-audited 2026-09-05 after #9830 touched `gc/policy.rs`. That change is (a) six `thread_local! {` blocks rewritten as `crate::perry_thread_local! {` and (b) one `#[cfg(test)]` accessor listing the trigger path's hot-slot indices. The macro keeps the same storage, the same `.with()` at every read and write, and the same destructor registration (the teardown guard exists exactly when `needs_drop` holds, which is what `std::thread_local!` already decided); no value, predicate or branch in the file changes, so no mark or sweep control flow does. The one new behaviour is on a declaration's FIRST read: `HotKey::resolve_and_cache` takes a mutex and allocates a key through the GLOBAL allocator. Even if a first read landed inside this window it would be sound \u2014 the window's contract is that nothing relocates and no JS callback runs, and a mimalloc allocation does neither. `census_pass1_if_armed` is still inside `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` inside `step_sweep`; the bracketing is untouched. Re-audited 2026-09-06 (train132) after #9860 and #9845 touched `gc/mod.rs`. Both hunks are re-export lists and nothing else: #9860 adds `idle_reclaim_elapsed_starts` / `IDLE_RECLAIM_REARM_MS`, and #9845 adds `owner_is_dead_copied_minor_from_space_of_type`. No mark or sweep control flow changes. #9845's substantive work sits in `gc/oldgen.rs` and `gc/copying.rs`, neither pinned: the copying-minor arm (`finalize_dead_copied_minor_from_space_regexps`) runs on a MINOR, which skips both census boundaries; the full-cycle arm (`collect_dead_registered_regexps_post_trace`, from `with_dead_collection_finalize`) walks the RegExp registry building a Vec of addresses \u2014 no GC allocation, no JS callback, so it cannot relocate the snapshot's subjects \u2014 and it is reached from the sweep body, i.e. AFTER `census_take_if_armed_at_full_sweep_start` has already `take()`n the snapshot out of the thread-local. The mark-complete -> sweep-entry window is unchanged. Re-audited 2026-09-07 for #9965 after 1ec9e0e8a touched `gc/cycle.rs` and `gc/mod.rs`: `gc/mod.rs:216-217` only declares and imports the failure-attribution module, while `gc/cycle.rs:1414-1417` reads the trigger and diagnostic counters immediately before evacuation verification inside `atomic_finalize_minor_prelude`. Full cycles bypass `MinorPrelude` at `gc/cycle.rs:1192-1196`; evacuation remains guarded by the minor-only context at `gc/cycle.rs:1330-1372`. The snapshot store remains at `gc/cycle.rs:963-964` after synchronous full marking, and its take remains at `gc/cycle.rs:1454-1457` before sweep. No new write, relocation, collection, or JS callback was added to that full-cycle interval, so the PASS1_MARKED window is unaffected. Re-audited 2026-09-07 for the regex census rows: all new work is in `take_census` after `census_take_if_armed_at_full_sweep_start` has taken PASS1_MARKED out of TLS; neither boundary nor the intervening cycle control flow changed. Re-audited 2026-09-08 (train144) after #9976 and #9977 touched pinned files. `gc/mod.rs` gains exactly three lines: `mod copying_phase;` and `mod regex_census;` (declarations) and one `reg_scanner!(regex::site_test::scan_roots_mut)` registration. A scanner registration adds a root SOURCE for the mutable-root walks; it does not move either census boundary and runs nowhere between them. `gc/census.rs` widens `side_tables()` to `pub(super)`, extends it with regex rows and adds a test module \u2014 all census REPORTING, which runs from the diagnostic dump, not inside a cycle. Mark/sweep control flow between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start` is untouched. Re-audited 2026-09-08 for #9849 JSON construction deferral. `gc/mod.rs` adds the `json_defer` module/re-export and a trusted-header layout helper used only by already-validated JSON emitters; neither changes or runs in collector phase control flow. `gc/policy.rs` adds JSON completion scheduling, construction-grace checks, and safepoint deferral predicates. These are called from mutator-side JSON allocation/output boundaries and ordinary safepoint entry; they do not alter `step_mark_propagation`, `step_sweep`, or invoke callbacks or relocation between the census boundaries. The mark-complete to sweep-entry window is unchanged. The follow-up adds a cfg(test)-only one-shot boolean for deterministic explicit-pressure fixtures; it is absent from production builds and cannot affect the census window. The first predicate read consumes it, so post-parse accounting exercises normal pricing. Re-audited 2026-09-09 for bounded tiny-JSON completion polling. The policy.rs changes split the mutator-side pending-parse check into an inlined empty fast path plus an outlined debt-service path, and amortize the mutator-side arena-pressure read across 64 bounded parse completions. Neither function is reachable from step_mark_propagation or step_sweep; neither census boundary nor the synchronous full-cycle interval between them changes. Re-audited 2026-09-09 for lazy JSON record batches: policy.rs only widens gc_budgeted_cycle_active visibility from pub(super) to pub(crate). Its body remains a read-only Cell query. The new caller is lazy_get materialization in the mutator; run_to_completion, step_mark_propagation, census snapshot consumption at step_sweep, and the synchronous non-moving window are unchanged. Re-audited 2026-09-09 for completed JSON-output debt: the added gc_service_json_output_sweep function calls the existing trigger check from a rooted mutator boundary and reports whether its malloc-count request remains due. It is not called from any census or collector phase; the synchronous mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-09 for the JSON byte-debt carry: the same mutator-only service helper now distinguishes requests satisfied before its call from those satisfied by its trigger check. The added enum contains no payload, both count reads are scalar, and no census boundary or collector phase changed. Re-audited 2026-09-11 for #10055: gc/mod.rs only registers the weak UTF-16 index scanner during gc_init. It neither marks strings nor allocates GC objects or runs JS; offset vectors use the Rust allocator. The mark-complete to sweep-entry census window and cycle control flow are unchanged. Re-audited 2026-09-11 for #10054: gc/mod.rs adds only the trim-cache mutable-root scanner registration in gc_init. Its scanner visits two existing string slots without allocating or invoking JS. Root scanning still precedes mark completion, and neither census boundary nor the synchronous mark-complete to sweep-entry window changes. Re-audited 2026-09-11 for #10060: the census array classifier now reads the logical element start and bounds its scan by the remaining capacity. The helper only reads the existing GC/header words and performs pointer arithmetic; it cannot allocate, collect, or call JS. This classifier runs in take_census after PASS1_MARKED has been taken out of TLS. Neither census boundary nor the mark-complete to sweep-entry control flow changed. Re-audited for #8512: gc/mod.rs only enables the existing PTY mutable-root scanner on Windows; it changes no mark/sweep phase or census boundary. The scanner visits NaN-boxed slots without running JS callbacks. Re-audited 2026-09-12 for the single regular-expression engine: `gc/mod.rs` changes `mod prefetch;` to `pub(crate) mod prefetch;` so the RegExp owner-table walks can prefetch headers, a visibility change with no new call in collector control flow; `gc/census.rs` changes only its `#[cfg(test)]` `regex_census_tests` module, dropping assertions for the previous engine's cache rows. Neither boundary (`census_pass1_if_armed` in `step_mark_propagation`, `census_take_if_armed_at_full_sweep_start` in `step_sweep`) nor the synchronous mark-complete to sweep-entry interval changes. Re-audited 2026-09-13 after the #10169 fix touched `gc/mod.rs` and `gc/policy.rs`. `gc/mod.rs` gains only `pub(crate) use` re-exports (`policy::note_young_leaf_born_old`, `policy::young_generation_holds_a_nursery`, `promote_in_place::{young_generation_measured_dying, young_generation_measured_retained}`, and cfg(test) survival seeders). `gc/policy.rs` gains a `Cell` thread-local (`GC_YOUNG_LEAF_BORN_OLD`, no pointer), its setter, a pure predicate over `copying_from_space_in_use_bytes` vs the base nursery cap, and a consumed-once branch at the top of `gc_budgeted_due_trigger` that may answer `YoungScavengeCap` ahead of `OldReclaim`. That branch decides WHICH collection a safepoint starts (a minor instead of a full); it runs before any cycle begins and never inside one, so the mark-complete \u2192 sweep-entry window of a synchronous full \u2014 where PASS1_MARKED is populated and consumed within one `run_to_completion` \u2014 is unchanged, and neither hunk adds an allocation, a JS callback, or a relocation to it. Re-audited 2026-09-13 for the heap generation (#10164 cross-call search positions): `gc/mod.rs` only declares `pub(crate) mod heap_generation;`. `gc/cycle.rs` wraps the `Sweep` and `Reclaim` arms of `GcCycleState::step` in a `HeapChange` scope and opens one inside `atomic_finalize_minor_prelude`'s evacuation branch (with a nested one around old-page defrag). Opening and closing a scope only increments two thread-local integer cells (`HEAP_GENERATION`, `OPEN_HEAP_CHANGES`); a first thread-local read may allocate a key through the global allocator, which neither relocates nor runs JS. The `Sweep` scope opens immediately before `step_sweep`, i.e. before `census_take_if_armed_at_full_sweep_start` takes PASS1_MARKED out of TLS, and adds no relocation, collection or JS callback to the synchronous mark-complete to sweep-entry window; the minor-prelude scope is unreachable from a full cycle, which bypasses `MinorPrelude`. Neither boundary nor the intervening control flow changed. Re-audited 2026-09-13 for #10182 block-granular reclamation, which touched `gc/cycle.rs`. Two hunks: (a) in the `RememberedSetRebuild` subphase of AtomicFinalize \u2014 INSIDE the window \u2014 the require-marked old-to-young rebuild is now constructed with `OldToYoungRememberedRebuildState::new_skipping`, whose cursor never enters blocks the census recorded as holding no reached, pinned or pre-marked object (`BlockCensus::unmarked_blocks`); computing that list reads `arena_block_snapshots()` and allocates one `Vec` through the global allocator. It visits a subset of the same objects the rebuild already walked (every skipped object would have been rejected as unmarked), and it neither allocates a GC object, relocates anything, nor runs a JS callback. (b) In `step_sweep`, `IncrementalSweepState::with_block_skip` runs after `census_take_if_armed_at_full_sweep_start` has already taken PASS1_MARKED out of TLS. Neither boundary moved and the synchronous mark-complete to sweep-entry interval gains no relocation, collection or callback. Re-audited 2026-09-11 for the startup memory profile: gc/mod.rs only retains the pre-main allocator-policy constructor in js_gc_init. The constructor applies process allocation options, without invoking GC or JS. No census boundary, collector phase, or mark-complete to sweep-entry control flow changed. Re-audited 2026-09-13 for #10179: census.rs only adds a native regex cache metadata row and its unit assertion; snapshot consumption and the full-cycle window are unchanged. Re-audited 2026-09-14 for the GC due-check fast path, which touched `gc/mod.rs` and `gc/policy.rs`. `gc/mod.rs` only changes the safepoint re-exports: `gc_runtime_safepoint` becomes cfg(test) and `gc_runtime_safepoint_poll` is added. `gc/policy.rs`: the budgeted step returns a debt-free `GcStepReport` (debt is attached by the FFI and test entry points after the step returns) and moves cycle start/step into an out-of-line `gc_budgeted_start_or_step`; `gc_check_trigger` reuses a repeatable due-trigger answer through `DueTriggerMemo`, placed after its `GC_FLAG_IN_ALLOC` and suppression early returns; the young scavenge cap reuses the old-gen pressure value the due trigger already read and checks the census-seeded flag first. All of it runs from mutator safepoints, allocation-point trigger checks and the host step API, before a cycle starts or between budgeted steps. None of it is reachable between `census_pass1_if_armed` in `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` in `step_sweep` of a synchronous full: an allocation inside that window reaches `gc_check_trigger` with `GC_FLAG_IN_ALLOC` set and returns before the changed code. No allocation, relocation, collection or JS callback is added to the window. Re-audited 2026-09-14 for the tiny-parse nursery-cap boundary, which touched `gc/policy.rs`. It adds `tiny_parse_generational_collection_due`, a pure predicate (the existing `tiny_parse_pressure_due` OR the existing `young_scavenge_cap_due` read), and calls it instead of `tiny_parse_pressure_due` from `gc_bump_malloc_trigger_inner` and `gc_collect_pending_suppressed_parse_slow` (generational branch only) and from `gc_schedule_parse_boundary_collection_if_pressure`. All three are JSON.parse mutator-side boundaries, none reachable from `step_mark_propagation` or `step_sweep`; the predicate reads counters and allocates nothing. Neither census boundary nor the synchronous mark-complete to sweep-entry interval changed. Re-audited 2026-09-13 for #10182's full-collection throughput follow-up, which touched `gc/cycle.rs` in one hunk, INSIDE the window: the `RememberedSetRebuild` subphase of a synchronous full now first asks `verify::full_remembered_rebuild_provably_empty` and, when it holds, installs `OldToYoungRememberedRebuildState::provably_empty()` (an empty sticky set, no walk) instead of the require-marked rebuild. The predicate reads `arena_block_snapshots()` (one `Vec` through the global allocator), the census's per-block reached/pre-marked facts and the malloc registry's length; the constructor bumps a `Cell` counter and prints one line under `PERRY_GC_DIAG`. None of it allocates a GC object, relocates anything, collects, or runs a JS callback, and both census boundaries stay where they were. Re-audited 2026-09-14 for #10182's pacing-full work, which touched `gc/cycle.rs`, `gc/mod.rs` and `gc/policy.rs`. `gc/cycle.rs`: `GcCycleState::new_full` no longer calls `materialize_all_promoted_page_runs`; that call ran in the constructor, before the census and far before `census_pass1_if_armed`, and removing it adds nothing to the window. `gc/mod.rs`: one `mod promoted_cohort;` declaration. `gc/policy.rs`: (a) `credit_promoted_bytes_to_old_baseline` also credits a `Cell` cohort counter (it runs after a copying minor completes); (b) `finish_full_old_reclaim_baseline` also records the verified old live bytes and resets that counter (Publish, after `step_sweep` consumed the snapshot); (c) `gc_safepoint_moving_minor` arms and disarms the promotion-census record around its nursery minor and calls `run_promoted_cohort_full_if_due`, which starts a synchronous full through the same `gc_collect_full_mark_sweep_with_trigger` entry and reads byte counters before and after it. All of it runs before a cycle starts or after it completes; none of it runs between mark completion and sweep entry, allocates a GC object, relocates anything, or calls into JS. The census the promoted-cohort full may adopt from the promotion walk is built in `BuildValidPointerSet`, before either boundary. Both boundaries are unchanged. Re-audited 2026-09-14 for the #10182 dead-stack scrub in `gc/cycle.rs`: `step_build_valid_pointer_set` now calls `scrub_dead_stack_below`, which zeroes a local array in its own frame (dead stack below the caller), right after the census finishes \u2014 in `BuildValidPointerSet`, before the root scan and far before `census_pass1_if_armed`. It writes no heap memory, allocates nothing, relocates nothing and calls no JS; both boundaries are unchanged. Re-audited 2026-09-14 for #10241 (cohort survival), which touched `gc/cycle.rs` and `gc/policy.rs`. `gc/cycle.rs`: one call, `promoted_cohort::survival::check_minor_view_at_full_sweep_start()`, in `step_sweep` immediately AFTER `census_take_if_armed_at_full_sweep_start` has taken PASS1_MARKED out of TLS, i.e. outside the window. It is a no-op unless a promoted-cohort full armed its survival probe; when armed it walks the old page index over the preceding minor's dirty pages (`old_arena_walk_objects_on_pages`, Rust-allocator Vecs), reads GC headers' mark flags and the slots of unmarked ones, and records one enum. It writes no heap memory, allocates no GC object, relocates nothing and calls no JS. `gc/policy.rs`: `run_promoted_cohort_full_if_due` arms the probe before `gc_collect_full_mark_sweep_with_trigger` and takes it after the full returns (feeding `note_full_measured_promotion_survival` and one diagnostic line); both run before a cycle starts or after it completes. Both boundaries are unchanged. Re-audited 2026-09-14 for #10241's in-place-only cohort: `gc/policy.rs` drops the `promoted_cohort::note_promoted` call from `credit_promoted_bytes_to_old_baseline` (the copying minor now calls `promoted_cohort::note_minor_promotion` itself, after the credit). Both run at the end of a copying minor, outside any full cycle; the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-14 for the parse-boundary side-allocation band (medium-parse pacing), which touched `gc/policy.rs`. Three hunks: (a) a `Cell` thread-local (`GC_LAST_COLLECTION_EXTERNAL_SIDE_BYTES`, a byte COUNT, no pointer) plus three pure predicates over it and `external_side_live_bytes()`; (b) that predicate added as a third disjunct of `tiny_parse_generational_collection_due`, which is read only from the three JSON.parse mutator-side boundaries (`gc_bump_malloc_trigger_inner`, `gc_collect_pending_suppressed_parse_slow`, `gc_schedule_parse_boundary_collection_if_pressure`), none of them reachable from `step_mark_propagation` or `step_sweep`; and (c) one extra `Cell` store in `note_collection_finished_arena_occupancy` plus two extra reads in the `PERRY_GC_DIAG` tiny-parse line. `note_collection_finished_arena_occupancy` runs from `publish_reclaim_outcome` in the Publish subphase, i.e. AFTER `step_sweep` has already `take()`n the snapshot out of the thread-local, exactly as #9831's store on the same line does. Nothing added allocates a GC object, relocates anything, or runs a JS callback, and neither census boundary moved. Re-audited 2026-09-14 for the drained-bytes counterweight to that band, which touched `gc/policy.rs` again. Four hunks: a second `Cell` thread-local (`GC_EXTERNAL_SIDE_DRAINED_SINCE_FULL`, a byte COUNT); one increment of it inside `gc_note_external_side_free`; a pure read (`external_side_old_reclaim_pressure_bytes`) substituted for `external_side_live_bytes()` at the four old-reclaim pressure sites; and one `Cell` store at the top of `finish_full_old_reclaim_baseline`. None of it can run between the census boundaries. `gc_note_external_side_free` is also reached by mutator-side tape materialization, regex scratch teardown, native-addon adjustments and buffer replacement. Its added operation is only a saturating increment of a scalar Cell, with no GC allocation, relocation, collection or JS callback, so this wider caller set does not invalidate the census window. `finish_full_old_reclaim_baseline` runs from `publish_reclaim_outcome` in the Publish subphase, the same place #9831's store already sits. The pressure reads happen at trigger decisions, before a cycle starts. No allocation, relocation, collection or JS callback is added to the mark-complete -> sweep-entry window, and neither boundary moved. Re-audited 2026-09-15 for turnloop P0, which touched `gc/mod.rs` with one added call: `crate::event_pump::shutdown_wait_driver()` inside `js_gc_release_current_thread_collection_side_allocations`, the process-exit funnel. That function runs once no more JavaScript can run on the thread, never from inside a collection cycle; the added call drops the thread's turnloop wait loop (closing its kqueue/epoll descriptor) and may print a diagnostic line. It allocates no GC object, relocates nothing, starts no collection and runs no JS callback. The census boundaries and the mark-complete -> sweep-entry window are untouched.. Re-audited 2026-09-16 for the copying minor's per-parent weak-holder fact: `gc/mod.rs` gains exactly one line, `mod copying_parent_facts;`, a module declaration. The module it declares holds `weak_holder_fact` (a read of the parent's `obj_type`/`class_id` via `weakref::is_weak_holder_header`) and the copying minor's `visit_slot_with_parent`, moved verbatim out of `gc/copying.rs` for the 2000-line lint. Both run only inside a COPYING MINOR, which skips both census boundaries (`census_pass1_if_armed` in `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` in `step_sweep` are synchronous-full only). Nothing was added to any full-cycle phase, and the declaration itself executes no code. Neither boundary moved and the synchronous mark-complete to sweep-entry window gains no allocation, relocation, collection or JS callback. Re-audited 2026-09-18 for the #10532 follow-up argument-list rooting fix, which touched `gc/mod.rs`. The only change there is `mod collection_points;` plus a `pub(crate) use collection_points::collection_point;` re-export (and, under `#[cfg(test)]`, `arm_collection_point`). `collection_point` is an inline no-op outside `cfg(test)`; under test it only runs a copying minor when called from ordinary MUTATOR code (`proxy.rs`'s `Reflect.apply` rebind path and `registry.rs`'s rest-array bundler), never from inside `step_mark_propagation` or `step_sweep`. Neither `census_pass1_if_armed` nor `census_take_if_armed_at_full_sweep_start` is reachable from it, so the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-18 (same PR, round 2) for the added `arm_collection_point_after` re-export in `gc/mod.rs`: another pure re-export line, same as the `collection_point`/`arm_collection_point` one already covered above. `arm_collection_point_after` only changes test-only arming state in `collection_points.rs` (which named site fires and on which hit); it still runs no mark/sweep control flow. Re-audited 2026-09-19 for #10735 (require.main threading): gc/mod.rs gains exactly one line, `reg_scanner!(crate::module_require::scan_cjs_main_module_root_mut);`, registering the new CJS_MAIN_MODULE thread-local's mutable-root scanner beside the existing `scan_module_path_roots_mut` registration. A scanner registration adds a root SOURCE for the mutable-root walks; it runs during root scanning, before mark propagation completes, and does not execute between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start`. Neither census boundary moved and the synchronous mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-20 for #10834 (inherited-property read cache). `gc/mod.rs` gains exactly one line: `reg_scanner!(crate::object::inherited_read_cache::scan_inherited_read_cache_roots_mut);` in `gc_init()`. A scanner registration adds a root SOURCE for the mutable-root walks. The walk runs inside `RootScanCycleState::step_current_subphase`, i.e. entirely within the RootScan phase: `step_root_scan` only sets `self.phase = GcCyclePhase::MarkPropagation` once that loop reports done (`gc/cycle.rs:958-961`), and `census_pass1_if_armed()` fires at the END of `step_mark_propagation` (`gc/cycle.rs:982`). The scanner therefore runs strictly BEFORE the window opens and can never execute between the boundaries. Its body is a bounded walk of a fixed 512-entry thread-local array calling `visit_tagged_usize_slot` / `visit_usize_slot`; it allocates nothing, relocates nothing and runs no JS callback. Same shape as #9769, #9976/#9977, #10054, #10055 and #10735, all previously cleared. The PR also adds an `INHERITED_READ_CACHE` entry to `DEAD_KEY_PRUNES` in `gc/dead_owner.rs` (not a pinned source). That registry is consumed by `IncrementalSweepState::with_dead_collection_finalize` at `gc/cycle.rs:1548`, which is AFTER `census_take_if_armed_at_full_sweep_start` at `gc/cycle.rs:1505` has already `take()`n the snapshot out of the thread-local -- the same argument that cleared #9845's `collect_dead_registered_regexps_post_trace`. The prune reads addresses and zeroes entries; no GC allocation, relocation or callback. Both additions sit outside the window, on opposite sides of it. Neither boundary moved and the synchronous mark-complete to sweep-entry interval is unchanged. Re-audited 2026-09-22 for #10399 (per-thread module init), which touched `gc/mod.rs`. Two hunks, both init-time: a new free function `raise_default_thread_stack_floor()` and one call to it at the top of `js_gc_init`, before `enter_current_thread_image`'s successor statements. The function reads `RUST_MIN_STACK` from the environment and, only when it is unset, sets it to 32 MiB so a thread spawned against a multi-megabyte static TLS block still has usable stack (glibc carves static TLS out of the thread's stack mapping). It touches no heap object, allocates no GC object, relocates nothing and runs no JS callback. `js_gc_init` is the first runtime call of a compiled `main`, so it runs once before any cycle exists, and it is not reachable from `step_mark_propagation` or `step_sweep`. Same shape as the 2026-09-11 startup-memory-profile re-audit, which cleared the pre-main allocator-policy constructor in the same function. Neither census boundary moved and the synchronous mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-23 (size/runtime-decouple, #11135) after the binary-size branch touched `gc/census.rs`, `gc/mod.rs` and `gc/policy.rs`. census.rs: `census_pass1_if_armed` / `census_take_if_armed_at_full_sweep_start` keep their bodies verbatim, moved into `_impl` functions compiled only with the new `gc-instruments` feature (without it both are empty and `census_path()` is `None`, so nothing is ever armed); the take still empties PASS1_MARKED before `take_census`. gc/mod.rs: `gc_init` gains a startup env check that aborts when an instrument knob is set without the feature, before any cycle exists. gc/policy.rs: env-knob OnceLock caches now initialize through `crate::once_init::get_or_init` (same closures, same values). No mark/sweep control flow between the two census boundaries changed; the window is unchanged. Re-audited for Fetch handle reclamation: cycle.rs only redirects the incomplete-cycle Drop cancellation hook to also cancel the Fetch trace. The full-trace finish hook removes native records and cached slots without allocating GC objects or invoking JS; it cannot relocate the census addresses before sweep entry. Re-audited 2026-09-22 for #10928 (one proportional old-reclaim rule), which touched `gc/policy.rs`. Six hunks. (a) Two new thread-locals, `GC_OLD_RECLAIM_PRE_IN_USE_BYTES` (`Cell`) and `GC_OLD_RECLAIM_BACKOFF_SHIFT` (`Cell`): both are byte/shift COUNTS, neither holds a pointer. (b) `gc_old_reclaim_growth_band_bytes` gains a `Cell` read and a left shift -- pure arithmetic over byte counts. (c) `old_reclaim_pressure_due` loses the #7937 absolute first-crossing arm, splits its pure form out as `old_reclaim_pressure_due_inner`, and calls `note_old_reclaim_cycle_started()` when the answer is true. That predicate is read at TRIGGER decisions only -- the allocation-point `gc_check_trigger` and `gc_budgeted_due_trigger` at safepoints -- i.e. before a cycle starts, never between the boundaries; an allocation inside the window reaches `gc_check_trigger` with `GC_FLAG_IN_ALLOC` set and returns before this code, the same argument the 2026-09-14 due-check fast-path re-audit made for the same function. Even if it did run there it would be sound: `note_old_reclaim_cycle_started` stores one scalar `Cell` from `pacing_arena_in_use_bytes()` (a read of `arena_live_allocated_bytes`), which allocates no GC object, relocates nothing and runs no JS callback -- the window's contract. (d) `update_old_reclaim_backoff` is called only from `finish_full_old_reclaim_baseline`, which runs from `publish_reclaim_outcome` in the Publish subphase, AFTER `step_sweep` has already `take()`n the snapshot out of the thread-local -- exactly where #9831's store and the medium-parse pacing store already sit. (e) `gc_old_reclaim_debt_bytes` drops the absolute arm it mirrored; it remains pure arithmetic read at debt/trigger decisions. (f) `#[cfg(test)]` seams, absent from production builds. `census_pass1_if_armed` is still inside `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` inside `step_sweep`. Nothing added allocates a GC object, relocates anything, collects, or invokes a JS callback between them; the change alters only WHEN a collection is scheduled, never what runs inside one. Neither boundary moved and the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-24 for #10960 (growth-aware old-reclaim backoff), which touched `gc/policy.rs` again. One new thread-local, `GC_OLD_RECLAIM_LAST_POST_IN_USE_BYTES` (`Cell`), a byte COUNT that holds no pointer. It is written only by `update_old_reclaim_backoff`, which runs from `finish_full_old_reclaim_baseline` in the Publish subphase, after `step_sweep` has already taken the snapshot out of the thread-local; the change there is pure integer arithmetic deciding whether to widen the band. Nothing added allocates a GC object, relocates anything, collects, or invokes a JS callback, and neither window boundary moved. Re-audited 2026-09-24 after the class-capture environment added one `reg_scanner!` registration (`scan_class_env_roots_mut`, visiting each guarded class environment's owner class object) to `gc/mod.rs`: a root-scanner registration alters no mark/sweep control flow and runs nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-25 after the inherited-access lane touched `gc/mod.rs`: the change is one `reg_scanner!` registration for `object::chain_store::scan_chain_store_roots_mut`, a root scanner that visits store-site chain verdicts (one interned key pointer each) during root scanning. It runs at the start of a cycle, never between mark completion and sweep entry, relocates nothing and runs no JS callback. The synchronous mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-25 for #10698 (allocation-point trigger watermark), which touched `gc/mod.rs` and `gc/policy.rs`. `gc/mod.rs` gains `pub(crate) mod trigger_watermark;` and a `pub(crate) use` re-export -- no control flow. In `gc/policy.rs`: (a) `gc_check_trigger` becomes an out-of-line wrapper over an inlined fast path that reads the watermark cell, the malloc registry's length through an unguarded shared borrow and the inline allocator's offset, then either returns or runs the previous body unchanged (`gc_check_trigger_evaluate`). An allocation inside the window reaches it with `GC_FLAG_IN_ALLOC` set: the fast path returns without acting, which is the outcome of the `GC_FLAG_IN_ALLOC` early return it would otherwise reach, and the slow path still takes that early return. (b) The due-trigger evaluation also returns a watermark -- integer arithmetic over values it already read -- published only on the no-action return past every entry guard. (c) The ladder's thresholds and flags are retyped `Cell` -> `TriggerInput`, whose writes add one store retiring the watermark; heap-generation advances and `Arena::set_current` retire it too. (d) `#[cfg(test)]` seams. Nothing added allocates a GC object, relocates anything, collects, or invokes a JS callback; `census_pass1_if_armed` is still inside `step_mark_propagation` and `census_take_if_armed_at_full_sweep_start` inside `step_sweep`, so the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-25 for #10498 (class-accessor cache), which adds one `reg_scanner!` registration (`scan_class_accessor_cache_roots_mut`, marking and rewriting the cache's key strings) to `gc/mod.rs`: again a root-scanner registration, altering no mark/sweep control flow and running nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-26 for #10572: `gc/mod.rs` gains one `#[cfg(not(feature = \"hot-diag\"))]` call to `hot_diag::refuse_knobs_without_hot_diag()` in `gc_init`, a startup-only knob check that runs before any cycle and alters no mark/sweep control flow; the mark-complete to sweep-entry window is unchanged. Re-audited 2026-09-27 for #11416: `gc/mod.rs` loses the `reg_scanner!` registration for `class_accessor_cache::scan_class_accessor_cache_roots_mut` (the cache is deleted); removing a root-scanner registration alters no mark/sweep control flow and runs nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-27 for the method-calls lane, which adds one `reg_scanner!` registration (`scan_method_site_roots_mut`, marking and rewriting the method closures inherited method-site entries hold) to `gc/mod.rs`: a root-scanner registration, altering no mark/sweep control flow and running nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-28 (charter step 5, P0): the field-representation census adds `mod census_field_repr;` to `gc/mod.rs` and, in `census.rs`, one Rust-owned accumulator fed from `visit_object` inside `take_census`, which runs after `census_take_if_armed_at_full_sweep_start` has already taken PASS1_MARKED; it reads slots and the per-object layout maps (try_borrow), allocates nothing on the JS heap and alters no mark/sweep control flow. The window is unchanged. Re-audited 2026-09-27 for #11416: `gc/mod.rs` loses the `reg_scanner!` registration for `class_accessor_cache::scan_class_accessor_cache_roots_mut` (the cache is deleted); removing a root-scanner registration alters no mark/sweep control flow and runs nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-27 for the method-calls lane, which adds one `reg_scanner!` registration (`scan_method_site_roots_mut`, marking and rewriting the method closures inherited method-site entries hold) to `gc/mod.rs`: a root-scanner registration, altering no mark/sweep control flow and running nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-27 (class constructors as function objects, #11414): `gc/mod.rs` gains one `reg_scanner!` registration (`object::class_value::scan_class_value_roots_mut`, the per-agent class function-object table, visited and rewritten) \u2014 a root-scanner registration that alters no mark/sweep control flow and runs nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-29 after #11659 added `verify::verify_array_hole_tails_at_collection()` to `gc/mod.rs` and `gc/policy.rs`. Every call sits at a collection or budgeted-cycle ENTRY, beside `roots::ensure_stack_maps_built()`, i.e. before mark begins and so before `census_pass1_if_armed` opens the window. The call is a read-only walk of array headers (debug builds, or release with PERRY_GC_VERIFY_ARRAY_HOLES): it neither allocates GC memory, relocates, runs JS, nor reads or writes PASS1_MARKED, and it panics rather than continuing on a violation. Mark/sweep control flow between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start` is untouched. Pin re-audited 2026-09-29 after PR #11646 added one call inside that window, in `cycle.rs` beside `census_take_if_armed_at_full_sweep_start`: `object::shapes::store_kind::audit_heap_at_full_sweep_start()`. It compiles to nothing unless the `shape-fact-audit` feature is on; when on it walks the arena read-only (`gc::for_each_live_object_at_sweep_start`, in `gc/verify.rs`) and reads each marked object header and its shape record. It allocates no GC object, moves nothing, runs no JS callback and holds no address past the walk, so PASS1_MARKED stays valid across it; a disagreement panics rather than continuing. Re-audited 2026-09-29 after the pinned-roots fix touched `gc/cycle.rs` and `gc/mod.rs`: the block-persistence live-block predicate now reads GC_FLAG_MARKED alone (pinned objects are marked as roots, so the set of live blocks is unchanged), and `gc/mod.rs` adds one `reg_scanner!` registration. Neither relocates, runs JS, or moves the mark-complete to sweep-entry window. Re-audited again 2026-09-29 (same fix, copying-minor follow-up): `BlockPersistCycleState`'s force-mark in `gc/cycle.rs` no longer skips an unmarked pinned header, so it marks and pushes it like any other object of a live block. That is marking inside the budgeted cycle's persistence step: nothing relocates, no JS runs, and the budgeted path still skips both census boundaries. The window is unchanged. Re-audited 2026-09-29 for this-as-a-parameter stage 3: `gc/mod.rs` registers `scan_dispatch_binding_roots_mut` in place of `scan_implicit_this_roots_mut` (the implicit-`this` cell is deleted; the same scanner body keeps `new.target`, the static-`this` override and the static private-owner stack) and rewords its comment; a root-scanner registration, altering no mark/sweep control flow and running nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-28 for the JsFunctionInfo lane: `gc/census.rs`'s `side_tables()` loses its `closure_registry_census()` row (the closure-body registry is deleted); a census report row, altering no mark/sweep control flow and running nothing inside the mark-complete to sweep-entry window. Re-audited 2026-09-29 for the read-site holder entry: `gc/mod.rs` adds one `reg_scanner!` registration, `read_holder::scan_read_holder_roots_mut`, which visits the holder and hop words of registered read-site caches during ROOT SCAN, before mark propagation completes. It rewrites root slots only through the visitor (as every registered scanner does), runs no JS, and nothing it does executes between `census_pass1_if_armed` and `census_take_if_armed_at_full_sweep_start`. The window is unchanged. Re-audited 2026-09-30 for Step 5 P4: census.rs only removes the typed-layout count, and gc/mod.rs removes shape-install registration/re-export and an init-time typed-intact verifier gate. The snapshot remains stored after mark propagation and taken at sweep entry; none of these edits relocates objects or invokes callbacks in that interval. Re-audited 2026-09-29 (#11549 trace cost): `gc/mod.rs` gains one `mod copying_object_scan;` declaration, the copying minor's plain-object slot walk. It is reached only from the copying drain, never from a full cycle, and changes no mark or sweep control flow. The window is unchanged. Re-audited 2026-09-30 after rebasing P4 over #11549/#11676: gc/mod.rs retains the copying_object_scan declaration and P4 removes typed-layout registration; both are module wiring outside the synchronous full mark-complete to sweep-entry window. The census boundaries and the window owner in cycle.rs are unchanged. Re-audited 2026-09-30 for the scope-context change: gc/mod.rs only removes the box root-scanner registration and exit-time box statistics. The former ran during root scan, before mark completion, and the latter runs at process exit. Neither changes the synchronous mark-complete to sweep-entry window. Re-audited 2026-09-28 for #11605 (link-time runtime feature installs): `gc/mod.rs` registers the interpreter root scanner through its always-present forwarder (`dyn_eval_hooks::scan_dyn_eval_roots_mut`, which calls the real scanner through a slot the `dyn-eval` install fills) instead of a feature-gated direct registration, and `js_gc_init` ends by running the program\u2019s feature installer, which only stores fn pointers into `Hook` slots at startup, before any user code or collection. Neither alters mark/sweep control flow or runs inside the mark-complete to sweep-entry window. Re-audited 2026-10-01 after main integration: the pinned census, cycle, policy and progress files are byte-identical to current main. The only gc/mod.rs delta replaces the interpreter scanner registration with its installed-slot forwarder and invokes the installer at js_gc_init startup, before user code and any collection. No change executes between mark completion and sweep entry. Re-audited 2026-09-28 for the release-runtime instrument strip: every changed line in `gc/census.rs`, `gc/cycle.rs`, `gc/mod.rs` and `gc/policy.rs` renames a `feature = \"diagnostics\" | \"gc-instruments\" | \"hot-diag\"` gate to the build-script cfg `perry_diagnostics` / `perry_gc_instruments` / `perry_hot_diag`, which `perry-runtime/build.rs` sets exactly when the feature is on unless PERRY_RELEASE_STRIP_INSTRUMENTS=1. With the cfg set the compiled code is unchanged; with it unset (release packages) the census that fills this snapshot is not compiled, so the window never opens. No mark/sweep control flow changes. Re-audited 2026-10-01 while integrating the repaired #11605 parent: all five pinned files are byte-identical to that reviewed parent after normalizing only the diagnostics/instrument cfg names; its current census_field_repr module and startup installer hooks remain intact. The synchronous mark-complete to sweep-entry window is unchanged. Re-audited for A2 inherited-read-cache deletion: gc/mod.rs removes only its old root-scanner registration and adjacent comments. The method-site and read-holder scanners still run during root scan before mark completion; no collection phase or callback was added inside the mark-complete to sweep-entry window. Re-audited 2026-09-30 for the setter-site root scanner: gc/mod.rs adds only its reg_scanner! registration in the root-scan setup. That scanner visits per-site key and holder slots before mark propagation completes; it runs no JS and adds no relocation or callback between census pass1 and sweep entry. The synchronous-full snapshot window and both boundaries remain unchanged. Re-audited 2026-10-01 against main a8f4f3dd76: census.rs, cycle.rs, policy.rs and progress.rs are byte-identical to reviewed #11629 head 85e0fe18ec. The only pinned-file delta in gc/mod.rs removes the deleted inherited-read-cache scanner, retains chain-store/method-site/read-holder scanners, and registers the setter-site scanner during gc_init. These registrations run during root scanning before mark completion. Both link-time feature installers and release instrument cfgs are preserved; neither census boundary nor the non-moving synchronous-full interval changes. Re-audited 2026-09-28 for #11605 (link-time runtime feature installs): `gc/mod.rs` registers the interpreter root scanner through its always-present forwarder (`dyn_eval_hooks::scan_dyn_eval_roots_mut`, which calls the real scanner through a slot the `dyn-eval` install fills) instead of a feature-gated direct registration, and `js_gc_init` ends by running the program\u2019s feature installer, which only stores fn pointers into `Hook` slots at startup, before any user code or collection. Neither alters mark/sweep control flow or runs inside the mark-complete to sweep-entry window. Re-audited 2026-10-01 after main integration: the pinned census, cycle, policy and progress files are byte-identical to current main. The only gc/mod.rs delta replaces the interpreter scanner registration with its installed-slot forwarder and invokes the installer at js_gc_init startup, before user code and any collection. No change executes between mark completion and sweep entry. Re-audited 2026-10-01 while integrating main 7b5912d4e7 into #11605: census.rs, cycle.rs, policy.rs and progress.rs are byte-identical to that main. The only gc/mod.rs differences register the existing interpreter root scanner through its installed-slot forwarder and run the selected feature installer at js_gc_init startup before user code. The current main method/read-holder/setter-site scanners are preserved. No work was added between mark completion and sweep entry; the synchronous non-moving snapshot window is unchanged. Re-audited 2026-10-02 for the SPECIAL ConstFn verifier: gc/mod.rs adds only a feature-gated forwarding-helper re-export. The helper follows existing validated forwarding and runs no JS, allocation or collection. No holder, scanner registration or mark/sweep control flow is added; both census boundaries and their synchronous window remain unchanged.", "window": { "start": { "file": "crates/perry-runtime/src/gc/census.rs", @@ -358,10 +358,10 @@ "function": "run_to_completion" }, "sources": { - "crates/perry-runtime/src/gc/census.rs": "4a611bfe5615559642b0e6e1eaf0f25440c5e228db5302d67dba43e577e0a1b6", - "crates/perry-runtime/src/gc/cycle.rs": "9b992d27700382c84758f6f4a5574115a9862234bed2e18e0c5a3cca25c25236", - "crates/perry-runtime/src/gc/mod.rs": "d58fa9c46d87bfebb078875c2a35547e7a9257e81a53a3bcfd3f517385c19efd", - "crates/perry-runtime/src/gc/policy.rs": "84a869e0aa09e932a4d2b6601186260129ba851b4482f29579047713c60fc95e", + "crates/perry-runtime/src/gc/census.rs": "34d3054f47720478dfe90708c8a65895cb21d1e10d83b71dcce4742e3f480a59", + "crates/perry-runtime/src/gc/cycle.rs": "ecc6ff4833cd3a49ad2a4cb93df4de58032e0b0bacd07892263cc0043145c991", + "crates/perry-runtime/src/gc/mod.rs": "88e7c629cc7b825f311b26890b23b8609b807d27b0a9c002baf89543ecce3f71", + "crates/perry-runtime/src/gc/policy.rs": "59bd224294917f513b792398f9a399da577c9133653960a0c0b2d362cce19682", "crates/perry-runtime/src/gc/progress.rs": "a5ad3971bbe4047229ca57325234780daa85921dbc778e1c08dff4ad07ccfb96" } }