Skip to content

Commit 101d022

Browse files
author
Ralph Küpper
committed
fix(gc): make old-page defrag opt-in again until a workload can exercise it
1 parent 05edeac commit 101d022

1 file changed

Lines changed: 129 additions & 8 deletions

File tree

crates/perry-runtime/src/gc/oldgen_defrag.rs

Lines changed: 129 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,32 @@ impl Drop for OldDefragTestEnable {
106106
}
107107
}
108108

109+
/// RAII *disable* for defrag on this thread, so the OFF arm is exercised
110+
/// deterministically rather than depending on the ambient environment.
111+
///
112+
/// Without this the OFF state has no behavioural coverage at all: the value
113+
/// mapping is unit-tested, but nothing asserts that a disabled collector
114+
/// actually declines to select a page. That gap is what #7917 records.
115+
#[cfg(test)]
116+
pub(crate) struct OldDefragTestDisable;
117+
118+
#[cfg(test)]
119+
impl OldDefragTestDisable {
120+
pub(crate) fn new() -> Self {
121+
OLD_DEFRAG_TEST_OVERRIDE.with(|c| c.set(Some(false)));
122+
OldDefragTestDisable
123+
}
124+
}
125+
126+
#[cfg(test)]
127+
impl Drop for OldDefragTestDisable {
128+
fn drop(&mut self) {
129+
OLD_DEFRAG_TEST_OVERRIDE.with(|c| c.set(None));
130+
}
131+
}
132+
109133
fn old_page_defrag_enabled_from_value(value: Option<&str>) -> bool {
110-
!matches!(value, Some("0") | Some("off") | Some("false"))
134+
matches!(value, Some("1") | Some("on") | Some("true"))
111135
}
112136

