Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions changelog.d/7249-realm-bootstrap-no-move-window.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
### Fixed

- **GC: the lazy `globalThis` bootstrap now runs in a no-move window (#7217).**
`test_gap_gc_spread_accessor_rooting` — #7207's reproducer for #7200 —
SIGSEGV'd 10/10 deterministically under
`PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off`
(the allocation-point route) months after three separate rooting fixes had
been verified green on the safepoint route. **The failing collection was not
in the code any of those fixes touched.**

`js_get_global_this()` builds the whole realm on first use, and it is reached
*lazily* — in this program from `js_object_set_field_by_name` →
`object_prototype_addr_matches` → `js_get_global_this_builtin_value`, i.e.
from an ordinary property write several hundred loop iterations in, after
~8 MB of churn. The bootstrap then allocates ~1.15 MB of its own, so under an
8 MB heap limit **minor #0 lands in the middle of it**. #6982 rooted the
`globalThis` singleton — one pointer. The bootstrap builds a *graph*:
`intl::install_constructor` threads `ctor`, `proto` and `ns_obj` as bare
`*mut ObjectHeader` locals across dozens of allocating installs, and so do the
error, typed-array, generator, Reflect, Atomics and WebAssembly installers,
across a dozen files. Every one is a slot the collector does not rewrite.

`PERRY_GC_PROTECT_FROMSPACE=1` (#7196) named it without inference:
`set_builtin_property_attrs` ← `intl::install_function` ←
`install_constructor` ← `install_intl_namespace` ←
`populate_global_this_builtins` ← `js_get_global_this`, on an address with
`retired_by_minor=#0`. Confirmed before any code changed: adding
`const __warm = typeof (globalThis as any).Intl;` at the top of the
*unmodified* reproducer — so the bootstrap runs while the arena is nearly
empty — makes it clean 5/5 with 6 copying minors and 4 613–5 797 objects
copied each.

**Why the safepoint route could not see it, which is the general finding.** A
loop back-edge poll fires only while user JS is running, and the bootstrap
runs no user JS, so none of those locals is ever live across a collection
there. On the allocation-point route the bootstrap's *own* allocations are the
collection points, so the entire graph is exposed at once. The two routes are
not two chances to catch the same bug: `loop_polls` cannot expose an unrooted
local in any runtime code that does not re-enter user JS, which is most of the
runtime.

**The invariant: a bootstrap that builds an IMMORTAL object graph through raw
pointers held across its own allocations must run in a NO-MOVE WINDOW.**
Rooting each holder individually is unbounded (hundreds of sites) and
ungateable — `scripts/gc_root_dominance_check.py` reads emitted LLVM IR and is
structurally blind to all of them. The window is one line and provably enough,
and it costs nothing a collection would have recovered: every object born in
it is reachable from `globalThis` for the life of the thread.

The fix is one line: `crate::gc::GcSuppressScope` (the existing nesting-safe
RAII no-move window, already used by `descriptor_state.rs`) at the top of
`populate_global_this_builtins`. `GC_FLAG_SUPPRESSED` gates
`gc_check_trigger`, the budgeted stepper **and** `gc_safepoint_moving_minor`,
so the window is comprehensive rather than allocation-point-only. No
installer's rooting was touched — adding a `RuntimeHandleScope` to one of
fifty installers would imply the other forty-nine are fine. No env knob is
added and no collector behaviour changes anywhere else.

Measured on the allocation-point arm, same host, idle, one target dir, 10 runs
per cell. The base arm was produced by reverting the source change and
rebuilding, and came back **bit-identical** to the pre-change build
(`perry` md5 `6142b49f…` both times) against `068af604…` for the fix, so the
two arms are demonstrably different binaries. Every row was then re-run
against the final shipped tree (`f1f002f8…`, after the two #7251 windows were
dropped) and is unchanged:

| witness | base `8b024958f` | fixed |
|---|---|---|
| `test_gap_gc_spread_accessor_rooting` | **exit=139, no output, 10/10** | **`bad plain 0 hot 0 tail 0` 10/10** |
| `test_gap_gc_static_block_this_rooting` | `bad 1` 10/10 | **`bad 0` 10/10** |
| `test_gap_gc_inline_ctor_this_rooting` | green 10/10 | green 10/10 |

`loop_polls` (compiled **and** run with `PERRY_GC_MOVING_LOOP_POLLS=1` plus
`PERRY_GC_FORCE_EVACUATE=1`): all five `test_gap_gc_*_rooting` witnesses green
5/5, and all five still relocate (1–5 cycles, 224–26 063 objects copied), so
none went inert. Shipped default: clean 3/3, byte-exact against
`node --experimental-strip-types`. `PERRY_GC_PROTECT_FROMSPACE=1` on the
reproducer now reports **no fault at all**. The static
`gc_root_dominance_check.py` reports 0 violations before *and* after, in both
its default and `--unrooted-allocas` modes — it reads emitted LLVM IR, so it
is structurally blind to a bug that lives in the runtime's Rust locals, which
is worth recording as a limit of that gate rather than as a clean bill.

**The window defers a collection, it does not add one.** On
`console.log("hi", typeof globalThis.Intl)`, peak RSS drops from
10 878 976 / 10 895 360 / 10 878 976 bytes to
10 649 600 / 10 649 600 / 10 633 216, and GC cycles at `HEAP_LIMIT=8` go from
2 to 1 — the collection that used to run mid-bootstrap copied the bootstrap's
own live set and then had all of it survive anyway.

### Testing

- **`crates/perry-runtime/src/gc/tests/global_bootstrap.rs`** — a `--lib` unit
test, so it runs in the per-PR `cargo-test` gate rather than in a
nightly-only `tests/*.rs` suite. It arms **one** pending collection, runs
the bootstrap, and asserts it was not serviced, that the request is
**deferred rather than dropped**, that the window spans at least one arena
block (so `arena_alloc_gc` genuinely reached `gc_check_trigger` inside it),
and that the window closed. Each then runs **the control**: the *same* armed
request, on the *same* thread, must be serviced by ordinary allocation once
the window is over. Without that second half the test would pass on a tree
where nothing was ever due — CLAUDE.md's fourth way a gate cannot fail.
Sabotage-checked in the failing direction: removing the `GcSuppressScope`
reddens it with `left: 1, right: 0` and the message naming the installer
locals.

### Known issues

- Two of the five `test_gap_gc_*_rooting` witnesses remain red on the
allocation-point route for **unrelated** reasons, and their twenty
`test-parity/gc_repsel_triage.txt` entries are retargeted rather than deleted
— one of the two triage texts was asserting a cause now known to be wrong.
Both are green on `loop_polls`, which `gc-moving-witnesses.yml` gates.
- **#7247** — `test_gap_gc_regexp_receiver_rooting`, unchanged (exit=139 10/10
on both arms). `js_regexp_new` holds `string_as_str(pattern)` /
`string_as_str(flags)` — `&str` borrows into a movable `StringHeader`
payload — across its whole body. The #7215 borrow shape.
- **#7248** — `test_gap_gc_assign_string_source_rooting`, improved from
`bad char 3 count 3` to `bad char 1 count 1` (10/10 each). The residual
failure is a stale `js_eq` left operand in the test's own
`got !== ALPHA[i % 26]` assertion — a register loaded above the allocating
`js_string_index_get_boxed` sibling and never re-read — not anything in
`js_object_assign_one`. The #7206/#7214 operand family.
- **#7251** — the same defect shape exists in `ensure_generator_intrinsics` and
`ensure_typed_array_intrinsic`, which build the same kind of immortal tower
through the same kind of raw locals and are *also* reachable lazily ahead of
the bootstrap. Windows for them were written and then **deliberately dropped
from this PR**: a tower is three orders of magnitude smaller than the
bootstrap, fits inside one arena block's tail, and so may reach no
`gc_check_trigger` at all — three successive versions of a gate for them
passed with the window deleted. Shipping a GC-trigger change with no test that
can fail without it is the thing CLAUDE.md's knob-kill policy exists to stop,
so the exposure is tracked instead, with the two candidate gate designs and an
unexplained observation (something may already be suppressing across part of
the tower build) written up in the issue.
- **#7154's `sfw-registry --help` symptom was not run** (the workload is not
present on this machine) and is **not** claimed resolved. What is measured is
that the five witnesses are green in the configuration a #7161 revert would
ship.
157 changes: 157 additions & 0 deletions crates/perry-runtime/src/gc/tests/global_bootstrap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
//! #7217: the lazy `globalThis` bootstrap must run in a NO-MOVE WINDOW.
//!
//! `populate_global_this_builtins` constructs an IMMORTAL object graph —
//! everything it allocates is reachable from `globalThis` for the life of the
//! thread. It constructs it by threading raw `*mut ObjectHeader` /
//! `*mut ClosureHeader` locals through several hundred installs in a dozen
//! installer modules, each of which allocates. Those locals are slots the
//! collector does not rewrite, so a *relocating* collection inside the window
//! leaves the rest of the bootstrap writing into from-space.
//!
//! This was invisible on the safepoint route (`PERRY_GC_MOVING_LOOP_POLLS=1`):
//! a back-edge poll only fires while user JS runs, and the bootstrap runs no
//! user JS. It is reachable on the allocation-point route, where the
//! bootstrap's own block allocations are the collection points — which is why
//! `test_gap_gc_spread_accessor_rooting` still SIGSEGV'd 10/10 under
//! `PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0
//! PERRY_CONSERVATIVE_STACK_SCAN=off` after three rooting fixes that were all
//! green at safepoints.
//!
//! ***BOTH HALVES ARE ASSERTED*** (CLAUDE.md's fourth way a gate cannot fail).
//! A test that only checked "no collection ran during the bootstrap" would pass
//! on a tree where nothing was ever due. The test below therefore arms ONE
//! pending collection, shows the bootstrap did not service it, and then shows
//! that the SAME armed request is serviced by ordinary allocation once the
//! window has closed. Same thread, same lever, same magnitude — the only
//! variable is whether the allocations happened inside the window.
//!
//! SCOPE. Only `populate_global_this_builtins` is covered, and deliberately so.
//! `ensure_generator_intrinsics` / `ensure_typed_array_intrinsic` build the same
//! shape of immortal graph through the same kind of raw locals and are ALSO
//! reachable lazily, ahead of the bootstrap — but a tower is three orders of
//! magnitude smaller than the bootstrap, fits inside one arena block's tail, and
//! so may reach no `gc_check_trigger` at all. A version of this file that armed a
//! collection around them PASSED with their windows deleted. Rather than ship a
//! GC-trigger change with no test that can fail without it — the exact thing
//! CLAUDE.md's knob-kill policy exists to stop — those two windows were dropped
//! and the exposure is tracked separately.

