From 33d276835e48010b87721cf54534c526f8a98d4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 10 Aug 2026 16:52:32 +0200 Subject: [PATCH 1/5] fix(gc): the seeded schedule arms the poll word, like zeal (#7778) PERRY_GC_SCHEDULE_RATE=1 saw 6 safepoints against zeal's 9,648 loop polls on the same reproduction: nothing kept the poll word armed for the schedule mode, so its loop-safepoint bypass sat behind a gate that never opened. resolve_poll_seed keeps the seed when the schedule is enabled, and ScheduleGuard mirrors ZealGuard's arm/release pair. Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix --- crates/perry-runtime/src/gc/poll_arm.rs | 10 ++++++- crates/perry-runtime/src/gc/schedule.rs | 24 ++++++++++++++--- crates/perry-runtime/src/gc/tests/triggers.rs | 26 +++++++++++++++++++ 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/crates/perry-runtime/src/gc/poll_arm.rs b/crates/perry-runtime/src/gc/poll_arm.rs index f6e331c108..a05584ac40 100644 --- a/crates/perry-runtime/src/gc/poll_arm.rs +++ b/crates/perry-runtime/src/gc/poll_arm.rs @@ -117,7 +117,15 @@ pub(crate) fn disarm_poll() { pub(crate) fn resolve_poll_seed() { static SEED: std::sync::Once = std::sync::Once::new(); SEED.call_once(|| { - if !super::gc_zeal_enabled() { + // #7778: the seeded schedule keeps the word armed for exactly zeal's + // reason — its collection decision lives inside the safepoint, so a + // disarmed poll never presents the safepoint to decide at. Measured + // before this line existed: `PERRY_GC_SCHEDULE_RATE=1` on #7606's + // reproduction saw SIX safepoints against zeal's 9,648 loop polls — + // the "collect at every opportunity" end of the dial was an + // event-loop-boundary instrument only, and the loop-safepoint bypass + // #7317 added was downstream of a gate that never opened. + if !super::gc_zeal_enabled() && !super::schedule::gc_schedule_enabled() { disarm_poll(); } }); diff --git a/crates/perry-runtime/src/gc/schedule.rs b/crates/perry-runtime/src/gc/schedule.rs index a88bf1b0e0..622a582c24 100644 --- a/crates/perry-runtime/src/gc/schedule.rs +++ b/crates/perry-runtime/src/gc/schedule.rs @@ -305,17 +305,35 @@ pub(crate) struct ScheduleGuard(Option<(u64, u64)>); #[cfg(test)] impl ScheduleGuard { pub(crate) fn set(seed: u64, threshold: u64) -> Self { - Self(SCHEDULE_OVERRIDE.with(|cell| cell.replace(Some((seed, threshold))))) + // #7778: mirror `ZealGuard` — a schedule that cannot reach the poll + // decides at six event-loop boundaries instead of thousands of + // back-edges. Arm on set, release on drop, exactly the zeal pair. + let prev = SCHEDULE_OVERRIDE.with(|cell| cell.replace(Some((seed, threshold)))); + if prev.is_none() { + super::arm_poll(); + } + Self(prev) } pub(crate) fn off() -> Self { - Self(SCHEDULE_OVERRIDE.with(|cell| cell.replace(None))) + let prev = SCHEDULE_OVERRIDE.with(|cell| cell.replace(None)); + if prev.is_some() { + super::disarm_poll(); + } + Self(prev) } } #[cfg(test)] impl Drop for ScheduleGuard { fn drop(&mut self) { - SCHEDULE_OVERRIDE.with(|cell| cell.set(self.0)); + let restored = self.0; + let current = SCHEDULE_OVERRIDE.with(|cell| cell.replace(restored)); + // Re-balance the arm to match the transition this drop performs. + match (current.is_some(), restored.is_some()) { + (true, false) => super::disarm_poll(), + (false, true) => super::arm_poll(), + _ => {} + } } } diff --git a/crates/perry-runtime/src/gc/tests/triggers.rs b/crates/perry-runtime/src/gc/tests/triggers.rs index 75067f2d85..61e97d0cae 100644 --- a/crates/perry-runtime/src/gc/tests/triggers.rs +++ b/crates/perry-runtime/src/gc/tests/triggers.rs @@ -1197,3 +1197,29 @@ fn zeal_holds_the_poll_word_armed_with_nothing_pending() { ); crate::gc::set_safepoint_pending(false); } + +/// #7778: the schedule's mirror of the zeal test above, and the regression +/// test for the gap that made `PERRY_GC_SCHEDULE_RATE=1` an event-loop-only +/// instrument: on #7606's reproduction it saw SIX safepoints against zeal's +/// 9,648 loop polls, because nothing armed the poll word for the mode whose +/// decision lives inside the safepoint the word gates. +#[test] +fn the_schedule_holds_the_poll_word_armed_like_zeal() { + let _isolation = GcTestIsolationGuard::new(); + crate::gc::set_safepoint_pending(false); + let base = crate::gc::PERRY_GC_POLL_ARMED.load(std::sync::atomic::Ordering::Relaxed); + { + let _sched = super::super::schedule::ScheduleGuard::set(7, u64::MAX); + assert_eq!( + crate::gc::PERRY_GC_POLL_ARMED.load(std::sync::atomic::Ordering::Relaxed), + base + 1, + "a live schedule must keep the poll reachable — its collection \ + decision happens inside the safepoint the word gates" + ); + } + assert_eq!( + crate::gc::PERRY_GC_POLL_ARMED.load(std::sync::atomic::Ordering::Relaxed), + base, + "dropping the ScheduleGuard must release the arm it took" + ); +} From 63fb54e94a8b6e99e564b036b2b133b8c77b99c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 10 Aug 2026 16:53:14 +0200 Subject: [PATCH 2/5] docs: key the poll-word fix's comments to the filed issue #7781 Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix --- crates/perry-runtime/src/gc/poll_arm.rs | 2 +- crates/perry-runtime/src/gc/schedule.rs | 2 +- crates/perry-runtime/src/gc/tests/triggers.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/perry-runtime/src/gc/poll_arm.rs b/crates/perry-runtime/src/gc/poll_arm.rs index a05584ac40..a70d170731 100644 --- a/crates/perry-runtime/src/gc/poll_arm.rs +++ b/crates/perry-runtime/src/gc/poll_arm.rs @@ -117,7 +117,7 @@ pub(crate) fn disarm_poll() { pub(crate) fn resolve_poll_seed() { static SEED: std::sync::Once = std::sync::Once::new(); SEED.call_once(|| { - // #7778: the seeded schedule keeps the word armed for exactly zeal's + // #7781: the seeded schedule keeps the word armed for exactly zeal's // reason — its collection decision lives inside the safepoint, so a // disarmed poll never presents the safepoint to decide at. Measured // before this line existed: `PERRY_GC_SCHEDULE_RATE=1` on #7606's diff --git a/crates/perry-runtime/src/gc/schedule.rs b/crates/perry-runtime/src/gc/schedule.rs index 622a582c24..8297376f0b 100644 --- a/crates/perry-runtime/src/gc/schedule.rs +++ b/crates/perry-runtime/src/gc/schedule.rs @@ -305,7 +305,7 @@ pub(crate) struct ScheduleGuard(Option<(u64, u64)>); #[cfg(test)] impl ScheduleGuard { pub(crate) fn set(seed: u64, threshold: u64) -> Self { - // #7778: mirror `ZealGuard` — a schedule that cannot reach the poll + // #7781: mirror `ZealGuard` — a schedule that cannot reach the poll // decides at six event-loop boundaries instead of thousands of // back-edges. Arm on set, release on drop, exactly the zeal pair. let prev = SCHEDULE_OVERRIDE.with(|cell| cell.replace(Some((seed, threshold)))); diff --git a/crates/perry-runtime/src/gc/tests/triggers.rs b/crates/perry-runtime/src/gc/tests/triggers.rs index 61e97d0cae..86615734d5 100644 --- a/crates/perry-runtime/src/gc/tests/triggers.rs +++ b/crates/perry-runtime/src/gc/tests/triggers.rs @@ -1198,7 +1198,7 @@ fn zeal_holds_the_poll_word_armed_with_nothing_pending() { crate::gc::set_safepoint_pending(false); } -/// #7778: the schedule's mirror of the zeal test above, and the regression +/// #7781: the schedule's mirror of the zeal test above, and the regression /// test for the gap that made `PERRY_GC_SCHEDULE_RATE=1` an event-loop-only /// instrument: on #7606's reproduction it saw SIX safepoints against zeal's /// 9,648 loop polls, because nothing armed the poll word for the mode whose From 4b2963bfbc392e6d8f2ec0771e2c2a30ff2a93e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 10 Aug 2026 17:09:28 +0200 Subject: [PATCH 3/5] docs(changelog): fragment for the poll-word fix Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix --- changelog.d/7782-schedule-arms-poll-word.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 changelog.d/7782-schedule-arms-poll-word.md diff --git a/changelog.d/7782-schedule-arms-poll-word.md b/changelog.d/7782-schedule-arms-poll-word.md new file mode 100644 index 0000000000..00c3d977af --- /dev/null +++ b/changelog.d/7782-schedule-arms-poll-word.md @@ -0,0 +1,14 @@ +### Fixed + +- **The seeded GC schedule never armed the poll word, so `PERRY_GC_SCHEDULE_RATE=1` was an event-loop-boundary instrument only (#7781).** On #7606's reproduction it saw **6 safepoints** where zeal's runs cross **9,648 / 19,248 loop polls** — the "collect at every opportunity" end of the dial saw six opportunities. #7735 collapsed the back-edge poll's no-work path to one load of `PERRY_GC_POLL_ARMED`; `resolve_poll_seed` kept the word armed for zeal, and nothing armed it for the schedule mode, so the loop-safepoint bypass #7317 added sat behind a gate that never opened. + + `resolve_poll_seed` now keeps the startup seed when `schedule::gc_schedule_enabled()`, exactly as for zeal, and `ScheduleGuard` mirrors `ZealGuard`'s arm/release pair so test-time schedules reach polls too. Regression test `the_schedule_holds_the_poll_word_armed_like_zeal` mirrors the zeal test and is sabotage-verified (removing the arm fails it). + + Re-run with the fix, same binary, same quarantine (`PERRY_GC_PROTECT_FROMSPACE=1`, depth 800): + + | | before | after | + |---|--:|--:| + | `rest_argument_rooting` | safepoints=6 | **safepoints=9,653**, 9,653 collections, exit 0 | + | `same_module_call_rooting` | safepoints=6 | **safepoints=19,253**, 19,253 collections, exit 0 | + + This was found testing #7741's precondition — that `SCHEDULE_RATE=1` be demonstrated equivalent to zeal on a real reproduction before the older instrument is deleted. The precondition now holds; without this fix it measurably did not. From 8d098faf45281054efb011b9bf774d697a679d68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 10 Aug 2026 17:09:32 +0200 Subject: [PATCH 4/5] chore: bump version to 0.5.1451 Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix --- CLAUDE.md | 2 +- Cargo.lock | 152 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 3 files changed, 78 insertions(+), 78 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6c7f244f10..0ea1f44c41 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1450 +**Current Version:** 0.5.1451 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 08f8b760f8..29a1920a1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1450" +version = "0.5.1451" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1450" +version = "0.5.1451" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1450" +version = "0.5.1451" [[package]] name = "perry-ui-tvos" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1450" +version = "0.5.1451" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 72e90511a0..e6209cec78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1450" +version = "0.5.1451" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" From 9adcf511868548668dccc36faba3319c479247e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 10 Aug 2026 17:25:06 +0200 Subject: [PATCH 5/5] =?UTF-8?q?fix(gc):=20ScheduleGuard's=20arm=20bookkeep?= =?UTF-8?q?ing=20must=20be=20asymmetric=20=E2=80=94=20disarm=5Fpoll=20satu?= =?UTF-8?q?rates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit off()-disarm-then-Drop-rearm leaks +1 permanently when the disarm lands on a zero word (saturation loses the decrement, the paired arm does not). The leak pinned the poll armed for the rest of the test binary: the timing test slowed and the generation-gate contract took a safepoint drain mid-stage, 2/2 consistently. Only set() arms; only its own Drop releases. 3/3 full-suite runs clean after. Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix --- crates/perry-runtime/src/gc/schedule.rs | 34 ++++++++++++++----------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/crates/perry-runtime/src/gc/schedule.rs b/crates/perry-runtime/src/gc/schedule.rs index 8297376f0b..f7fe3e7b0b 100644 --- a/crates/perry-runtime/src/gc/schedule.rs +++ b/crates/perry-runtime/src/gc/schedule.rs @@ -300,39 +300,43 @@ pub fn gc_schedule_safepoints() -> u64 { /// RAII test override. `threshold` is taken directly so tests can pin the /// always/never arms without going through float parsing. #[cfg(test)] -pub(crate) struct ScheduleGuard(Option<(u64, u64)>); +pub(crate) struct ScheduleGuard { + prev: Option<(u64, u64)>, + armed: bool, +} #[cfg(test)] impl ScheduleGuard { pub(crate) fn set(seed: u64, threshold: u64) -> Self { // #7781: mirror `ZealGuard` — a schedule that cannot reach the poll // decides at six event-loop boundaries instead of thousands of - // back-edges. Arm on set, release on drop, exactly the zeal pair. + // back-edges. Arm on set, release on drop. + // + // The bookkeeping is deliberately ASYMMETRIC: only `set` arms, and only + // its own `Drop` releases. `off()` must NOT disarm-then-let-Drop-rearm: + // `disarm_poll` saturates at zero, so a disarm that lands on 0 is lost + // while the paired re-arm is not — a permanent +1 leak that pins the + // poll armed for the rest of the process. Over-arming for a guard's + // lifetime costs a wasted call; a leaked arm is forever. let prev = SCHEDULE_OVERRIDE.with(|cell| cell.replace(Some((seed, threshold)))); - if prev.is_none() { + let armed = prev.is_none(); + if armed { super::arm_poll(); } - Self(prev) + Self { prev, armed } } pub(crate) fn off() -> Self { let prev = SCHEDULE_OVERRIDE.with(|cell| cell.replace(None)); - if prev.is_some() { - super::disarm_poll(); - } - Self(prev) + Self { prev, armed: false } } } #[cfg(test)] impl Drop for ScheduleGuard { fn drop(&mut self) { - let restored = self.0; - let current = SCHEDULE_OVERRIDE.with(|cell| cell.replace(restored)); - // Re-balance the arm to match the transition this drop performs. - match (current.is_some(), restored.is_some()) { - (true, false) => super::disarm_poll(), - (false, true) => super::arm_poll(), - _ => {} + SCHEDULE_OVERRIDE.with(|cell| cell.set(self.prev)); + if self.armed { + super::disarm_poll(); } } }