113137
fn old_page_defrag_enabled() -> bool {
@@ -123,9 +147,23 @@ fn old_page_defrag_enabled() -> bool {
123147
}
124148

125149
pub(super) fn select_old_page_defrag_pages(force: bool) -> OldPageDefragSelection {
126-
// #7876 restored the mutable-root contract for old movable addresses and
127-
// made defrag the production default. Keep an explicit kill switch for
128-
// field diagnosis and rollback without shipping a second binary.
150+
// #7876 restored the mutable-root contract for old movable addresses, and
151+
// #7913 shipped that restoration with defrag ON by default. The contract
152+
// work is sound and stays; the DEFAULT is what this reverts (#7917).
153+
//
154+
// #7876's own acceptance criteria said to "re-enable defrag only after the
155+
// reproducer and a dependency-scale stress corpus are clean". No such
156+
// corpus exists yet, and none of the 19 benchmark programs can produce a
157+
// candidate page: selection needs `dead_bytes >= live_bytes` on an old
158+
// page, which needs promote-then-die at scale. The retain family survives
159+
// at 999-1000 permille and the churn family promotes almost nothing, so
160+
// the suite yields neither a benefit signal nor a regression signal while
161+
// still inheriting the full old-address rewrite surface.
162+
//
163+
// So this is opt-in until a fragmentation workload exists that can
164+
// actually exercise it. When that lands, the losing arm gets DELETED
165+
// rather than left standing -- per CLAUDE.md, a mode that still exists is
166+
// a decision that has not been made.
129167
if !old_page_defrag_enabled() {
130168
return OldPageDefragSelection::default();
131169
}
@@ -135,17 +173,100 @@ pub(super) fn select_old_page_defrag_pages(force: bool) -> OldPageDefragSelectio
135173

136174
#[cfg(test)]
137175
mod tests {
138-
use super::old_page_defrag_enabled_from_value;
176+
use super::{
177+
old_page_defrag_enabled_from_value, select_old_page_defrag_pages, OldDefragTestDisable,
178+
OldDefragTestEnable,
179+
};
139180

140181
#[test]
141-
fn old_page_defrag_defaults_on_with_an_explicit_kill_switch() {
142-
assert!(old_page_defrag_enabled_from_value(None));
182+
fn old_page_defrag_is_opt_in_via_perry_gc_old_defrag() {
183+
// Unset means OFF: defrag is opt-in until a fragmentation workload
184+
// exists that can demonstrate it (#7917).
185+
assert!(!old_page_defrag_enabled_from_value(None));
143186
assert!(old_page_defrag_enabled_from_value(Some("1")));
144187
assert!(old_page_defrag_enabled_from_value(Some("on")));
145188
assert!(old_page_defrag_enabled_from_value(Some("true")));
146189
assert!(!old_page_defrag_enabled_from_value(Some("0")));
147190
assert!(!old_page_defrag_enabled_from_value(Some("off")));
148191
assert!(!old_page_defrag_enabled_from_value(Some("false")));
149-
assert!(old_page_defrag_enabled_from_value(Some("unexpected")));
192+
// Anything unrecognised is OFF, so a typo cannot silently enable
193+
// old-generation relocation.
194+
assert!(!old_page_defrag_enabled_from_value(Some("unexpected")));
195+
}
196+
197+
/// The OFF arm, asserted through the gated entry point rather than through
198+
/// the value mapping.
199+
///
200+
/// This matters because `select_old_page_defrag_pages_from_snapshot` does
201+
/// NOT consult the knob — the gate lives only in
202+
/// `select_old_page_defrag_pages` — so every pre-existing selection test
203+
/// bypasses the switch entirely. Before this test the OFF state had no
204+
/// behavioural coverage at all (#7917).
205+
///
206+
/// The observable is the page-meta snapshot counter rather than the
207+
/// returned selection, for two reasons. It needs no old-arena fixture, and
208+
/// more importantly an empty selection is **not** evidence on its own: a
209+
/// bare test process has no eligible old pages, so asserting only
210+
/// "disabled returns nothing" passes just as happily against a kill switch
211+
/// that does nothing at all. That is the gate-that-cannot-fail shape this
212+
/// codebase keeps re-learning, and the first version of this test walked
213+
/// straight into it.
214+
///
215+
/// So the positive control is load-bearing: it proves the enabled path
216+
/// really does reach the snapshot, which is the thing the disabled path
217+
/// must then be shown to skip.
218+
///
219+
/// It also pins the *placement* of the gate, not merely its effect: the
220+
/// short-circuit must happen before the O(old pages) snapshot, so a
221+
/// disabled collector pays nothing on every ordinary minor.
222+
#[test]
223+
fn disabled_defrag_short_circuits_before_taking_a_page_snapshot() {
224+
use crate::arena::old_page_meta_snapshot_calls_for_tests as snapshot_calls;
225+
226+
let before_enabled = snapshot_calls();
227+
let enabled = {
228+
let _enable = OldDefragTestEnable::new();
229+
select_old_page_defrag_pages(true)
230+
};
231+
let enabled_calls = snapshot_calls() - before_enabled;
232+
233+
let before_disabled = snapshot_calls();
234+
let disabled_forced = {
235+
let _disable = OldDefragTestDisable::new();
236+
select_old_page_defrag_pages(true)
237+
};
238+
let disabled_unforced = {
239+
let _disable = OldDefragTestDisable::new();
240+
select_old_page_defrag_pages(false)
241+
};
242+
let disabled_calls = snapshot_calls() - before_disabled;
243+
244+
assert_eq!(
245+
enabled_calls, 1,
246+
"positive control: enabled defrag must reach the page snapshot. If \
247+
this is 0 the assertions below prove nothing, because a switch \
248+
that never runs looks identical to one that correctly declines"
249+
);
250+
251+
assert_eq!(
252+
disabled_calls, 0,
253+
"the kill switch must short-circuit BEFORE the O(old pages) \
254+
snapshot, so a disabled collector pays nothing per minor"
255+
);
256+
257+
// `force` bypasses the dead>=live ratio, so this also proves the gate
258+
// beats a forced selection rather than merely losing the ratio test.
259+
assert_eq!(disabled_forced.selected_pages, 0);
260+
assert_eq!(disabled_forced.candidate_pages, 0);
261+
assert!(disabled_forced.pages.is_empty());
262+
assert!(disabled_forced.page_order.is_empty());
263+
assert_eq!(disabled_forced.selected_live_bytes, 0);
264+
assert_eq!(disabled_forced.selected_reclaimable_bytes, 0);
265+
assert_eq!(disabled_unforced.selected_pages, 0);
266+
assert!(disabled_unforced.pages.is_empty());
267+
268+
// Sanity: the enabled arm returned a real (possibly empty) selection
269+
// rather than the disabled default, i.e. the two paths are distinct.
270+
let _ = enabled;
150271
}
151272
}

0 commit comments

Comments
 (0)