use super::super::*;
use super::support::*;

/// Run `body` on a thread that has never touched `globalThis`, so the lazy
/// bootstrap really runs instead of returning the per-thread cache hit.
/// `THREAD_GLOBAL_THIS`, the arena, `GC_STATS` and `GC_OLD_RECLAIM_PENDING` are
/// all thread-local, so the arming and the measurement stay on this thread.
fn on_a_fresh_thread(body: impl FnOnce() + Send + 'static) {
std::thread::Builder::new()
.stack_size(16 << 20)
.spawn(body)
.expect("spawn bootstrap test thread")
.join()
.expect("bootstrap test thread panicked");
}

/// Make one collection due at the very next `gc_check_trigger()` — which
/// `arena_alloc_gc` calls every time the current block fills. This is the same
/// lever `scan_fallback.rs` uses, and it completes synchronously on the
/// allocation-point arm rather than deferring to a safepoint.
fn arm_one_pending_collection() {
GC_OLD_RECLAIM_PENDING.with(|pending| pending.set(true));
}

fn pending_collection_still_owed() -> bool {
GC_OLD_RECLAIM_PENDING.with(std::cell::Cell::get)
}

fn clear_pending_collection() {
GC_OLD_RECLAIM_PENDING.with(|pending| pending.set(false));
GC_SAFEPOINT_PENDING.with(|pending| pending.set(false));
let old_in_use = crate::arena::old_gen_in_use_bytes();
GC_LAST_OLD_RECLAIM_IN_USE_BYTES.with(|bytes| bytes.set(old_in_use));
}

