feat(gc): seeded GC-schedule fuzzing (PERRY_GC_SCHEDULE_SEED) — a failing schedule you can replay - #7317
feat(gc): seeded GC-schedule fuzzing (PERRY_GC_SCHEDULE_SEED) — a failing schedule you can replay#7317jdalton wants to merge 1 commit into
PERRY_GC_SCHEDULE_SEED) — a failing schedule you can replay#7317Conversation
📝 WalkthroughWalkthroughThis change adds deterministic, rate-controlled GC schedule fuzzing. It integrates schedule-selected safepoints with minor collection and evacuation policy, adds exit and failure diagnostics, provides tests and fuzzing scripts, and documents configuration and reproduction workflows. ChangesSeeded GC schedule fuzzing
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Runtime
participant Safepoint
participant Schedule
participant Collector
participant Reporter
Runtime->>Schedule: resolve seed and rate
Safepoint->>Schedule: advance handled safepoint
Schedule-->>Safepoint: return collection selection
Safepoint->>Collector: perform moving minor collection
Collector->>Reporter: record schedule-forced collection
Reporter-->>Runtime: report seed and counters on exit or failure
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
2467132 to
5d8ce73
Compare
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (3)
crates/perry-runtime/src/gc/schedule.rs (1)
244-252: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAlign the "startup banner" claim with the actual announcement point.
The module documentation at Lines 331-334 describes layer 1 as "a startup banner, so the seed is in the log even if the failure mode is a hang or a
_exitthat runs no handler at all".resolved()runs the announcement lazily, at the first call site. For a mode-ON run, that is the first safepoint or the firstgc_force_evacuate_enabled()query. A hang or_exitbefore that point prints nothing, and no panic hook or signal handler is installed either.Consider resolving the configuration eagerly from GC initialization, or narrow the documentation claim to "the first safepoint" so an operator does not read a missing banner as "the seed was not set".
🤖 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/schedule.rs` around lines 244 - 252, The startup-banner documentation does not match the lazy announcement in resolved(). Either eagerly resolve the configuration during GC initialization so publish_seed and announce run before early hangs or _exit paths, or narrow the layer-1 documentation to state that the banner appears at the first safepoint or gc_force_evacuate_enabled() query; preserve the existing seed publication behavior.crates/perry-runtime/src/gc/tests/schedule.rs (1)
276-283: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winRelease the GC root lock with a guard so a panic cannot leak the depth.
enter_gc_root_lock()andexit_gc_root_lock()are paired manually. Ifgc_safepoint_moving_minor()panics,exit_gc_root_lock()never runs, the root-lock depth stays non-zero for this thread, and every later collection on that thread is blocked. That converts one failure into a cascade of confusing failures in the same test binary.♻️ Proposed fix using a scope guard
let safepoints_before = gc_schedule_safepoints(); { let _schedule = ScheduleGuard::set(7, rate_threshold(1.0)); reset_thread_counter_for_test(); - super::super::roots::enter_gc_root_lock(); - gc_safepoint_moving_minor(); - super::super::roots::exit_gc_root_lock(); + struct RootLock; + impl RootLock { + fn enter() -> Self { + super::super::roots::enter_gc_root_lock(); + Self + } + } + impl Drop for RootLock { + fn drop(&mut self) { + super::super::roots::exit_gc_root_lock(); + } + } + let _lock = RootLock::enter(); + gc_safepoint_moving_minor(); }If the test support module already exposes a root-lock guard type, use it instead of the local shim.
🤖 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/schedule.rs` around lines 276 - 283, Update the test block around gc_safepoint_moving_minor to use the existing GC root-lock scope guard, if exposed by the test support module, instead of manually pairing enter_gc_root_lock and exit_gc_root_lock. Ensure the guard releases the lock during unwinding as well as normal completion, and remove the corresponding explicit exit call.docs/src/internals/gc-rooting-invariant.md (1)
271-279: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueKeep the
gc_schedule_fuzz.shargument syntax deterministic.The script accepts
<binary> [seed-count], butCLAUDE.mdstill says[seeds]. Update that line so the two docs use the actual positional argument semantics.🤖 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 `@docs/src/internals/gc-rooting-invariant.md` around lines 271 - 279, Update the gc_schedule_fuzz.sh usage text in CLAUDE.md to describe the second positional argument as seed-count, matching the script’s actual <binary> [seed-count] semantics. Also review the usage reference in docs/src/internals/gc-rooting-invariant.md and changelog.d/7307-seeded-gc-schedule-fuzzing.md at the specified ranges; update any remaining [seeds] wording there to [seed-count], with no other changes.
🤖 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 `@CLAUDE.md`:
- Around line 145-146: Add a required CI workflow arm for the seeded GC schedule
OFF state, using a compiled program to test both an unset PERRY_GC_SCHEDULE_SEED
and PERRY_GC_SCHEDULE_RATE set without a seed. Verify both remain schedule-inert
while pressure-driven collections still occur, reusing the existing
scripts/gc_schedule_fuzz.sh or schedule test infrastructure where appropriate.
In `@crates/perry-runtime/src/gc/mod.rs`:
- Around line 778-784: The exit summary is emitted during per-thread teardown,
so SUMMARY_EMITTED can capture counts before other threads finish. Update the
report_exit_summary call in the exit path to emit only after all worker threads
have completed teardown—prefer the existing main-thread or final-thread
coordination mechanism—and preserve once-only reporting with complete safepoints
and scheduled_collections totals.
In `@crates/perry-runtime/src/gc/schedule.rs`:
- Around line 240-243: Add a required CI workflow arm that runs the GC schedule
tests or relevant test suite with PERRY_GC_SCHEDULE_SEED and
PERRY_GC_SCHEDULE_RATE unset, verifying their default/OFF behavior alongside
existing CI coverage. Anchor the change to the workflow job invoking the tests
and preserve the current configured-knob coverage.
- Around line 584-608: In the signal-handler teardown around the previous
handler lookup, restore SIG_DFL before entering the previous > 1 chaining path,
so the default disposition is installed before invoking the chained handler.
Keep the existing chained-handler call and early return, but remove the
later-only restoration structure so schedule_fault_handler cannot loop when the
chained handler returns.
- Around line 493-502: Update the previous-handler storage in
reinstall_signal_reporter_after to check old.sa_flags for libc::SA_SIGINFO
before saving old.sa_sigaction. Store 0 for handlers without SA_SIGINFO, while
preserving the existing self-chain prevention and storing the handler value only
when the flag is present.
In `@docs/src/internals/gc-rooting-invariant.md`:
- Around line 281-285: Update the paragraph beginning “A rate is not a
substitute for a schedule” to qualify the ~3/N confidence bound as applying only
to independent trials. State that repeated runs with a fixed seed or
deterministic schedule are correlated, so 0/N failures provide no statistical
bound, while preserving the guidance to vary collection timing.
In `@docs/src/internals/memory-model.md`:
- Around line 138-139: Update the PERRY_GC_SCHEDULE_SEED and
PERRY_GC_SCHEDULE_RATE documentation to describe the configured rate as
additional schedule density for minor collections only, applied when
gc_budgeted_due_trigger() reports no pressure-driven collection is due. Clarify
that pressure-driven collections still occur independently, so the rate is not
the total fraction of safepoints that collect, and replace the current “iff”
wording with this behavior.
In `@scripts/gc_instrument_smoke.sh`:
- Around line 119-129: Replace the `run_arm ... | tail -1` command substitutions
for `sched_retired`, `sched_repeat`, and `sched_other` with output capture that
does not use a pipeline, then explicitly check each `run_arm` exit status and
abort on failure before comparing results. Apply the same status-preserving
change to the other arms in this script that use the pipeline pattern, while
retaining extraction of the final output line.
- Around line 150-164: The strict schedule-density checks in the smoke fixture
can fail on low safepoint counts without demonstrating a broken rate knob.
Update the fixture to generate enough handled GC safepoints for distinct
retirement counts, or revise both failure paths around sched_retired,
nozeal_retired, and zeal_retired to report all three counts before exiting.
In `@scripts/gc_schedule_fuzz.sh`:
- Around line 53-59: Validate SEED_COUNT immediately after argument parsing as a
positive integer, rejecting zero and non-numeric values with an error and
nonzero exit. In the final summary around FAILED_SEEDS and the PASS output,
track executed runs via passed plus failed seeds and exit nonzero with a failure
message when that total is zero; only report PASS after at least one seed ran.
---
Nitpick comments:
In `@crates/perry-runtime/src/gc/schedule.rs`:
- Around line 244-252: The startup-banner documentation does not match the lazy
announcement in resolved(). Either eagerly resolve the configuration during GC
initialization so publish_seed and announce run before early hangs or _exit
paths, or narrow the layer-1 documentation to state that the banner appears at
the first safepoint or gc_force_evacuate_enabled() query; preserve the existing
seed publication behavior.
In `@crates/perry-runtime/src/gc/tests/schedule.rs`:
- Around line 276-283: Update the test block around gc_safepoint_moving_minor to
use the existing GC root-lock scope guard, if exposed by the test support
module, instead of manually pairing enter_gc_root_lock and exit_gc_root_lock.
Ensure the guard releases the lock during unwinding as well as normal
completion, and remove the corresponding explicit exit call.
In `@docs/src/internals/gc-rooting-invariant.md`:
- Around line 271-279: Update the gc_schedule_fuzz.sh usage text in CLAUDE.md to
describe the second positional argument as seed-count, matching the script’s
actual <binary> [seed-count] semantics. Also review the usage reference in
docs/src/internals/gc-rooting-invariant.md and
changelog.d/7307-seeded-gc-schedule-fuzzing.md at the specified ranges; update
any remaining [seeds] wording there to [seed-count], with no other changes.
🪄 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: 77947a25-2bab-41b8-b287-31da05030b5f
📒 Files selected for processing (12)
CLAUDE.mdchangelog.d/7307-seeded-gc-schedule-fuzzing.mdcrates/perry-runtime/src/arena/quarantine.rscrates/perry-runtime/src/gc/mod.rscrates/perry-runtime/src/gc/policy.rscrates/perry-runtime/src/gc/schedule.rscrates/perry-runtime/src/gc/tests/mod.rscrates/perry-runtime/src/gc/tests/schedule.rsdocs/src/internals/gc-rooting-invariant.mddocs/src/internals/memory-model.mdscripts/gc_instrument_smoke.shscripts/gc_schedule_fuzz.sh
| | `PERRY_GC_SCHEDULE_SEED=<u64>` | seeded GC-schedule fuzzing — the middle setting between normal pacing and zeal. Three things, exactly: (1) `js_gc_loop_safepoint` stops requiring `GC_SAFEPOINT_PENDING` before descending into `gc_safepoint_moving_minor`, the same bypass zeal performs; (2) inside `gc_safepoint_moving_minor`, **past the entry guards**, a per-thread safepoint counter advances once per handled safepoint and, when `gc_budgeted_due_trigger()` reports nothing due, a minor runs anyway iff `splitmix64(splitmix64(seed) ^ counter) < threshold`; (3) `gc_force_evacuate_enabled()` becomes true, so survivors MOVE. **A value that does not parse as `u64` reads as OFF, not as seed 0.** The seed is printed at startup, at `atexit`, and on panic/SIGSEGV/SIGBUS/SIGABRT/SIGILL/SIGTRAP — the signal reporter chains to (and is re-layered on top of) the from-space quarantine's, so the two compose. Live-subject counters: `gc::gc_schedule_safepoints()` / `gc::gc_schedule_forced_collections()`. | bypass `gc_safepoint_moving_minor`'s entry guards — and a blocked safepoint deliberately does **not** tick the counter, so the ordinal sequence tracks the program's safepoints rather than its allocation state. Nor override `PERRY_GEN_GC_EVACUATE=0`. Nor emit loop polls (compile-time `PERRY_GC_MOVING_LOOP_POLLS=1`, as for zeal). Nor *suppress* pressure-driven collections — the rate is additional density, never less. Determinism is **per-thread**: the counter is thread-local, so a single-threaded program replays exactly, while a `perry/thread` program is only as reproducible as its OS scheduling. Say which you measured. | | ||
| | `PERRY_GC_SCHEDULE_RATE=<0..1>` (default `0.05`) | **only** the threshold `PERRY_GC_SCHEDULE_SEED`'s hash is compared against — the expected fraction of handled safepoints that collect. Out-of-range values clamp (a `2` reads as 1.0); unparseable and NaN fall back to the default. | do anything at all without a seed. It is inert alone. `=0` is an on-but-selects-nothing control (banner and reporters still install), `=1` is zeal's density. | |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
rg -n -C 4 \
'PERRY_GC_SCHEDULE_(SEED|RATE)|gc_schedule_fuzz|gc_instrument_smoke|unset .*PERRY_GC_SCHEDULE|env -u PERRY_GC_SCHEDULE' \
.github scripts crates || trueRepository: PerryTS/perry
Length of output: 28592
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo "== workflow names around relevant steps =="
sed -n '1060,1110p' .github/workflows/test.yml
echo
echo "== scripts/gc_schedule_fuzz.sh key parsing and loop =="
sed -n '1,180p' scripts/gc_schedule_fuzz.sh
echo
echo "== scripts/gc_instrument_smoke.sh pre/post comparisons =="
sed -n '1,190p' scripts/gc_instrument_smoke.sh
echo
echo "== schedule tests =="
sed -n '1,260p' crates/perry-runtime/src/gc/tests/schedule.rs
echo
echo "== env var searches for schedule in CI files =="
rg -n -C 3 'PERRY_GC_SCHEDULE|schedule|gc_schedule_fuzz|gc_instrument_smoke|required.*gate|gate.*required' .github scripts crates/perry-runtime/src/gc/tests/schedule.rs || trueRepository: PerryTS/perry
Length of output: 50369
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo "== workflow files and schedule references =="
git ls-files .github/workflows | sort
echo
rg -n -C 3 \
'(^name: .*|cargo test.*gc.*schedule|PERRY_GC_SCHEDULE_(SEED|RATE)|gc_instrument_smoke|gc_schedule_fuzz|PERRY_GC_SCHEDULE_SEED=|PERRY_GC_SCHEDULE_RATE=)' \
.github/workflows || trueRepository: PerryTS/perry
Length of output: 143
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo "== workflow file count and names =="
git ls-files .github/workflows | sort | awk '{print NR": "$0}'
echo
echo "== workflow schedule references (counted and summarized) =="
python3 - <<'PY'
from pathlib import Path
for p in sorted(Path(".github/workflows").glob("*.yml")) + sorted(Path(".github/workflows").glob("*.yaml")):
txt = p.read_text(errors="replace")
lines = txt.splitlines()
hits = [i for i,l in enumerate(lines,1) if any(s in l for s in [
"PERRY_GC_SCHEDULE_SEED", "PERRY_GC_SCHEDULE_RATE", "PERRY_GC_SCHEDULE",
"gc_schedule_fuzz", "gc_instrument_smoke", "cargo test --test gc_schedule"
])]
if hits:
print(f"\n{p}: {len(hits)} matches")
for i in hits[:25]:
print(f" {i}: {lines[i-1]}")
if len(hits) > 25:
print(f" ... plus {len(hits)-25}")
PYRepository: PerryTS/perry
Length of output: 143
Add required CI for the seeded GC schedule OFF state.
scripts/gc_schedule_fuzz.sh has a local baseline arm, and crates/perry-runtime/src/gc/tests/schedule.rs tests ScheduleGuard::off(), but no CI workflow runs a required gate for PERRY_GC_SCHEDULE_SEED unset or PERRY_GC_SCHEDULE_RATE alone. Add a required CI arm that covers both OFF-state conditions in a compiled program and checks pressure-only behavior.
🧰 Tools
🪛 LanguageTool
[style] ~145-~145: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ..._GC_MOVING_LOOP_POLLS=1`, as for zeal). Nor suppress pressure-driven collections ...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
🤖 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 `@CLAUDE.md` around lines 145 - 146, Add a required CI workflow arm for the
seeded GC schedule OFF state, using a compiled program to test both an unset
PERRY_GC_SCHEDULE_SEED and PERRY_GC_SCHEDULE_RATE set without a seed. Verify
both remain schedule-inert while pressure-driven collections still occur,
reusing the existing scripts/gc_schedule_fuzz.sh or schedule test infrastructure
where appropriate.
Source: Coding guidelines
| *CACHED.get_or_init(|| { | ||
| let seed = parse_seed(std::env::var("PERRY_GC_SCHEDULE_SEED").ok().as_deref())?; | ||
| let rate = parse_rate(std::env::var("PERRY_GC_SCHEDULE_RATE").ok().as_deref()); | ||
| let resolved = (seed, rate_threshold(rate)); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Look for a required CI arm that runs with the new GC knobs unset or OFF.
set -euo pipefail
fd -t f -e yml -e yaml . .github | xargs rg -n -C 5 'PERRY_GC_|gc_schedule|GC_ZEAL' || echo "no GC-knob references in workflows"Repository: PerryTS/perry
Length of output: 13727
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== workflow files =="
fd -t f -e yml -e yaml . .github
echo
echo "== schedule env var references in tracked files (excluding .git) =="
git ls-files | rg '(^crates/|^\.github/workflows/|Cargo\.lock$)' | xargs rg -n 'PERRY_GC_SCHEDULE_(SEED|RATE)|schedules_in_process|PERRY_RUNTIME_DIR' || true
echo
echo "== schedule.rs around module docs and resolved =="
sed -n '70,90p;220,250p' crates/perry-runtime/src/g c(schedule.rs) 2>/dev/null || sed -n '70,90p;220,250p' crates/perry-runtime/src/gc/schedule.rsRepository: PerryTS/perry
Length of output: 7957
Add a required CI arm that exercises the GC schedule knobs in their default/OFF state.
This change adds PERRY_GC_SCHEDULE_SEED and PERRY_GC_SCHEDULE_RATE, but the workflows do not show a required CI arm exercising both knobs with defaults/unset state. Add such an arm or remove the knobs after the required soak period.
🤖 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/schedule.rs` around lines 240 - 243, Add a
required CI workflow arm that runs the GC schedule tests or relevant test suite
with PERRY_GC_SCHEDULE_SEED and PERRY_GC_SCHEDULE_RATE unset, verifying their
default/OFF behavior alongside existing CI coverage. Anchor the change to the
workflow job invoking the tests and preserve the current configured-knob
coverage.
Source: Coding guidelines
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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 `@CLAUDE.md`:
- Line 145: Update the PERRY_GC_SCHEDULE_SEED documentation to name Perry’s
process-exit teardown funnel as the source of seed reporting for _exit-based
exits, while describing atexit only as an additional reporting path. Preserve
the existing panic and signal-reporting paths and all other seed behavior.
- Around line 145-148: Condense the PERRY_GC_SCHEDULE_SEED and
PERRY_GC_SCHEDULE_RATE entries in CLAUDE.md to their concise runtime contract,
removing implementation rationale, reproduction guidance, and historical
context. Move that detailed narrative, including measurement guidance, to
changelog.d/7317-seeded-gc-schedule-fuzzing.md while preserving the documented
behavior and configuration semantics.
- Line 148: Update the documented invocation of scripts/gc_schedule_fuzz.sh to
use the optional argument name [seed-count] instead of [seeds], while preserving
the existing binary argument and surrounding guidance.
🪄 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: 91c7b636-1ffb-4ec0-ba19-591b2f2b1559
📒 Files selected for processing (12)
CLAUDE.mdchangelog.d/7317-seeded-gc-schedule-fuzzing.mdcrates/perry-runtime/src/arena/quarantine.rscrates/perry-runtime/src/gc/mod.rscrates/perry-runtime/src/gc/policy.rscrates/perry-runtime/src/gc/schedule.rscrates/perry-runtime/src/gc/tests/mod.rscrates/perry-runtime/src/gc/tests/schedule.rsdocs/src/internals/gc-rooting-invariant.mddocs/src/internals/memory-model.mdscripts/gc_instrument_smoke.shscripts/gc_schedule_fuzz.sh
🚧 Files skipped from review as they are similar to previous changes (10)
- docs/src/internals/gc-rooting-invariant.md
- crates/perry-runtime/src/arena/quarantine.rs
- crates/perry-runtime/src/gc/tests/mod.rs
- scripts/gc_instrument_smoke.sh
- crates/perry-runtime/src/gc/mod.rs
- docs/src/internals/memory-model.md
- crates/perry-runtime/src/gc/policy.rs
- scripts/gc_schedule_fuzz.sh
- crates/perry-runtime/src/gc/tests/schedule.rs
- crates/perry-runtime/src/gc/schedule.rs
| | `PERRY_GC_SCHEDULE_SEED=<u64>` | seeded GC-schedule fuzzing — the middle setting between normal pacing and zeal. Three things, exactly: (1) `js_gc_loop_safepoint` stops requiring `GC_SAFEPOINT_PENDING` before descending into `gc_safepoint_moving_minor`, the same bypass zeal performs; (2) inside `gc_safepoint_moving_minor`, **past the entry guards**, a per-thread safepoint counter advances once per handled safepoint and, when `gc_budgeted_due_trigger()` reports nothing due, a minor runs anyway iff `splitmix64(splitmix64(seed) ^ counter) < threshold`; (3) `gc_force_evacuate_enabled()` becomes true, so survivors MOVE. **A value that does not parse as `u64` reads as OFF, not as seed 0.** The seed is printed at startup, at `atexit`, and on panic/SIGSEGV/SIGBUS/SIGABRT/SIGILL/SIGTRAP — the signal reporter chains to (and is re-layered on top of) the from-space quarantine's, so the two compose. Live-subject counters: `gc::gc_schedule_safepoints()` / `gc::gc_schedule_forced_collections()`. | bypass `gc_safepoint_moving_minor`'s entry guards — and a blocked safepoint deliberately does **not** tick the counter, so the ordinal sequence tracks the program's safepoints rather than its allocation state. Nor override `PERRY_GEN_GC_EVACUATE=0`. Nor emit loop polls (compile-time `PERRY_GC_MOVING_LOOP_POLLS=1`, as for zeal). Nor *suppress* pressure-driven collections — the rate is additional density, never less. Determinism is **per-thread**: the counter is thread-local, so a single-threaded program replays exactly, while a `perry/thread` program is only as reproducible as its OS scheduling. Say which you measured. | | ||
| | `PERRY_GC_SCHEDULE_RATE=<0..1>` (default `0.05`) | **only** the threshold `PERRY_GC_SCHEDULE_SEED`'s hash is compared against — the expected fraction of handled safepoints that collect. Out-of-range values clamp (a `2` reads as 1.0); unparseable and NaN fall back to the default. | do anything at all without a seed. It is inert alone. `=0` is an on-but-selects-nothing control (banner and reporters still install), `=1` is zeal's density. | | ||
|
|
||
| `PERRY_GC_ZEAL=1 PERRY_GC_PROTECT_FROMSPACE=1` together is the pairing that turns a #7154 bug into an immediate precise fault. Compile *and* run with `PERRY_GC_MOVING_LOOP_POLLS=1` for in-loop coverage. | ||
| `PERRY_GC_ZEAL=1 PERRY_GC_PROTECT_FROMSPACE=1` together is the pairing that turns a #7154 bug into an immediate precise fault. Compile *and* run with `PERRY_GC_MOVING_LOOP_POLLS=1` for in-loop coverage. Where zeal is too blunt — it distorts timing enough that some workloads die somewhere uninteresting first — `PERRY_GC_SCHEDULE_SEED` is the same pairing at a tunable density, and it hands back a reproducer. `scripts/gc_schedule_fuzz.sh <binary> [seeds]` sweeps it and prints a reproduce command per failing seed. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Keep the detailed narrative out of CLAUDE.md.
This addition contains detailed implementation rationale and reproduction guidance that the changelog fragment already records. Keep CLAUDE.md to the concise runtime contract for these knobs. Move detailed rationale and measurements to changelog.d/7317-seeded-gc-schedule-fuzzing.md.
As per coding guidelines, CLAUDE.md must remain concise and detailed change history belongs in changelog.d/ fragments.
🧰 Tools
🪛 LanguageTool
[style] ~145-~145: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ..._GC_MOVING_LOOP_POLLS=1`, as for zeal). Nor suppress pressure-driven collections ...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
🤖 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 `@CLAUDE.md` around lines 145 - 148, Condense the PERRY_GC_SCHEDULE_SEED and
PERRY_GC_SCHEDULE_RATE entries in CLAUDE.md to their concise runtime contract,
removing implementation rationale, reproduction guidance, and historical
context. Move that detailed narrative, including measurement guidance, to
changelog.d/7317-seeded-gc-schedule-fuzzing.md while preserving the documented
behavior and configuration semantics.
Source: Coding guidelines
|
The premise is right and it is the most useful framing anyone has put on this class:
That explains something we have been misreading. #7280's acceptance arms read 6, 8, 9 out of 30 across three runs of the same parent — we have been treating that as noise to work around, when it is really one schedule being sampled repeatedly. A seeded sweep is the right instrument, and it arrives at exactly the moment it is most needed: the owner has chosen to make statepoints the default and delete the shadow stack, and the soak deciding that is running now. Not merging yet, for two reasons:
What would make this land fast: the CI arm, and the two I have pointed the soak agent at this branch so it can use the sweep locally for schedule exploration without waiting on the merge — if it finds a failing seed on the statepoint arm, that is exactly the evidence the flip decision needs, and it would be a strong argument for landing this. |
5d8ce73 to
9c43d77
Compare
|
Thanks for the review. Pushed Fixed in
Already covered / won't-change, with reasons
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@changelog.d/7317-seeded-gc-schedule-fuzzing.md`:
- Around line 45-48: Update the 0/16 statistical statement in the changelog to
identify the confidence level and interval method used for the ~19% upper bound,
specifically describing it as a 95% Wilson upper bound.
In `@CLAUDE.md`:
- Around line 145-146: Update the CI workflow coverage for the GC scheduling
configuration to add required arms for an unset PERRY_GC_SCHEDULE_SEED and for
PERRY_GC_SCHEDULE_RATE configured without a seed. In each arm, verify
pressure-driven collections remain active while schedule-triggered collections
stay disabled, matching the documented OFF-state behavior.
🪄 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: 3b65e0cf-c3cf-401f-8906-3df203dedc2b
📒 Files selected for processing (12)
CLAUDE.mdchangelog.d/7317-seeded-gc-schedule-fuzzing.mdcrates/perry-runtime/src/arena/quarantine.rscrates/perry-runtime/src/gc/mod.rscrates/perry-runtime/src/gc/policy.rscrates/perry-runtime/src/gc/schedule.rscrates/perry-runtime/src/gc/tests/mod.rscrates/perry-runtime/src/gc/tests/schedule.rsdocs/src/internals/gc-rooting-invariant.mddocs/src/internals/memory-model.mdscripts/gc_instrument_smoke.shscripts/gc_schedule_fuzz.sh
🚧 Files skipped from review as they are similar to previous changes (10)
- crates/perry-runtime/src/arena/quarantine.rs
- crates/perry-runtime/src/gc/policy.rs
- docs/src/internals/gc-rooting-invariant.md
- scripts/gc_schedule_fuzz.sh
- docs/src/internals/memory-model.md
- crates/perry-runtime/src/gc/mod.rs
- crates/perry-runtime/src/gc/tests/schedule.rs
- crates/perry-runtime/src/gc/schedule.rs
- crates/perry-runtime/src/gc/tests/mod.rs
- scripts/gc_instrument_smoke.sh
| Seed 1 was re-run five times and failed **5/5** at the identical site in ≤ 1 s. | ||
| The control's 0/16 is consistent with the known ~1.7% rate (zero failures in 16 | ||
| runs bounds it at ~19%, which is why re-running was never going to settle | ||
| anything); the point is the contrast with 6/12 in two seconds. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
State the statistical method for the 0/16 bound.
The text reports a “~19%” bound but does not state the confidence level or interval method. Add that context, for example by identifying it as a 95% Wilson upper bound. Otherwise, readers cannot reproduce or interpret the claim.
Suggested wording
-The control's 0/16 is consistent with the known ~1.7% rate (zero failures in 16 runs bounds it at ~19%, which is why re-running was never going to settle anything); the point is the contrast with 6/12 in two seconds.
+The control's 0/16 is consistent with the known ~1.7% rate. Using a 95% Wilson upper bound, zero failures in 16 runs gives an upper bound of ~19%; the point is the contrast with 6/12 in two seconds.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Seed 1 was re-run five times and failed **5/5** at the identical site in ≤ 1 s. | |
| The control's 0/16 is consistent with the known ~1.7% rate (zero failures in 16 | |
| runs bounds it at ~19%, which is why re-running was never going to settle | |
| anything); the point is the contrast with 6/12 in two seconds. | |
| Seed 1 was re-run five times and failed **5/5** at the identical site in ≤ 1 s. | |
| The control's 0/16 is consistent with the known ~1.7% rate. Using a 95% Wilson upper bound, zero failures in 16 runs gives an upper bound of ~19%; the point is the contrast with 6/12 in two seconds. |
🤖 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 `@changelog.d/7317-seeded-gc-schedule-fuzzing.md` around lines 45 - 48, Update
the 0/16 statistical statement in the changelog to identify the confidence level
and interval method used for the ~19% upper bound, specifically describing it as
a 95% Wilson upper bound.
| | `PERRY_GC_SCHEDULE_SEED=<u64>` | seeded GC-schedule fuzzing — the middle setting between normal pacing and zeal. Three things, exactly: (1) `js_gc_loop_safepoint` stops requiring `GC_SAFEPOINT_PENDING` before descending into `gc_safepoint_moving_minor`, the same bypass zeal performs; (2) inside `gc_safepoint_moving_minor`, **past the entry guards**, a per-thread safepoint counter advances once per handled safepoint and, when `gc_budgeted_due_trigger()` reports nothing due, a minor runs anyway iff `splitmix64(splitmix64(seed) ^ counter) < threshold`; (3) `gc_force_evacuate_enabled()` becomes true, so survivors MOVE. **A value that does not parse as `u64` reads as OFF, not as seed 0.** The seed is printed at startup, from the process-exit teardown funnel every exit path routes through (`report_exit_summary`, on the collection-side-allocation release — perry's `_exit` paths never reach `atexit`, which is only a libc-return backstop), and on panic/SIGSEGV/SIGBUS/SIGABRT/SIGILL/SIGTRAP — the signal reporter chains to (and is re-layered on top of) the from-space quarantine's, so the two compose. Live-subject counters: `gc::gc_schedule_safepoints()` / `gc::gc_schedule_forced_collections()`. | bypass `gc_safepoint_moving_minor`'s entry guards — and a blocked safepoint deliberately does **not** tick the counter, so the ordinal sequence tracks the program's safepoints rather than its allocation state. Nor override `PERRY_GEN_GC_EVACUATE=0`. Nor emit loop polls (compile-time `PERRY_GC_MOVING_LOOP_POLLS=1`, as for zeal). Nor *suppress* pressure-driven collections — the rate is additional density, never less. Determinism is **per-thread**: the counter is thread-local, so a single-threaded program replays exactly, while a `perry/thread` program is only as reproducible as its OS scheduling. Say which you measured. | | ||
| | `PERRY_GC_SCHEDULE_RATE=<0..1>` (default `0.05`) | **only** the threshold `PERRY_GC_SCHEDULE_SEED`'s hash is compared against — the expected fraction of handled safepoints that collect. Out-of-range values clamp (a `2` reads as 1.0); unparseable and NaN fall back to the default. | do anything at all without a seed. It is inert alone. `=0` is an on-but-selects-nothing control (banner and reporters still install), `=1` is zeal's density. | |
There was a problem hiding this comment.
Add required CI coverage for both OFF states.
The documented kill-policy requires a required CI arm for every GC environment knob. The PR still has no workflow coverage for an unset PERRY_GC_SCHEDULE_SEED or for PERRY_GC_SCHEDULE_RATE without a seed. Add both cases and verify that pressure-driven collection remains active while schedule-triggered collection remains disabled.
🧰 Tools
🪛 LanguageTool
[style] ~145-~145: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ..._GC_MOVING_LOOP_POLLS=1`, as for zeal). Nor suppress pressure-driven collections ...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
🤖 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 `@CLAUDE.md` around lines 145 - 146, Update the CI workflow coverage for the GC
scheduling configuration to add required arms for an unset
PERRY_GC_SCHEDULE_SEED and for PERRY_GC_SCHEDULE_RATE configured without a seed.
In each arm, verify pressure-driven collections remain active while
schedule-triggered collections stay disabled, matching the documented OFF-state
behavior.
Source: Coding guidelines
…cible A rooting bug (PerryTS#7154 family) is a value live but not rooted across a collection point. Whether it is caught is decided by the GC schedule, not by the bug — so re-running one binary sixty times re-runs one schedule sixty times and explores almost nothing. Two settings existed: normal pacing and PERRY_GC_ZEAL=1 (every safepoint). This is the middle, and it hands back a reproducer. PERRY_GC_SCHEDULE_SEED=<u64> makes "should this safepoint collect?" a deterministic function of the seed and a per-thread safepoint ordinal, at a density set by PERRY_GC_SCHEDULE_RATE (default 0.05). scripts/gc_schedule_fuzz.sh sweeps seeds and prints a reproduce command per failure. On Socket Firewall's sfw-registry --help (PerryTS#7291's tree, iovalkey forced, loop polls compiled and run): control 0/16 failures at 55s each; seeds 1..12 at rate 0.05, 6/12 failed in under two seconds. Seed 1 reproduces 5/5 at the identical zod schemas.ts:1318 site. Three other seeds reach the node-machine-id path that makes zeal unusable on this workload. Default off and proven inert: with no seed, PERRY_GC_DIAG traces are byte-identical to the parent across five configurations on two fixtures (367 lines under plain polls, 4941 under zeal, 6151 under zeal + from-space protection). gc/tests/schedule.rs asserts both directions of both knobs; gc_instrument_smoke.sh gains three integrated arms gating that the schedule is a middle setting (strictly between pressure-only and zeal) and that the same seed retires exactly the same page-sets. The seed is printed at startup, at exit, on panic, and from a chained async-signal-safe handler that the from-space quarantine re-layers, so the seed survives the pairing an investigator actually uses.
9c43d77 to
ca397ef
Compare
|
Follow-up
On the OFF-state arm: the required |
There was a problem hiding this comment.
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/native_handle.rs`:
- Around line 62-73: Update is_main_thread_or_unrecorded and its exit-summary
callers so an unrecorded MAIN_THREAD_ID does not allow a worker thread to claim
SUMMARY_EMITTED. Prefer ensuring js_native_handle_mark_main_thread() runs during
startup before workers begin, or make the teardown diagnostic skip callers while
MAIN_THREAD_ID remains 0; preserve acceptance only for the registered main
thread.
🪄 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: 5fe7a9c1-d4d9-4981-bddc-14cde04fae59
📒 Files selected for processing (13)
CLAUDE.mdchangelog.d/7317-seeded-gc-schedule-fuzzing.mdcrates/perry-runtime/src/arena/quarantine.rscrates/perry-runtime/src/gc/mod.rscrates/perry-runtime/src/gc/policy.rscrates/perry-runtime/src/gc/schedule.rscrates/perry-runtime/src/gc/tests/mod.rscrates/perry-runtime/src/gc/tests/schedule.rscrates/perry-runtime/src/native_handle.rsdocs/src/internals/gc-rooting-invariant.mddocs/src/internals/memory-model.mdscripts/gc_instrument_smoke.shscripts/gc_schedule_fuzz.sh
🚧 Files skipped from review as they are similar to previous changes (10)
- docs/src/internals/gc-rooting-invariant.md
- crates/perry-runtime/src/gc/tests/mod.rs
- crates/perry-runtime/src/gc/policy.rs
- scripts/gc_instrument_smoke.sh
- docs/src/internals/memory-model.md
- crates/perry-runtime/src/arena/quarantine.rs
- crates/perry-runtime/src/gc/mod.rs
- crates/perry-runtime/src/gc/tests/schedule.rs
- scripts/gc_schedule_fuzz.sh
- crates/perry-runtime/src/gc/schedule.rs
| /// True on the runtime's main thread, or when the main thread has not been | ||
| /// recorded yet. The unrecorded case returns `true` on purpose: callers use | ||
| /// this to gate a once-only diagnostic, and never emitting is worse than | ||
| /// emitting from a not-yet-identified thread. Pure read — unlike | ||
| /// [`runtime_main_thread_id`] it does not capture the caller as main. | ||
| pub(crate) fn is_main_thread_or_unrecorded() -> bool { | ||
| match MAIN_THREAD_ID.load(Ordering::Acquire) { | ||
| 0 => true, | ||
| main => current_thread_id() == main, | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
# Locate main-thread registration, exit-summary gating, and worker creation.
rg -n -C 8 \
'runtime_main_thread_id|is_main_thread_or_unrecorded|MAIN_THREAD_ID|report_exit_summary|thread::spawn|thread::Builder' \
--glob '*.rs' .Repository: PerryTS/perry
Length of output: 50370
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
printf '--- is_main_thread_or_unrecorded call sites ---\n'
rg -n -C 4 'is_main_thread_or_unrecorded' crates/perry-runtime src crates --glob '*.rs' || true
printf '\n--- SUMMARY_EMITTED / report_exit_summary occurrences ---\n'
rg -n -C 6 'SUMMARY_EMITTED|report_exit_summary' crates/perry-runtime src crates --glob '*.rs' || true
printf '\n--- js_native_handle_mark_main_thread call sites ---\n'
rg -n -C 4 'js_native_handle_mark_main_thread' crates/perry-runtime src crates --glob '*.rs' || true
printf '\n--- native_handle outline ---\n'
ast-grep outline crates/perry-runtime/src/native_handle.rs --view compact || true
printf '\n--- gc/schedule outline ---\n'
ast-grep outline crates/perry-runtime/src/gc/schedule.rs --view compact || trueRepository: PerryTS/perry
Length of output: 242
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- is_main_thread_or_unrecorded call sites ---'
rg -n -C 4 'is_main_thread_or_unrecorded' crates/perry-runtime src crates --glob '*.rs' || true
printf '%s\n' ''
printf '%s\n' '--- SUMMARY_EMITTED / report_exit_summary occurrences ---'
rg -n -C 6 'SUMMARY_EMITTED|report_exit_summary' crates/perry-runtime src crates --glob '*.rs' || true
printf '%s\n' ''
printf '%s\n' '--- js_native_handle_mark_main_thread call sites ---'
rg -n -C 4 'js_native_handle_mark_main_thread' crates/perry-runtime src crates --glob '*.rs' || true
printf '%s\n' ''
printf '%s\n' '--- native_handle outline ---'
ast-grep outline crates/perry-runtime/src/native_handle.rs --view compact || true
printf '%s\n' ''
printf '%s\n' '--- gc/schedule outline ---'
ast-grep outline crates/perry-runtime/src/gc/schedule.rs --view compact || trueRepository: PerryTS/perry
Length of output: 17747
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- native_handle relevant sections ---'
sed -n '1,90p' crates/perry-runtime/src/native_handle.rs | cat -n
sed -n '208,275p' crates/perry-runtime/src/native_handle.rs | cat -n
printf '%s\n' ''
printf '%s\n' '--- gc/schedule relevant sections ---'
sed -n '360,430p' crates/perry-runtime/src/gc/schedule.rs | cat -n
sed -n '745,790p' crates/perry-runtime/src/gc/mod.rs | cat -n
printf '%s\n' ''
printf '%s\n' '--- call sites with direct file context ---'
rg -n -C 4 'js_native_handle_mark_main_thread|runtime_main_thread_id|is_main_thread_or_unrecorded|report_exit_summary|install_exit_summary|js_gc_release_current_thread_collection_side_allocations|js_process_exit' crates/perry-runtime --glob '*.rs'
printf '%s\n' ''
printf '%s\n' '--- worker creation calls in crates/perry-runtime with concise context ---'
rg -n -C 3 'std::thread::spawn|thread::spawn\(|thread::Builder::new' crates/perry-runtime src crates --glob '*.rs' | head -n 200Repository: PerryTS/perry
Length of output: 49624
Require explicit main-thread registration before accepting unrecorded callers.
is_main_thread_or_unrecorded() passes every thread while MAIN_THREAD_ID is 0, but the generated exit epilogue only calls js_native_handle_mark_main_thread() once all native work has drained. A worker thread can therefore take over SUMMARY_EMITTED before teardown from the main thread. Use explicit startup registration or make the exit summary skip unrecorded workers instead of treating them as main for this once-only diagnostic.
🤖 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/native_handle.rs` around lines 62 - 73, Update
is_main_thread_or_unrecorded and its exit-summary callers so an unrecorded
MAIN_THREAD_ID does not allow a worker thread to claim SUMMARY_EMITTED. Prefer
ensuring js_native_handle_mark_main_thread() runs during startup before workers
begin, or make the teardown diagnostic skip callers while MAIN_THREAD_ID remains
0; preserve acceptance only for the registered main thread.
What
PERRY_GC_SCHEDULE_SEED=<u64>— collect at a safepoint iff a deterministic pseudo-random function of the seed and a per-thread safepoint ordinal says so, at a density set byPERRY_GC_SCHEDULE_RATE(default0.05). Plusscripts/gc_schedule_fuzz.sh <binary> [seeds], which sweeps seeds and prints a reproduce command per failure.Why
A #7154-class bug is a value live but not rooted across a collection point. Whether it is caught is a property of the GC schedule, not of the bug — so re-running one binary sixty times re-runs one schedule sixty times and explores almost nothing. With zero failures in
Nruns the 95% upper bound on the true rate is only ~3/N: 120 clean runs bound a 1.7% bug at 2.5%, i.e. no evidence at all.Two settings existed. Normal pacing puts collections tens of megabytes apart.
PERRY_GC_ZEAL=1collects at every safepoint — maximum pressure, but one fixed schedule, slow, and timing-distorting enough that it cannot be used on the registry at all (it dies innode-machine-idbefore the interesting code runs). This is the middle, and unlike either it hands back a reproducer.The result that matters
sfw-registry --help(#7291's tree,PERRY_FORCE_WELL_KNOWN=iovalkey, compiled and run withPERRY_GC_MOVING_LOOP_POLLS=1,--debug-symbols) fails ~1 run in 60 in the plain-polls configuration. Same binary, macOS arm64, four runs in parallel:1..12,RATE=0.05Two stable signatures:
The first is the signature the registry hunt has been chasing. Seed 1 was re-run 5/5 and failed every time at the identical site in ≤ 1 s:
The second signature is the
node-machine-idpath that makes zeal unusable here — at 5% density it is reachable without also losing the rest of the program.Cost: a seeded run is ~5–10× slower on this workload, which is why half the sweep is censored rather than passed. Failing seeds cost 1–2 s, so a sweep's wall clock is dominated entirely by the seeds that find nothing.
What the knobs gate, precisely
PERRY_GC_SCHEDULE_SEEDdoes exactly three things:js_gc_loop_safepointstops requiringGC_SAFEPOINT_PENDINGbefore descending intogc_safepoint_moving_minor— the bypass zeal performs, for the same reason: a schedule cannot select a safepoint the gate already returned from.gc_safepoint_moving_minor, past the entry guards, a per-thread counter advances once per handled safepoint; with nothing due, a minor runs anyway iffsplitmix64(splitmix64(seed) ^ counter) < threshold.gc_force_evacuate_enabled()becomes true, so a scheduled minor MOVES survivors — otherwise the mode would promise relocation stress and deliver sweep pressure (gc: no reachable configuration exercises an evacuating minor with unpinned runtime locals — the #6655/#6935 bug class is untestable #6942/GC testing: PERRY_GC_FORCE_EVACUATE is inert for gc()-driven tests (full mark-sweep + forced conservative scan) — stress claims may be unsupported #6946).It does not bypass the entry guards, and a blocked safepoint deliberately does not tick the counter, so the ordinal sequence tracks the program's safepoints rather than its allocation state. It does not override
PERRY_GEN_GC_EVACUATE=0. It cannot emit loop polls codegen never produced. It never suppresses a pressure-driven collection — the rate is additional density, never less. A value that does not parse as au64reads as OFF, not as seed 0.PERRY_GC_SCHEDULE_RATEgates only the comparison threshold, and is inert without a seed.0is an on-but-selects-nothing control;1is zeal's density.Determinism, scoped honestly
The decision reads no wall clock, no address, no thread identity — so a single-threaded program replays a seed exactly. The counter is thread-local, so a
perry/threadprogram gets a deterministic schedule per thread given that thread's own safepoint sequence, but nothing makes the OS schedule that sequence identically twice. A global counter would be strictly worse: it would make even one thread's schedule depend on interleaving. Deterministic for single-threaded programs; per-thread but not run-to-run reproducible for multi-threaded ones.Default off, proven inert
With no seed set,
PERRY_GC_DIAG=1collector traces are byte-identical to the branch parent across five configurations on two fixtures — 367-line traces under plain polls, 4941 under zeal, 6151 under zeal + from-space protection, plus the no-polls and forced-evacuation arms.The seed is never lost
Printed at startup, at exit (
[gc-schedule] done: seed=… safepoints=… scheduled_collections=…, from the process-exit teardown funnel — perry's exits call_exit, soatexitalone would miss them), on panic, and from an async-signal-safe handler for SIGSEGV/SIGBUS/SIGABRT/SIGILL/SIGTRAP. That handler chains rather than clobbers, andarena/quarantine.rsre-layers it after installing its own, soPERRY_GC_SCHEDULE_SEED=… PERRY_GC_PROTECT_FROMSPACE=1reports both the seed and the precise fault site.Tests
gc/tests/schedule.rs, 11 tests, both directions of both knobs: parse (includingu64::MAX + 1,-1,0x10→ OFF), threshold endpoints, 100k-ordinal determinism across five seeds, adjacent-seed divergence, realised density vs requested at four rates, collect / decline / blocked at a real safepoint, and the evacuation implication with itsPERRY_GEN_GC_EVACUATE=0precedence arm.scripts/gc_instrument_smoke.shgains three integrated arms that gate the three claims end to end. Measured on the fixture:pressure-only=0 < seeded(0.25)=989 < zeal=1230— a middle setting, not a second name for an endpoint — and the same seed twice retires989 == 989.cargo test -p perry-runtimeon this branch: 1670 passed, 0 failed (--test-threads=1, two consecutive runs). The branch parent, same machine, same conditions: 1658 passed, 1 failed (pty::…::js_pty_spawn_shell_data_and_exit, a 15 s pty wait that times out under load). The default parallel mode is flaky on both — threeobject::failures on the branch, a different four on the parent, none overlapping — a pre-existing isolation problem, not this change.No collector policy changed. Every scheduled collection runs at a point the collector already treats as a precise-root safepoint; only how often changes.
Summary by CodeRabbit
PERRY_GC_SCHEDULE_SEEDandPERRY_GC_SCHEDULE_RATE.