Skip to content

fix(gc): run the globalThis bootstrap in a no-move window (#7217) - #7249

Merged
proggeramlug merged 1 commit into
mainfrom
fix/7217-alloc-point-spread-rooting
Aug 2, 2026
Merged

fix(gc): run the globalThis bootstrap in a no-move window (#7217)#7249
proggeramlug merged 1 commit into
mainfrom
fix/7217-alloc-point-spread-rooting

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Closes #7217.

The answer to the question #7217 actually asked

the question to answer first is what differs about a collection initiated inside the allocating helper versus at a back-edge poll — the answer probably explains all three at once.

It does, and it is not what the issue guessed. The collection that was killing test_gap_gc_spread_accessor_rooting was not in js_object_assign_one, was not in interning, and was not in keys-array growth. It was minor #0 landing inside the lazy globalThis bootstrap.

js_get_global_this() builds the entire realm on first use. It is reached lazily — here from js_object_set_field_by_nameobject_prototype_addr_matchesjs_get_global_this_builtin_value, i.e. from an ordinary property write several hundred iterations into the loop, after ~8 MB of churn. The bootstrap then allocates ~1.15 MB of its own in one go, so under PERRY_GC_HEAP_LIMIT=8 the very first collection of the program lands in the middle of it.

That matters because the bootstrap builds a graph through raw pointers. intl::install_constructor holds ctor, proto and ns_obj as bare *mut ObjectHeader locals across dozens of allocating installs; so do the error, typed-array, generator, Reflect, Atomics and WebAssembly installers, across a dozen files. #6982 rooted the singleton — one pointer — and that is all it rooted.

PERRY_GC_PROTECT_FROMSPACE=1 (#7196) names the site without inference:

[gc-fromspace-protect] FAULT: signal 10 at 0x478b0f8fc60
  block=0x478b0e90000 +1047648 retired_bytes=1048536 retired_by_minor=#0
  last-known object: user_ptr=0x478b0f8fc68 obj_type=2 size=168
2  perry_runtime::object::descriptor_state::set_builtin_property_attrs
3  perry_runtime::intl::install_function + 576
4  perry_runtime::intl::install::install_constructor + 564
5  perry_runtime::intl::install_intl_namespace + 1828
6  ...global_this::populate::populate_global_this_builtins + 13768
7  js_get_global_this + 220
8  js_get_global_this_builtin_value + 72
9  perry_runtime::array::indexing::object_prototype_addr_matches + 156
10 js_object_set_field_by_name + 1004
11 test_gap_gc_spread_accessor_rooting_ts____AnonShape_..._constructor + 208

retired_by_minor=#0, deterministic 10/10 — the signature CLAUDE.md records for a table rather than a register, and here it is a whole subsystem's worth of them.

Confirmed independently before writing any code: adding one line, 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 on the allocation-point arm, with 6 copying minors and 4 613–5 797 objects copied each. The spread path was never the bug.

Why the safepoint route could not see it

A back-edge poll fires only while user JS is running, and the bootstrap runs no user JS. So on the loop_polls route not one of those installer locals is ever live across a collection. On the allocation-point route the bootstrap's own allocations are the collection points, so the whole graph is exposed at once.

That is the general statement #7217's second comment asked for: 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. Three fixes verified green there and red here were not three flawed fixes; two of the three were failing on a collection in code they had never touched.

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, and gc_root_dominance_check.py reads emitted LLVM IR so it is structurally blind to all of them. The window is one line and is provably enough. It costs nothing a collection would have recovered: every object born in the window is reachable from globalThis for the life of the thread, so a collection inside it frees nothing.

The change

One line: crate::gc::GcSuppressScope (the existing RAII no-move window, nesting-safe, 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 alloc-point-only. No collector behaviour changes anywhere else, and no env knob is added.

No installer's rooting was touched. Adding a RuntimeHandleScope to one of fifty installers would imply the other forty-nine are fine.

Two sibling windows were written and then deliberately dropped — see #7251. ensure_generator_intrinsics and ensure_typed_array_intrinsic build the same shape of immortal tower 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 may reach no gc_check_trigger at all — three successive gate designs for them PASSED with the window deleted (an armed-collection gate; the same with a block-boundary pre-fill and a re-exec into a virgin process, because the intrinsic slots are process-global and libtest's ordering meant array::tests built the towers first; and a #[cfg(test)] observer of gc_is_suppressed() inside the builder, which reported suppressed even with the scope removed, unexplained). 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 with both candidate gate designs written up rather than shipped ungated.

Verification

Same host, idle, --profile perry-dev, one target dir, 10 runs per cell, at
PERRY_GC_HEAP_LIMIT=8 PERRY_GC_INCREMENTAL=0 PERRY_CONSERVATIVE_STACK_SCAN=off
with no compile-time GC env — the allocation-point arm #7217 names.

The base arm was produced by reverting the three source files, rebuilding, and it came back bit-identical to the pre-change build (perry md5 6142b49fcf7f29f0f7ee9ab09ccc2f2d both times), while the fixed build is 068af60471ba94006e4fe6ac828e326d. The two arms are demonstrably different binaries.

witness base 8b024958f this PR
test_gap_gc_spread_accessor_rooting (#7217) 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
test_gap_gc_assign_string_source_rooting bad char 3 count 3 10/10 bad char 1 count 1 10/10 — not green, see below
test_gap_gc_regexp_receiver_rooting exit=139 10/10 exit=139 10/10 — unchanged, see below

Other arms, this PR:

arm result
loop_polls (compiled and run with PERRY_GC_MOVING_LOOP_POLLS=1, +FORCE_EVACUATE=1) all five witnesses green, 5/5 each
shipped default (no env) bad plain 0 hot 0 tail 0, 3/3
node --experimental-strip-types oracle bad plain 0 hot 0 tail 0 — byte-exact
PERRY_GC_PROTECT_FROMSPACE=1 on the reproducer no fault at all (was: fault at intl::install_function, retired_by_minor=#0)
gc_root_dominance_check.py (default + --unrooted-allocas) 0 violations before and after — it reads emitted LLVM IR and is structurally blind to this bug, exactly as expected

Cost — the window defers one collection, it does not add one:

console.log("hi", typeof globalThis.Intl) base this PR
peak RSS (3 runs) 10 878 976 / 10 895 360 / 10 878 976 10 649 600 / 10 649 600 / 10 633 216
GC cycles at HEAP_LIMIT=8 2 1

RSS is ~230 KB lower: the collection that used to run mid-bootstrap copied the bootstrap's own live set and then immediately had it all survive anyway.

cargo test -p perry-runtime: 1637 passed, 0 failed (--test-threads=1, which is how CI runs this crate — RUST_TEST_THREADS=1 at .github/workflows/test.yml:565). The crate's parallel-mode flakes are pre-existing and measured at the same 2–4/run with and without the new tests.

The gate

crates/perry-runtime/src/gc/tests/global_bootstrap.rs, in the per-PR cargo-test surface (a --lib unit test, not a tests/*.rs integration suite, which per CLAUDE.md would only run nightly).

It arms one pending collection, runs the bootstrap, and asserts it was not serviced, that the request is deferred rather than dropped (leaving it unserviced-and-unset would disable the trigger for the rest of the thread), 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.

Then it 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:

global_this_bootstrap_runs_in_a_no_move_window ... FAILED
  a collection ran inside the globalThis bootstrap — every installer local
  (`ctor`, `proto`, `ns_obj`, …) is now a from-space address
  left: 1   right: 0

The two witnesses this does NOT fix, and why their triage is retargeted rather than deleted

test-parity/gc_repsel_triage.txt says "DELETE ALL TEN when #7217 is fixed" for each. Both are still red, so the twenty entries are retargeted with their real causes — and one of the two triage texts was asserting a cause that is now known to be wrong.

Both remain hard gates on loop_polls, which gc-moving-witnesses.yml runs on every collector-touching PR, and both are green there 5/5.

#7161 / #7154

Not claimed, and here is exactly what was and was not measured.

I could not find the sfw-registry workload anywhere on this machine, so #7154's own symptom was not run and I am not asserting it is resolved. That check still belongs to whoever proposes the #7161 revert.

What is measured: in the configuration a #7161 revert would ship — compiled and run with PERRY_GC_MOVING_LOOP_POLLS=1, plus PERRY_GC_FORCE_EVACUATE=1 — all five test_gap_gc_*_rooting witnesses are green 5/5 on this branch. And this PR removes one of the two reasons #7217 gave for calling the revert-readiness statement incomplete: the object-spread path is now verified on the allocation-point route, not merely at safepoints. #7247 and #7248 remain open on that route, but neither is a shipped configuration — the allocation-point relocating arm requires an explicit PERRY_CONSERVATIVE_STACK_SCAN=off, which the matrix header itself calls a measurement configuration and not one anyone ships.

Summary by CodeRabbit

  • Bug Fixes

    • Improved garbage-collection safety while initializing global built-ins, preventing potential allocation-related crashes during startup.
    • Ensured deferred collection requests are handled correctly after initialization completes.
  • Tests

    • Added coverage for global initialization across allocation boundaries and pending collection scenarios.
    • Updated garbage-collection test tracking to reflect resolved and remaining issues.
  • Documentation

    • Added release notes describing the fix, validation results, and known follow-up items.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@proggeramlug, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 23 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c6d4783f-7edc-45e5-9dc3-48389c5a88b8

📥 Commits

Reviewing files that changed from the base of the PR and between 6683473 and e09de5b.

📒 Files selected for processing (6)
  • changelog.d/7249-realm-bootstrap-no-move-window.md
  • crates/perry-runtime/src/gc/tests/global_bootstrap.rs
  • crates/perry-runtime/src/gc/tests/mod.rs
  • crates/perry-runtime/src/object/global_this/populate.rs
  • test-parity/gc_repsel_corpus.txt
  • test-parity/gc_repsel_triage.txt
📝 Walkthrough

Walkthrough

The change adds a GcSuppressScope around globalThis built-in bootstrap, adds fresh-thread regression coverage for deferred collections, and updates GC witness records and release notes with validation results and remaining failures.

Changes

Global bootstrap GC safety

Layer / File(s) Summary
Bootstrap no-move scope
crates/perry-runtime/src/object/global_this/populate.rs
populate_global_this_builtins suppresses moving collections throughout built-in construction.
Bootstrap regression coverage
crates/perry-runtime/src/gc/tests/global_bootstrap.rs, crates/perry-runtime/src/gc/tests/mod.rs
The test arms pending GC, crosses an arena-block boundary during bootstrap, verifies suppression ends, and confirms later allocation services the collection.
GC parity triage updates
test-parity/gc_repsel_corpus.txt, test-parity/gc_repsel_triage.txt
Bootstrap-related witnesses are marked green. Remaining Object.assign and regexp failures are retargeted to issues 7248 and 7247.
Validation and release notes
changelog.d/7249-realm-bootstrap-no-move-window.md
The changelog records failure details, validation, performance measurements, test coverage, and excluded intrinsic windows.

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

Sequence Diagram(s)

sequenceDiagram
  participant BootstrapTest
  participant populate_global_this_builtins
  participant GcSuppressScope
  participant GCAllocator
  BootstrapTest->>GCAllocator: arm pending collection
  BootstrapTest->>populate_global_this_builtins: initialize globalThis
  populate_global_this_builtins->>GcSuppressScope: enter no-move scope
  populate_global_this_builtins->>GCAllocator: allocate built-in objects
  GCAllocator-->>GcSuppressScope: defer collection
  populate_global_this_builtins-->>BootstrapTest: complete bootstrap
  BootstrapTest->>GCAllocator: perform ordinary allocation
  GCAllocator-->>BootstrapTest: service deferred collection
Loading

Possibly related issues

  • PerryTS/perry issue 6991: Both changes address GC hazards during lazy globalThis bootstrap.
  • PerryTS/perry issue 7248: The bootstrap fix reduces related failures, but the stale js_eq operand remains separate.

Possibly related PRs

  • PerryTS/perry#6994: Both modify populate_global_this_builtins for GC-safe bootstrap.
  • PerryTS/perry#7050: This change extends related arena-allocation and GC-trigger work with deferred collection testing.
  • PerryTS/perry#7228: Both update GC witness results and triage for allocation-point failures.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR addresses #7217 by preventing allocation-point relocation during globalThis bootstrap and adds a regression test for deferred collection behavior.
Out of Scope Changes check ✅ Passed The code, regression test, release note, and triage updates directly support #7217 and document related unresolved witness failures.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The title clearly and concisely identifies the GC fix and the no-move window used during globalThis bootstrap.
Description check ✅ Passed The description provides detailed context, changes, related issue, test results, limitations, and verification evidence, although it does not follow the template headings exactly.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/7217-alloc-point-spread-rooting

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

proggeramlug pushed a commit that referenced this pull request Aug 2, 2026
@proggeramlug
proggeramlug force-pushed the fix/7217-alloc-point-spread-rooting branch from f7f84fb to 4870d65 Compare August 2, 2026 10:36

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with 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.

Inline comments:
In `@crates/perry-runtime/src/gc/tests/global_bootstrap.rs`:
- Around line 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.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c947c769-41fe-4a6c-b669-48d697a67bad

📥 Commits

Reviewing files that changed from the base of the PR and between 8b02495 and f7f84fb.

📒 Files selected for processing (8)
  • changelog.d/7249-realm-bootstrap-no-move-window.md
  • crates/perry-runtime/src/gc/tests/global_bootstrap.rs
  • crates/perry-runtime/src/gc/tests/mod.rs
  • crates/perry-runtime/src/object/global_this/generator.rs
  • crates/perry-runtime/src/object/global_this/populate.rs
  • crates/perry-runtime/src/object/global_this/typed_array.rs
  • test-parity/gc_repsel_corpus.txt
  • test-parity/gc_repsel_triage.txt

Comment on lines +111 to +117
// 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})"
);

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.

@proggeramlug
proggeramlug force-pushed the fix/7217-alloc-point-spread-rooting branch from 4870d65 to 6683473 Compare August 2, 2026 11:06
@proggeramlug proggeramlug changed the title fix(gc): run the lazy realm bootstraps in a no-move window (#7217) fix(gc): run the globalThis bootstrap in a no-move window (#7217) Aug 2, 2026
@proggeramlug
proggeramlug force-pushed the fix/7217-alloc-point-spread-rooting branch from 6683473 to 4609344 Compare August 2, 2026 11:08
`test_gap_gc_spread_accessor_rooting` SIGSEGV'd 10/10 on the allocation-point
route long after three rooting fixes had been verified green at safepoints. The
failing collection was in none of the code they touched: `js_get_global_this()`
builds the whole realm lazily — here reached from an ordinary property write
several hundred loop iterations in — allocates ~1.15 MB doing it, and under an
8 MB heap limit minor #0 lands in the middle of it. #6982 rooted the singleton,
which is one pointer. The bootstrap builds a graph, threading `ctor`, `proto`
and `ns_obj` as bare `*mut ObjectHeader` locals across dozens of allocating
installs in a dozen installer modules.

`PERRY_GC_PROTECT_FROMSPACE=1` names it: `set_builtin_property_attrs` <-
`intl::install_function` <- `install_constructor` <- `install_intl_namespace`
<- `populate_global_this_builtins`, `retired_by_minor=#0`. Confirmed before any
code changed: warming `globalThis` at the top of the unmodified reproducer makes
it clean 5/5 with six copying minors.

A back-edge poll fires only while user JS runs and the bootstrap runs none, so
the safepoint route can never expose those locals; the allocation-point route
makes the bootstrap's own allocations the collection points and exposes all of
them at once.

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 is unbounded and ungateable (the dominance checker reads LLVM IR and is
blind to all of them); the window is one line and costs nothing a collection
would have recovered, since everything born there lives for the life of the
thread.

Allocation-point arm, 10 runs each, base rebuilt bit-identically for the A/B:
  spread_accessor_rooting    exit=139 10/10  ->  clean 10/10, quarantine silent
  static_block_this_rooting  `bad 1`  10/10  ->  `bad 0` 10/10
loop_polls: all five witnesses green 5/5, all still relocating. RSS on a
globalThis-touching hello drops ~230 KB and one GC cycle — the window defers a
collection, it does not add one.

Gated by a `--lib` unit test that arms one pending collection, shows the
bootstrap does not service it, and then shows the same armed request IS serviced
by ordinary allocation once the window closes. Sabotage-checked.

Windows for the two sibling `ensure_*` intrinsic-tower builders were written and
then deliberately dropped: a tower fits inside one arena block's tail and may
reach no `gc_check_trigger` at all, so three successive gate designs PASSED with
the window deleted. Tracked as #7251 rather than shipped ungated.

Two other witnesses stay red on that route for unrelated, now-localized reasons
(#7247 `js_regexp_new` `&str` borrows, #7248 stale `js_eq` left operand); their
triage entries are retargeted rather than deleted.
@proggeramlug
proggeramlug force-pushed the fix/7217-alloc-point-spread-rooting branch from 4609344 to e09de5b Compare August 2, 2026 11:10
@proggeramlug
proggeramlug merged commit 64c1f56 into main Aug 2, 2026
7 of 9 checks passed
@proggeramlug
proggeramlug deleted the fix/7217-alloc-point-spread-rooting branch August 2, 2026 11:14
This was referenced Aug 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GC: #7207's spread-accessor reproducer still SIGSEGVs on main under the evac_minor arm (allocation-point relocation, not the safepoint route)

1 participant