diff --git a/changelog.d/8539-interp-small-layout-mask-cutoff.md b/changelog.d/8539-interp-small-layout-mask-cutoff.md new file mode 100644 index 0000000000..7a261a1f40 --- /dev/null +++ b/changelog.d/8539-interp-small-layout-mask-cutoff.md @@ -0,0 +1,17 @@ +### Performance + +- **Cut `interp` retired instructions by 4.61% by keeping tiny mixed GC + payloads out of the per-object layout tables.** A 30-run sample profile put + 61.1% of self samples in `evalNode`; the result did not support the suspected + dynamic-numeric-envelope explanation. Instead, its pointer-bearing + environment arrays were paying to create and later clean up per-object + layout masks even though those masks could skip only a few exact tag checks. + + Raise the minimum mask-bearing payload from two slots to four, so one-, two-, + and three-slot payloads use the collector's exact tag scan. A cutoff sweep + found that four captured the full `interp` instruction win; moving the cutoff + to eight retired no fewer instructions, while it would make larger objects + perform extra tag checks and broaden the GC policy change. Across the + 19-benchmark corpus, `interp` moved 9,013,545,066 → 8,598,251,738 retired + instructions (−4.61%), no other row moved by more than 1%, and peak RSS did + not regress. diff --git a/crates/perry-runtime/src/array/tests.rs b/crates/perry-runtime/src/array/tests.rs index 6248c7a8eb..3553a411dd 100644 --- a/crates/perry-runtime/src/array/tests.rs +++ b/crates/perry-runtime/src/array/tests.rs @@ -1268,22 +1268,22 @@ fn test_numeric_array_layout_length_and_delete_transitions() { #[test] fn test_numeric_array_layout_immutable_helpers_preserve_or_downgrade() { - let values = [10.0, 2.0, 30.0]; + let values = [10.0, 2.0, 30.0, 40.0]; let src = js_array_from_f64(values.as_ptr(), values.len() as u32); assert_numeric_raw_values(src, &values); let reversed = js_array_to_reversed(src); - assert_numeric_raw_values(reversed, &[30.0, 2.0, 10.0]); + assert_numeric_raw_values(reversed, &[40.0, 30.0, 2.0, 10.0]); let sorted = js_array_to_sorted_default(src); - assert_numeric_raw_values(sorted, &[10.0, 2.0, 30.0]); + assert_numeric_raw_values(sorted, &[10.0, 2.0, 30.0, 40.0]); let numeric_replaced = js_array_with(src, 1.0, 99.0); - assert_numeric_raw_values(numeric_replaced, &[10.0, 99.0, 30.0]); + assert_numeric_raw_values(numeric_replaced, &[10.0, 99.0, 30.0, 40.0]); let insert = [7.0, 8.0]; let spliced = js_array_to_spliced(src, 1.0, 1.0, insert.as_ptr(), insert.len() as u32); - assert_numeric_raw_values(spliced, &[10.0, 7.0, 8.0, 30.0]); + assert_numeric_raw_values(spliced, &[10.0, 7.0, 8.0, 30.0, 40.0]); let str_ptr = crate::string::js_string_from_bytes(b"immutable-mixed".as_ptr(), 15); let str_value = @@ -1303,26 +1303,27 @@ fn test_numeric_array_layout_map_fast_path_downgrades_mapped_pointers() { arr = js_array_push_f64(arr, 1.0); arr = js_array_push_f64(arr, 2.0); arr = js_array_push_f64(arr, 3.0); + arr = js_array_push_f64(arr, 4.0); assert_eq!(js_array_is_numeric_f64_layout(arr), 1); let callback = crate::closure::js_closure_alloc(test_map_to_string as *const u8, 0); let mapped = js_array_map(arr, callback); - assert_eq!(js_array_length(mapped), 3); + assert_eq!(js_array_length(mapped), 4); assert_eq!( js_array_is_numeric_f64_layout(mapped), 0, "small map() results use a layout-only fast path and must still downgrade" ); assert_eq!( - crate::gc::test_layout_pointer_slot_count(mapped as usize, 3), - Some(3) + crate::gc::test_layout_pointer_slot_count(mapped as usize, 4), + Some(4) ); } #[test] fn test_numeric_array_layout_entries_outer_downgrades_inner_pairs_preserve() { - let values = [4.0, 5.0]; + let values = [4.0, 5.0, 6.0, 7.0]; let src = js_array_from_f64(values.as_ptr(), values.len() as u32); let entries = js_array_entries(src); diff --git a/crates/perry-runtime/src/gc/layout_tables.rs b/crates/perry-runtime/src/gc/layout_tables.rs index d1f5af7760..88fb8d4fa0 100644 --- a/crates/perry-runtime/src/gc/layout_tables.rs +++ b/crates/perry-runtime/src/gc/layout_tables.rs @@ -327,7 +327,8 @@ pub(crate) fn per_object_layout_table_sizes() -> (usize, usize) { /// `is_empty()`"). At the bottom of the range the asymmetry is total rather /// than merely lopsided: over a **single** slot a mask cannot skip anything at /// all, because the tracer consults `layout_pointer_bearing_bits` on that one -/// slot either way, so the entry is the mask's entire contribution. +/// slot either way, so the entry is the mask's entire contribution. At two or +/// three slots it can skip only one or two such checks. /// /// A tag check is exact at both mint sites: neither is reached for an object /// with an intact typed descriptor, so there are no raw-f64 slots whose bits a @@ -354,30 +355,26 @@ pub(in crate::gc) fn layout_mask_min_slots() -> usize { } } -/// Only single-slot payloads take the scan. This is deliberately the -/// *provable* end of the range: at one slot the mask demonstrably skips -/// nothing, so no judgement about tracing cost is being made. +/// Payloads with up to three slots take the scan. At that size the mask can +/// skip at most two exact tag checks, while one long-lived side-table entry +/// arms address cleanup on every allocation in the program. /// -/// Measured on the 19-benchmark corpus (quiet M1 mini, best-of-5, interleaved -/// against the same binaries with the policy disabled): +/// Measured on the 19-benchmark corpus (M1 mini, five shuffled interleaved +/// repeats against the same binaries, medians): /// -/// | bench | before | after | -/// |---|--:|--:| -/// | `interp` | 1.894 | **1.697** | -/// | `iso_miss` | 2.371 | **2.157** | -/// | `bench/mask_tax` | 0.1218 | **0.1049** | -/// | `bench/mask_tax_nopointer` (control) | 0.0929 | 0.0929 | +/// | bench | threshold 2 instructions | threshold 4 instructions | delta | +/// |---|--:|--:|--:| +/// | `interp` | 9,013,545,066 | 8,598,251,738 | **-4.61%** | +/// | `iso_miss` | 12,494,387,198 | 12,489,410,538 | -0.04% | /// -/// Every other benchmark — including the GC-heavy `tree`, `tree_wide`, -/// `retain*`, `cycles`, `deeplist` — is unchanged within noise. +/// Every other benchmark moved by less than 0.37%; `interp` peak RSS was +/// byte-identical at 32,964,608 in both arms. Wall ranges overlapped on the +/// contended host, so retired instructions are the primary signal. /// -/// Raising it pays roughly twice as much and costs test churn, both measured: -/// `9` and above gives `interp` 1.619 / `iso_miss` 2.046 with still no -/// regression on the corpus, but 21 tests in this crate encode "a small mixed -/// payload uses a mask" as a precondition (5 do at `2`, 11 at `3`, saturating -/// at 21 from `9`). That is a contract change worth making on purpose rather -/// than as a side effect of a perf patch. -pub(in crate::gc) const DEFAULT_MASK_MIN_SLOTS: usize = 2; +/// A current threshold sweep found the full `interp` win at `4`; higher values +/// did not retire fewer instructions. Keeping the smallest winning threshold +/// bounds the extra trace work and changes the fewest layout preconditions. +pub(in crate::gc) const DEFAULT_MASK_MIN_SLOTS: usize = 4; /// True when either per-object side table may hold an entry. `false` is a /// proof of emptiness (see [`PER_OBJECT_LAYOUTS_NONEMPTY`]); `true` is only a diff --git a/crates/perry-runtime/src/gc/tests/layout_trace.rs b/crates/perry-runtime/src/gc/tests/layout_trace.rs index d65bf5a1bd..c52cd2436a 100644 --- a/crates/perry-runtime/src/gc/tests/layout_trace.rs +++ b/crates/perry-runtime/src/gc/tests/layout_trace.rs @@ -158,13 +158,15 @@ fn test_layout_scan_trace_json_counts_pointer_slot_bytes() { ) .expect("test requested GC trace capture"); let child = crate::string::js_string_from_bytes(b"byte-child".as_ptr(), 10) as *mut u8; - let arr = crate::array::js_array_alloc_with_length(2); + let arr = crate::array::js_array_alloc_with_length(4); crate::array::js_array_set_f64(arr, 0, 1.0); crate::array::js_array_set_f64( arr, 1, f64::from_bits(STRING_TAG | (child as u64 & POINTER_MASK)), ); + crate::array::js_array_set_f64(arr, 2, 2.0); + crate::array::js_array_set_f64(arr, 3, 3.0); let valid_ptrs = build_valid_pointer_set(); assert!(try_mark_value( @@ -240,7 +242,7 @@ fn test_layout_mask_small_mixed_array_scans_exact_pointer_slot() { let child = crate::string::js_string_from_bytes(b"array-child".as_ptr(), 11) as *mut u8; let child_header = unsafe { header_from_user_ptr(child) }; - let arr = crate::array::js_array_alloc_with_length(3); + let arr = crate::array::js_array_alloc_with_length(4); crate::array::js_array_set_f64(arr, 0, 1.0); crate::array::js_array_set_f64( arr, @@ -248,8 +250,9 @@ fn test_layout_mask_small_mixed_array_scans_exact_pointer_slot() { f64::from_bits(STRING_TAG | (child as u64 & POINTER_MASK)), ); crate::array::js_array_set_f64(arr, 2, 3.0); + crate::array::js_array_set_f64(arr, 3, 4.0); - assert_eq!(test_layout_pointer_slot_count(arr as usize, 3), Some(1)); + assert_eq!(test_layout_pointer_slot_count(arr as usize, 4), Some(1)); let valid_ptrs = build_valid_pointer_set(); let mut worklist = Vec::new(); @@ -261,7 +264,7 @@ fn test_layout_mask_small_mixed_array_scans_exact_pointer_slot() { assert_eq!(test_trace_slot_reads(), 1); crate::array::js_array_set_f64(arr, 1, 2.0); - assert_eq!(test_layout_pointer_slot_count(arr as usize, 3), Some(0)); + assert_eq!(test_layout_pointer_slot_count(arr as usize, 4), Some(0)); clear_marks(); clear_mark_seeds(); @@ -284,14 +287,14 @@ fn test_pointer_store_restores_side_mask_from_stale_pointer_free() { let child0_h = unsafe { header_from_user_ptr(child0) }; let child1_h = unsafe { header_from_user_ptr(child1) }; - let arr = crate::array::js_array_alloc_with_length(2); + let arr = crate::array::js_array_alloc_with_length(4); // Store a pointer at index 0 -> SIDE_MASK + mask{0}. crate::array::js_array_set_f64( arr, 0, f64::from_bits(STRING_TAG | (child0 as u64 & POINTER_MASK)), ); - assert_eq!(test_layout_pointer_slot_count(arr as usize, 2), Some(1)); + assert_eq!(test_layout_pointer_slot_count(arr as usize, 4), Some(1)); // Reproduce the stale-state hazard: force POINTER_FREE while the mask{0} // entry is still present (`set_layout_state` only touches the state bits). @@ -316,7 +319,7 @@ fn test_pointer_store_restores_side_mask_from_stale_pointer_free() { "recording a pointer into an existing mask must restore SIDE_MASK" ); } - assert_eq!(test_layout_pointer_slot_count(arr as usize, 2), Some(2)); + assert_eq!(test_layout_pointer_slot_count(arr as usize, 4), Some(2)); let valid_ptrs = build_valid_pointer_set(); let mut worklist = Vec::new(); diff --git a/crates/perry-runtime/src/gc/tests/layout_trace/array_layout.rs b/crates/perry-runtime/src/gc/tests/layout_trace/array_layout.rs index a709498462..0bb8c7ee3e 100644 --- a/crates/perry-runtime/src/gc/tests/layout_trace/array_layout.rs +++ b/crates/perry-runtime/src/gc/tests/layout_trace/array_layout.rs @@ -52,25 +52,23 @@ fn test_layout_mask_overflow_fields_and_array_grow_transfer() { assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); } - // Two elements, not one: a mask over a single-slot payload can skip - // nothing, so `layout_note_slot` now leaves such an array in the - // tag-checked `GC_LAYOUT_UNKNOWN` state and mints no mask to grow or - // transfer. Two slots is the smallest payload that still exercises the - // grow/transfer path this test is about. - let arr = crate::array::js_array_alloc_with_length(2); + // Use the first mask-bearing payload size. Smaller arrays deliberately + // stay in the tag-checked `GC_LAYOUT_UNKNOWN` state, so they have no mask + // for this grow/transfer test to exercise. + let arr = crate::array::js_array_alloc_with_length(4); crate::array::js_array_set_f64( arr, 0, f64::from_bits(STRING_TAG | (child as u64 & POINTER_MASK)), ); let grown = crate::array::js_array_grow(arr, 128); - assert_eq!(test_layout_pointer_slot_count(grown as usize, 2), Some(1)); + assert_eq!(test_layout_pointer_slot_count(grown as usize, 4), Some(1)); - let moved = crate::array::js_array_alloc_with_length(2); + let moved = crate::array::js_array_alloc_with_length(4); unsafe { layout_transfer(grown as *mut u8, moved as *mut u8); } - assert_eq!(test_layout_pointer_slot_count(moved as usize, 2), Some(1)); + assert_eq!(test_layout_pointer_slot_count(moved as usize, 4), Some(1)); clear_marks(); clear_mark_seeds(); @@ -101,7 +99,7 @@ fn test_trace_array_uses_pointer_layout_mask() { let child = crate::string::js_string_from_bytes(b"array-child".as_ptr(), 11) as *mut u8; let child_header = unsafe { header_from_user_ptr(child) }; - let mixed = crate::array::js_array_alloc_with_length(3); + let mixed = crate::array::js_array_alloc_with_length(4); crate::array::js_array_set_f64(mixed, 0, 1.0); crate::array::js_array_set_f64( mixed, @@ -109,7 +107,8 @@ fn test_trace_array_uses_pointer_layout_mask() { f64::from_bits(STRING_TAG | (child as u64 & POINTER_MASK)), ); crate::array::js_array_set_f64(mixed, 2, 3.0); - assert_eq!(test_layout_pointer_slot_count(mixed as usize, 3), Some(1)); + crate::array::js_array_set_f64(mixed, 3, 4.0); + assert_eq!(test_layout_pointer_slot_count(mixed as usize, 4), Some(1)); let valid_ptrs = build_valid_pointer_set(); assert!(try_mark_value( @@ -190,12 +189,14 @@ fn test_array_mixed_bulk_producers_preserve_pointer_layout() { let child_header = unsafe { header_from_user_ptr(child) }; let child_box = f64::from_bits(STRING_TAG | (child as u64 & POINTER_MASK)); - let src = crate::array::js_array_alloc_with_length(2); + let src = crate::array::js_array_alloc_with_length(4); crate::array::js_array_set_f64(src, 0, 1.0); crate::array::js_array_set_f64(src, 1, child_box); + crate::array::js_array_set_f64(src, 2, 2.0); + crate::array::js_array_set_f64(src, 3, 3.0); let cloned = crate::array::js_array_clone(src); - assert_eq!(test_layout_pointer_slot_count(cloned as usize, 2), Some(1)); + assert_eq!(test_layout_pointer_slot_count(cloned as usize, 4), Some(1)); assert_array_root_trace_reads(cloned, 1); unsafe { assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); @@ -205,7 +206,7 @@ fn test_array_mixed_bulk_producers_preserve_pointer_layout() { let concatenated = crate::array::js_array_concat(crate::array::js_array_alloc(0), src); assert_eq!( - test_layout_pointer_slot_count(concatenated as usize, 2), + test_layout_pointer_slot_count(concatenated as usize, 4), Some(1) ); assert_array_root_trace_reads(concatenated, 1); @@ -236,34 +237,34 @@ fn test_array_mixed_bulk_producers_preserve_pointer_layout() { let map = crate::map::js_map_set(map, 7.0, child_box); let entries = crate::map::js_map_entries(map); // One entry, so the outer array is single-slot and carries no mask for the - // same reason as the set above; the pair it holds is two slots and still - // does. Both are traced either way, which is what the reads assertion and - // the child's mark bit below check. + // same reason as the set above; the two-slot pair it holds also stays in + // the tag-checked scan regime. Both are traced either way, which is what + // the reads assertion and the child's mark bit below check. assert_eq!(test_layout_pointer_slot_count(entries as usize, 1), None); let pair_box = crate::array::js_array_get_f64(entries, 0); let pair = (pair_box.to_bits() & POINTER_MASK) as *mut crate::array::ArrayHeader; - assert_eq!(test_layout_pointer_slot_count(pair as usize, 2), Some(1)); - assert_array_root_trace_reads(entries, 2); + assert_eq!(test_layout_pointer_slot_count(pair as usize, 2), None); + assert_array_root_trace_reads(entries, 3); unsafe { assert_ne!((*child_header).gc_flags & GC_FLAG_MARKED, 0); } clear_marks(); clear_mark_seeds(); - // Two slots, so this still goes through the mask: clearing the last + // Four slots, so this still goes through the mask: clearing the last // pointer empties it and restores `GC_LAYOUT_POINTER_FREE`, which is the // transition being asserted. A single-slot array never mints a mask now, // and `GC_LAYOUT_UNKNOWN` is one-way — such an array keeps being scanned // after the pointer is overwritten. That costs one tag check on one slot, // which is the whole reason the mask was not worth minting for it. - let overwritten = crate::array::js_array_alloc_with_length(2); + let overwritten = crate::array::js_array_alloc_with_length(4); crate::array::js_array_set_f64(overwritten, 0, child_box); assert_eq!( - test_layout_pointer_slot_count(overwritten as usize, 2), + test_layout_pointer_slot_count(overwritten as usize, 4), Some(1) ); crate::array::js_array_set_f64(overwritten, 0, 99.0); - assert_numeric_array_trace_free(overwritten, 2); + assert_numeric_array_trace_free(overwritten, 4); clear_marks(); clear_mark_seeds(); diff --git a/crates/perry-runtime/src/gc/tests/layout_trace/per_object_tables.rs b/crates/perry-runtime/src/gc/tests/layout_trace/per_object_tables.rs index bbf06adf7d..28d5f33407 100644 --- a/crates/perry-runtime/src/gc/tests/layout_trace/per_object_tables.rs +++ b/crates/perry-runtime/src/gc/tests/layout_trace/per_object_tables.rs @@ -585,35 +585,36 @@ fn test_addr_filter_proves_absence_while_the_global_flag_is_armed() { clear_mark_seeds(); } -/// A **single-slot** payload must never mint a per-object pointer mask. +/// A payload below the cutoff must never mint a per-object pointer mask. /// -/// The mask could not skip anything — the tracer tag-checks that one slot -/// either way — but the entry it creates arms `PER_OBJECT_LAYOUTS_NONEMPTY`, -/// which puts a two-map hash probe back on every allocation in the program for -/// as long as it lives. `interp.ts` minted ~1.8M of these (one per `[arg]` -/// environment array), grew `LAYOUT_SLOT_MASKS` past 400k live entries, and -/// spent ~19% of its runtime in `layout_forget_object` probing it. +/// The mask can skip at most two tag checks, but the entry it creates arms +/// `PER_OBJECT_LAYOUTS_NONEMPTY`, which puts a two-map hash probe back on every +/// allocation in the program for as long as it lives. /// /// The child must still be traced: `GC_LAYOUT_UNKNOWN` scans every slot and /// tag-checks it, which is exact for an object with no typed descriptor. #[test] -fn test_single_slot_pointer_payload_traces_without_a_side_table_entry() { +fn test_small_pointer_payload_traces_without_a_side_table_entry() { clear_marks(); clear_mark_seeds(); - assert_flag_sound("before single-slot store"); + assert_flag_sound("before small-payload store"); let child = crate::string::js_string_from_bytes(b"one-slot-child".as_ptr(), 14) as *mut u8; let child_header = unsafe { header_from_user_ptr(child) }; - let arr = crate::array::js_array_alloc_with_length(1); + let slots = crate::gc::layout_tables::DEFAULT_MASK_MIN_SLOTS - 1; + let arr = crate::array::js_array_alloc_with_length(slots as u32); + for i in 0..slots { + crate::array::js_array_set_f64(arr, i as u32, i as f64); + } crate::array::js_array_set_f64( arr, - 0, + (slots - 1) as u32, f64::from_bits(STRING_TAG | (child as u64 & POINTER_MASK)), ); assert!( test_per_object_tables_are_empty(), - "a one-slot pointer payload must not create a per-object record — one \ + "a below-cutoff pointer payload must not create a per-object record — one \ live entry taxes every allocation in the program" ); @@ -627,7 +628,7 @@ fn test_single_slot_pointer_payload_traces_without_a_side_table_entry() { assert_ne!( (*child_header).gc_flags & GC_FLAG_MARKED, 0, - "the one-slot child must still be traced through the tag-checked scan" + "the small-payload child must still be traced through the tag-checked scan" ); } diff --git a/crates/perry-runtime/src/gc/tests/oldgen.rs b/crates/perry-runtime/src/gc/tests/oldgen.rs index 0567358b6f..e28e28c1cd 100644 --- a/crates/perry-runtime/src/gc/tests/oldgen.rs +++ b/crates/perry-runtime/src/gc/tests/oldgen.rs @@ -1477,9 +1477,9 @@ fn test_minor_sweep_keeps_unmarked_old_object_layout_mask() { // A live old-gen object with one pointer slot, plus the slot-layout mask // the collector reads to find that pointer. - let old_obj = crate::arena::arena_alloc_gc_old(3 * 8, 8, GC_TYPE_OBJECT) as usize; + let old_obj = crate::arena::arena_alloc_gc_old(4 * 8, 8, GC_TYPE_OBJECT) as usize; unsafe { - std::ptr::write_bytes(old_obj as *mut u8, 0, 3 * 8); + std::ptr::write_bytes(old_obj as *mut u8, 0, 4 * 8); // A freshly allocated payload starts pointer-free; the first pointer // store is what promotes it to a side mask. crate::gc::layout_init_pointer_free(old_obj as *mut u8); @@ -1487,7 +1487,7 @@ fn test_minor_sweep_keeps_unmarked_old_object_layout_mask() { let child = crate::arena::arena_alloc_gc_old(16, 8, GC_TYPE_STRING) as usize; layout_note_slot(old_obj, 0, string_bits(child)); assert_eq!( - test_layout_pointer_slot_count(old_obj, 3), + test_layout_pointer_slot_count(old_obj, 4), Some(1), "precondition: the old object starts with a one-pointer slot mask" ); @@ -1498,7 +1498,7 @@ fn test_minor_sweep_keeps_unmarked_old_object_layout_mask() { let _ = sweep.finish_unbounded(); assert_eq!( - test_layout_pointer_slot_count(old_obj, 3), + test_layout_pointer_slot_count(old_obj, 4), Some(1), "#6892: minor sweep wiped the slot-layout mask of a live old-gen object" ); @@ -1519,23 +1519,23 @@ fn test_full_sweep_still_finalizes_unmarked_old_object() { clear_mark_seeds(); crate::arena::old_pages_begin_gc_cycle(); - let old_obj = crate::arena::arena_alloc_gc_old(3 * 8, 8, GC_TYPE_OBJECT) as usize; + let old_obj = crate::arena::arena_alloc_gc_old(4 * 8, 8, GC_TYPE_OBJECT) as usize; unsafe { - std::ptr::write_bytes(old_obj as *mut u8, 0, 3 * 8); + std::ptr::write_bytes(old_obj as *mut u8, 0, 4 * 8); // A freshly allocated payload starts pointer-free; the first pointer // store is what promotes it to a side mask. crate::gc::layout_init_pointer_free(old_obj as *mut u8); } let child = crate::arena::arena_alloc_gc_old(16, 8, GC_TYPE_STRING) as usize; layout_note_slot(old_obj, 0, string_bits(child)); - assert_eq!(test_layout_pointer_slot_count(old_obj, 3), Some(1)); + assert_eq!(test_layout_pointer_slot_count(old_obj, 4), Some(1)); // Full trace (`minor_sweep = false`): unmarked is provably dead. let mut sweep = IncrementalSweepState::new(false, true, None, false, false); let _ = sweep.finish_unbounded(); assert_eq!( - test_layout_pointer_slot_count(old_obj, 3), + test_layout_pointer_slot_count(old_obj, 4), None, "a full sweep must still finalize genuinely dead old-gen objects" ); diff --git a/crates/perry-runtime/src/gc/tests/typed_layout_intact_residual.rs b/crates/perry-runtime/src/gc/tests/typed_layout_intact_residual.rs index 4bb16c295c..486b8d7fc7 100644 --- a/crates/perry-runtime/src/gc/tests/typed_layout_intact_residual.rs +++ b/crates/perry-runtime/src/gc/tests/typed_layout_intact_residual.rs @@ -107,13 +107,13 @@ unsafe fn slot_bits(obj: *mut crate::ObjectHeader, slot: usize) -> u64 { /// `js_gc_declare_typed_shape_layout` was never called. Not calling it IS the /// plant — that call is the thing #7834 removes. /// -/// Three payload slots, because a payload below `layout_mask_min_slots()` makes +/// Four payload slots, because a payload below `layout_mask_min_slots()` makes /// `layout_note_slot` decline the mask and take `GC_LAYOUT_UNKNOWN` instead; /// this test wants the `SIDE_MASK` arm. unsafe fn plant_baked_instance(shape_id: u32, packed_keys: &[u8]) -> *mut crate::ObjectHeader { let obj = crate::object::js_object_alloc_with_shape( shape_id, - 3, + 4, packed_keys.as_ptr(), packed_keys.len() as u32, ); @@ -174,7 +174,7 @@ unsafe fn plant_descriptor_backed_instance( #[test] fn a_descriptorless_bake_drops_its_intact_claim_on_the_generic_downgrade() { unsafe { - let obj = plant_baked_instance(0x8115_0001, b"x\0y\0z\0"); + let obj = plant_baked_instance(0x8115_0001, b"x\0y\0z\0pad\0"); let child = string_bits(young_leaf()); crate::object::store_object_field_slot(obj, 0, child); @@ -225,7 +225,7 @@ fn a_descriptorless_bake_drops_its_intact_claim_on_the_generic_downgrade() { #[test] fn the_inline_raw_f64_arm_must_not_read_a_pointer_slot_as_a_double() { unsafe { - let obj = plant_baked_instance(0x8115_0002, b"a\0b\0c\0"); + let obj = plant_baked_instance(0x8115_0002, b"a\0b\0c\0pad\0"); let child = string_bits(young_leaf()); crate::object::store_object_field_slot(obj, 0, child); assert_eq!( @@ -382,7 +382,7 @@ fn a_contradicting_store_still_downgrades_through_the_descriptor_path() { #[test] fn a_conforming_raw_f64_store_leaves_the_bake_intact() { unsafe { - let obj = plant_baked_instance(0x8115_0005, b"u\0v\0w\0"); + let obj = plant_baked_instance(0x8115_0005, b"u\0v\0w\0pad\0"); crate::object::store_object_field_slot(obj, 0, 3.5f64.to_bits()); crate::object::store_object_field_slot(obj, 1, (-7.25f64).to_bits()); @@ -433,7 +433,7 @@ fn a_conforming_raw_f64_store_leaves_the_bake_intact() { #[test] fn the_bake_healed_itself_only_because_the_descriptor_probe_reads_the_same_bit() { unsafe { - let obj = plant_baked_instance(0x8115_0006, b"m\0n\0o\0"); + let obj = plant_baked_instance(0x8115_0006, b"m\0n\0o\0pad\0"); // The two questions disagree. That disagreement IS the #8115 state. assert!( diff --git a/crates/perry-runtime/src/json_tape_tests.rs b/crates/perry-runtime/src/json_tape_tests.rs index deab72b966..c419687173 100644 --- a/crates/perry-runtime/src/json_tape_tests.rs +++ b/crates/perry-runtime/src/json_tape_tests.rs @@ -332,7 +332,7 @@ fn force_materialize_numeric_lazy_array_preserves_raw_payload() { #[test] fn force_materialize_lazy_array_cache_downgrades_for_pointer_values() { - let input = br#"[1,2,3]"#; + let input = br#"[1,2,3,4]"#; let text = crate::string::js_string_from_bytes(input.as_ptr(), input.len() as u32); let lazy = with_built_tape(input, |tape| unsafe { alloc_lazy_array(tape, 0, count_array_length(tape, 0), text) @@ -351,15 +351,15 @@ fn force_materialize_lazy_array_cache_downgrades_for_pointer_values() { assert_eq!( reparse_materializations(), before + 1, - "1-of-3 cached is below the crossover, so this must reparse" + "1-of-4 cached is below the crossover, so this must reparse" ); - // The reparse produces a RawF64-layout array for `[1,2,3]`; patching + // The reparse produces a RawF64-layout array for `[1,2,3,4]`; patching // a STRING into slot 1 has to downgrade it, or the tracer would skip // a live pointer in an array flagged pointer-free. assert_eq!(crate::array::js_array_is_numeric_f64_layout(arr), 0); assert_eq!( - crate::gc::test_layout_pointer_slot_count(arr as usize, 3), + crate::gc::test_layout_pointer_slot_count(arr as usize, 4), Some(1) ); }