/// THE CONTROL. Allocate ordinary young objects — nothing rooted, nothing
/// exotic — until either the armed collection is serviced or two arena blocks
/// have been consumed without it. Returns whether it was serviced.
///
/// Two blocks is deliberately more than the bootstrap window spans (measured:
/// ~1.15 MB, i.e. just over one 1 MB block), so a `false` here means the
/// arming is inert on this thread and the subject assertion above it proved
/// nothing.
fn ordinary_allocation_services_the_armed_collection(collections_before: u64) -> bool {
let arena_before = crate::arena::arena_total_bytes();
for _ in 0..500_000 {
let _ = young_leaf();
if gc_collection_count() > collections_before {
return true;
}
if crate::arena::arena_total_bytes() >= arena_before + (2 << 20) {
return false;
}
}
false
}

#[test]
fn global_this_bootstrap_runs_in_a_no_move_window() {
on_a_fresh_thread(|| {
// Pin the shipped pacing (#7161 flipped `PERRY_GC_MOVING_LOOP_POLLS`
// off) so this asserts against a declared mode rather than whatever the
// process-wide OnceLock happened to resolve to.
let _pacing = crate::gc::policy::force_legacy_gc_pacing();
crate::gc::ensure_gc_initialized();
clear_pending_collection();

arm_one_pending_collection();
let arena_before = crate::arena::arena_total_bytes();
let collections_before = gc_collection_count();

// THE SUBJECT: the one-shot realm bootstrap.
let global = crate::object::js_get_global_this();
assert!(
crate::value::JSValue::from_bits(global.to_bits()).is_pointer(),
"js_get_global_this must return a real singleton, else the \
bootstrap never ran and this test measured nothing"
);

let arena_after = crate::arena::arena_total_bytes();
let collections_after = gc_collection_count();

// LIVE SUBJECT, half 1: the window really did span a block boundary, so
// `arena_alloc_gc` really did reach `gc_check_trigger()` inside it.
assert!(
arena_after >= arena_before + (1 << 20),
"the bootstrap must consume at least one arena block for this test \
to say anything (before={arena_before} after={arena_after})"
);
Comment on lines +121 to +127

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# List the feature-gated installers inside the bootstrap window.
rg -nP --type=rust -B 2 -A 2 '#\[cfg\(feature' crates/perry-runtime/src/object/global_this/

# Check whether these features are default-on for the runtime crate.
fd -t f 'Cargo.toml' crates/perry-runtime --exec sed -n '/\[features\]/,/^\[/p'

Repository: PerryTS/perry

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate files =="
fd -t f 'global_bootstrap.rs|populate.rs|policy.rs|arena.rs' crates/perry-runtime

echo "== global_bootstrap outline =="
ast-grep outline crates/perry-runtime/src/gc/tests/global_bootstrap.rs --view expanded | sed -n '1,220p' || true

echo "== Relevant global_bootstrap region =="
cat -n crates/perry-runtime/src/gc/tests/global_bootstrap.rs | sed -n '80,140p'

echo "== Relevant generator_intrinsic_tower_runs_in_a_no_move_window snippet =="
rg -n "generator_intrinsic_tower_runs_in_a_no_move_window|arena_total_bytes|young_leaf|gc_check_trigger|force_legacy_gc_pacing" crates/perry-runtime/src/gc/tests -A 25 -B 10

echo "== Populate relevant region =="
cat -n crates/perry-runtime/src/object/global_this/populate.rs | sed -n '560,650p'

echo "== Feature snippets for global-* and temporal =="
rg -n 'global-math|global-json|global-reflect|global-atomics|temporal|global-url|global-text|global-webcrypto|global-webfetch|global-websocket' crates/perry-runtime/Cargo.toml crates/perry-runtime/src/object/global_this/populate.rs crates/perry-runtime/src/object/global_this/install_static.rs -A 3 -B 3

echo "== Arena API size constants =="
rg -n 'fn arena_total_bytes|pub const.*BLOCK|BLOCK_SIZE|BLOCK_SIZE_BYTES|allocate|arena_alloc' crates/perry-runtime/src/arena.rs crates/perry-runtime/src/gc crates/perry-runtime/src -g '*.rs' | sed -n '1,220p'

Repository: PerryTS/perry

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Candidate file sizes =="
wc -l crates/perry-runtime/src/gc/tests/global_bootstrap.rs crates/perry-runtime/src/object/global_this/populate.rs crates/perry-runtime/src/arena.rs crates/perry-runtime/Cargo.toml

echo "== global_bootstrap region =="
sed -n '80,140p' crates/perry-runtime/src/gc/tests/global_bootstrap.rs

echo "== Robust pattern references =="
rg -n "generator_intrinsic_tower_runs_in_a_no_move_window|arena_total_bytes|young_leaf|arm_one_pending_collection|gc_check_trigger|force_legacy_gc_pacing" crates/perry-runtime/src/gc/tests crates/perry-runtime/src/gc -g '*.rs' | sed -n '1,220p'

echo "== populate namespace installer region =="
sed -n '570,645p' crates/perry-runtime/src/object/global_this/populate.rs

echo "== Feature slices in Cargo.toml =="
sed -n '/^\[features\]/,/^$/p' crates/perry-runtime/Cargo.toml | sed -n '1,80p'

echo "== Targeted feature-gated global namespace installers in populate/install_static =="
rg -n --type=rust 'install_math_namespace|install_json_namespace_members|install_reflect_namespace_members|install_atomics_namespace_members|install_temporal_namespace|global-math|global-json|global-reflect|global-atomics|temporal' crates/perry-runtime/src/object/global_this/populate.rs crates/perry-runtime/src/object/global_this/install_static.rs crates/perry-runtime/Cargo.toml | sed -n '1,220p'

echo "== Arena total API =="
rg -n --type=rust 'pub .*fn .*arena_total_bytes|fn (?:(pub.*?) )?arena_total_bytes|BLOCK_SIZE|bytes_for|general_block' crates/perry-runtime/src/arena.rs crates/perry-runtime/src -g '*.rs' | sed -n '1,220p'

Repository: PerryTS/perry

Length of output: 415


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Arena files =="
fd -t f '^arena\.rs$' crates/perry-runtime src crates 2>/dev/null || true
fd -t f 'arena' crates/perry-runtime 2>/dev/null | sed -n '1,100p'

echo "== Arena total API in all tracked Rust files =="
rg -n --type=rust 'arena_total_bytes|arena_alloc_gc\(|general_block|BLOCK_SIZE|BYTES|bytes_for' crates/perry-runtime/src crates/perry-runtime -g '*.rs' | sed -n '1,240p'

echo "== global_bootstrap region =="
sed -n '80,140p' crates/perry-runtime/src/gc/tests/global_bootstrap.rs

echo "== Robust pattern references =="
rg -n --type=rust 'generator_intrinsic_tower_runs_in_a_no_move_window|arena_total_bytes|young_leaf|arm_one_pending_collection|gc_check_trigger|force_legacy_gc_pacing' crates/perry-runtime/src/gc crates/perry-runtime/src/gc/tests -g '*.rs' | sed -n '1,240p'

echo "== populate namespace installer region =="
sed -n '570,645p' crates/perry-runtime/src/object/global_this/populate.rs

echo "== Feature slices in Cargo.toml =="
sed -n '/^\[features\]/,/^$/p' crates/perry-runtime/Cargo.toml | sed -n '1,90p'

echo "== Targeted feature-gated namespace installers =="
rg -n --type=rust 'install_math_namespace|install_json_namespace_members|install_reflect_namespace_members|install_atomics_namespace_members|install_temporal_namespace|global-math|global-json|global-reflect|global-atomics|temporal' crates/perry-runtime/src/object/global_this/populate.rs crates/perry-runtime/Cargo.toml | sed -n '1,200p'

Repository: PerryTS/perry

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Feature-only tests and minimal feature names =="
rg -n --type=rust '#\[cfg\(.*test.*features|cfg(test)|cfg\(.*feature=.*full|features.*minimal|no-default-features|default.*global-json|default.*global-math|temporal' crates/perry-runtime tests -g '*.rs' -g 'tests/**/*.rs' 2>/dev/null | sed -n '1,240p'

echo "== targeted features in tests =="
fd -t f '.*\.rs$' crates/perry-runtime/src/gc/tests crates/perry-runtime/tests crates -x sh -c 'rg -n --type=rust "global-j|i|i|global-math|global-reflect|global-atomics|temporal|no-default-features|cfg\\(test" "$1" | sed -n "1,80p"' sh {}

echo "== global_bootstrap full helper and tests =="
sed -n '1,190p' crates/perry-runtime/src/gc/tests/global_bootstrap.rs

echo =="native_arena block size definitions =="
rg -n --type=rust 'GENERAL_.*BLOCK|NURSERY|BLOCK_SIZE|BLOCK_SIZE_BYTES|bytes_for|alloc_gen|arena_total_bytes' crates/perry-runtime/src/native_arena.rs | sed -n '1,220p'

echo "== native_arena relevant size implementation =="
sed -n '300,460p' crates/perry-runtime/src/native_arena.rs

Repository: PerryTS/perry

Length of output: 50370


Remove the 1-MB bootstrap footprint precondition.

global_this_bootstrap_runs_in_a_no_move_window requires the single one-shot bootstrap to exceed arena_before + 1 MB, but the feature-gated global namespace installers can reduce that allocation. Apply the pre-arm filler pattern from generator_intrinsic_tower_runs_in_a_no_move_window, so the subject’s own allocation can cross gc_check_trigger() even when the bootstrap is small.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/gc/tests/global_bootstrap.rs` around lines 111 -
117, Remove the 1-MB arena-growth assertion from
global_this_bootstrap_runs_in_a_no_move_window and apply the pre-arm filler
allocation pattern used by generator_intrinsic_tower_runs_in_a_no_move_window,
ensuring the filler positions the arena so the bootstrap subject allocation
crosses gc_check_trigger() without depending on bootstrap size.

// THE INVARIANT: nothing collected, and therefore nothing moved, while
// the installers held raw pointers.
assert_eq!(
collections_after, collections_before,
"a collection ran inside the globalThis bootstrap — every installer \
local (`ctor`, `proto`, `ns_obj`, …) is now a from-space address"
);
assert!(
pending_collection_still_owed(),
"the window must DEFER the request, not drop it: leaving it \
unserviced-and-unset would disable the trigger for the rest of \
the thread"
);
assert!(
!crate::gc::gc_is_suppressed(),
"the no-move window must close when the bootstrap returns"
);

// LIVE SUBJECT, half 2 — THE CONTROL. Same thread, same armed request,
// ordinary allocation. If this does not collect, the arming was inert
// and the assertion above is vacuous.
assert!(
ordinary_allocation_services_the_armed_collection(collections_after),
"the armed collection was never serviceable on this thread, so \
'the bootstrap did not collect' proved nothing"
);

clear_pending_collection();
});
}
1 change: 1 addition & 0 deletions crates/perry-runtime/src/gc/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mod error_side_tables;
mod evacuation;
mod fromspace_protect;
mod fromspace_scan;
mod global_bootstrap;
mod helper_stores;
mod host_safepoints;
mod incremental_sweep_reclaim;
Expand Down
37 changes: 37 additions & 0 deletions crates/perry-runtime/src/object/global_this/populate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,43 @@ pub(crate) fn populate_global_this_builtins(singleton_at_entry: *mut ObjectHeade
// Only reachable when the conservative native-stack scan is off, which is
// production's `Auto -> SkipDisabled` resolution; the scan was masking this
// by pinning the argument register.
//
// #7217: rooting the singleton is necessary but NOT sufficient, and the
// difference is the whole point of the allocation-point route.
//
// The singleton is one pointer. The bootstrap it drives is a *graph*:
// `install_intl_namespace` -> `install_constructor` -> `install_function`
// holds `ctor`, `proto` and `ns_obj` as raw `*mut ObjectHeader` locals
// across dozens of allocating installs, and so do the error, typed-array,
// generator, Reflect, Atomics, WebAssembly, … installers, in a dozen files
// and several hundred call sites. Every one of those is a slot the
// collector does not rewrite.
//
// On the SAFEPOINT route none of them can be exposed: a collection reached
// from a loop back-edge poll only happens while user JS is running, and the
// bootstrap runs no user JS. On the ALLOCATION-POINT route every one of the
// bootstrap's own ~1.15 MB of allocations is a collection point, so the
// whole graph is exposed at once. That is why three separate rooting fixes
// verified green on `PERRY_GC_MOVING_LOOP_POLLS=1` were still red under
// `PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0
// PERRY_CONSERVATIVE_STACK_SCAN=off`: the collection they were failing on
// was not in the code they had fixed, it was minor #0 landing inside this
// bootstrap. `PERRY_GC_PROTECT_FROMSPACE=1` names it exactly —
// `set_builtin_property_attrs` <- `intl::install_function` <-
// `install_constructor` <- `install_intl_namespace` <- here, on an address
// `retired_by_minor=#0`.
//
// THE INVARIANT: a bootstrap that builds an IMMORTAL object graph through
// raw pointers held across its own allocations must run in a NO-MOVE
// WINDOW. Rooting each holder individually is unbounded (hundreds of sites
// across a dozen installer modules) and ungateable (no checker can prove
// the set complete), while the window is one line and provably enough. It
// costs nothing a collection would have recovered: every object born here
// is reachable from `globalThis` for the life of the process, so a
// collection inside the window frees nothing. Measured footprint of the
// whole window: ~1.15 MB allocated, ~410 KB of it live afterwards, once per
// thread.
let _no_move = crate::gc::GcSuppressScope::new();
let scope = crate::gc::RuntimeHandleScope::new();
let singleton_handle = scope.root_raw_mut_ptr(singleton_at_entry);
let singleton = || singleton_handle.get_raw_mut_ptr::<ObjectHeader>();
Expand Down
Loading
Loading