Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions changelog.d/11518-delete-regex-source-table.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
perf(runtime): deleted `REGEX_SOURCE_TABLE`; a RegExp is now identified by its
own GC header (#11503). The table was an address-keyed thread-local
`PtrHashMap<usize, RegexMetadata>` whose only payload was
`registered_owner: bool`, yet every RegExp construction inserted into it, every
copying minor rekeyed it, and every collection walked it (once from the
copied-minor from-space pass, once from the sweep-entry
`collect_dead_registered_regexps_post_trace` subphase) just to find dead
RegExps and clear their expandos — work the shared dead-owner fan-out
(`prune_dead_exotic_expando_owners`) already did in the same windows.

- `is_regex_pointer` / `is_valid_regex_ptr` / `is_registered_regex` answer from
the header alone (`GC_TYPE_REGEXP` + size + `REGEXP_MAGIC`), which they
already checked first; the table fallback only ever changed the answer for a
stale entry. Every reader of `registered_owner` was an "is this a regex we
allocated" membership check — none carried other semantics.
- `GC_TYPE_REGEXP` now uses the shared `GcMoveHookKind::ExoticExpandoOwner`
move hook and no finalize hook. `GcMoveHookKind::RegExpSideTables`,
`GcFinalizeHookKind::RegExpSideTables`, the copied-minor regex finalizer, the
sweep's `dead_regexps` list, the `REGEX_EVER_REGISTERED` latch and the
now-unused `prefetch_gc_owner_headers` / `exotic_expando_owner_clear_dead`
helpers are gone. A dead RegExp's expando entry is dropped by the dead-owner
fan-out; `expando_clear_on_alloc` at construction remains the backstop for an
owner that died pinned. Block-skip may now reclaim whole dead blocks holding
RegExps without visiting them.
- Gates: the `REGEX_SOURCE_TABLE` entry is deleted from
`scripts/gc_runtime_root_holders.json`; `scripts/shape_descriptor_census.py`
now requires RegExp's type metadata to carry `ExoticExpandoOwner` and
`GcFinalizeHookKind::None`. (The table was rekeyed by a move hook, not a
`visit_metadata_*` site, so `gc_rekeyed_key_tables.json` and
`DEAD_KEY_PRUNES` had no entry for it.)

Tests: `regexp_identity_is_the_header_not_an_address_registry`,
`regexp_gc_type_needs_no_bespoke_side_table_hooks`,
`test_dead_regexp_expando_pruned_on_full_gc`,
`test_live_regexp_expando_survives_full_gc`; the copied-minor
`nursery_regexp_that_dies_young_is_finalized_by_the_copied_minor` and
`test_movable_regexp_evacuation_migrates_all_address_owned_state` now assert
the expando is dropped / migrated (and that a copying minor ran) instead of
reading the deleted table.
10 changes: 1 addition & 9 deletions crates/perry-runtime/src/gc/copying_phase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ impl CopyingMinorPhaseDiag {
let mut out = String::new();
write!(
out,
"root_scan={}/{} copy_evacuation={}/{}/{} remembered_set_young_logs={}/{}/{} promotion={}/{}/{} dead_owner_side_table_pruning={}{} from_space_finalization={}/map:{}/{}+set:{}/{}+errors:{}/{}+regex:{}/{}+lazytape:{}/{} forwarding_fixups={} block_reset_flip={} other={} phase_sum_us={}",
"root_scan={}/{} copy_evacuation={}/{}/{} remembered_set_young_logs={}/{}/{} promotion={}/{}/{} dead_owner_side_table_pruning={}{} from_space_finalization={}/map:{}/{}+set:{}/{}+errors:{}/{}+lazytape:{}/{} forwarding_fixups={} block_reset_flip={} other={} phase_sum_us={}",
self.root_scan_ns / 1000,
scan_us,
self.copy_evacuation_ns / 1000,
Expand All @@ -120,8 +120,6 @@ impl CopyingMinorPhaseDiag {
finalization.sets,
finalization.errors_ns / 1000,
finalization.errors,
finalization.regex_ns / 1000,
finalization.regexps,
finalization.lazy_tape_ns / 1000,
finalization.lazy_tapes,
self.forwarding_fixups_ns / 1000,
Expand All @@ -143,8 +141,6 @@ pub(super) struct CopiedMinorFinalizationDiag {
pub(super) sets: usize,
pub(super) errors_ns: u64,
pub(super) errors: usize,
pub(super) regex_ns: u64,
pub(super) regexps: usize,
pub(super) dead_owner_ns: u64,
pub(super) dead_owner_detail: String,
pub(super) lazy_tapes: usize,
Expand Down Expand Up @@ -175,10 +171,6 @@ pub(super) fn finalize_dead_copied_minor_from_space_side_allocations() -> Copied
crate::node_submodules::diagnostics_gc::finalize_dead_copied_minor_from_space_errors();
out.errors_ns = start.map_or(0, |start| start.elapsed().as_nanos() as u64);

let start = diag.then(Instant::now);
out.regexps = crate::regex::finalize_dead_copied_minor_from_space_regexps();
out.regex_ns = start.map_or(0, |start| start.elapsed().as_nanos() as u64);

let start = diag.then(Instant::now);
out.lazy_tapes = crate::json_tape_store::finalize_dead_copied_minor_from_space_lazy_tapes();
out.lazy_tape_ns = start.map_or(0, |start| start.elapsed().as_nanos() as u64);
Expand Down
4 changes: 2 additions & 2 deletions crates/perry-runtime/src/gc/dead_owner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,8 @@ impl PostTraceProbe {
/// forwarded — every live from-space object was evacuated (FORWARDED) or is
/// pinned-and-marked by this point. Mirrors `is_dead_copied_minor_from_space_map`.
/// Crate-visible form for the per-type registry walkers that finalize their
/// own dead from-space instances after a copied minor (`regex`): is `addr` a
/// from-space `obj_type` cell that was neither evacuated nor pinned?
/// own dead from-space instances after a copied minor (`json_tape_store`): is
/// `addr` a from-space `obj_type` cell that was neither evacuated nor pinned?
pub(crate) fn owner_is_dead_copied_minor_from_space_of_type(addr: usize, obj_type: u8) -> bool {
owner_is_dead_copied_minor_from_space(addr, Some(obj_type))
}
Expand Down
7 changes: 0 additions & 7 deletions crates/perry-runtime/src/gc/oldgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1154,7 +1154,6 @@ enum SweepCycleSubphase {
pub(super) struct IncrementalSweepState {
subphase: SweepCycleSubphase,
dead_sets: Vec<usize>,
dead_regexps: Vec<usize>,
dead_buffers: Vec<usize>,
dead_typed_arrays: Vec<usize>,
dead_lazy_arrays: Vec<usize>,
Expand All @@ -1177,7 +1176,6 @@ impl IncrementalSweepState {
Self {
subphase: SweepCycleSubphase::Malloc,
dead_sets: Vec::new(),
dead_regexps: Vec::new(),
dead_buffers: Vec::new(),
dead_typed_arrays: Vec::new(),
dead_lazy_arrays: Vec::new(),
Expand Down Expand Up @@ -1215,7 +1213,6 @@ impl IncrementalSweepState {
synchronous_full_trace,
);
self.dead_sets = crate::set::collect_dead_registered_sets_post_trace(full_trace);
self.dead_regexps = crate::regex::collect_dead_registered_regexps_post_trace(full_trace);
self.dead_buffers = crate::buffer::collect_dead_registered_buffers_post_trace(full_trace);
self.dead_typed_arrays =
crate::typedarray::collect_dead_registered_typed_arrays_post_trace(full_trace);
Expand All @@ -1227,7 +1224,6 @@ impl IncrementalSweepState {
registered_lazy_array_is_dead_post_trace(addr, full_trace)
});
if !self.dead_sets.is_empty()
|| !self.dead_regexps.is_empty()
|| !self.dead_buffers.is_empty()
|| !self.dead_typed_arrays.is_empty()
|| !self.dead_lazy_arrays.is_empty()
Expand All @@ -1252,8 +1248,6 @@ impl IncrementalSweepState {
while spent < budget {
if let Some(addr) = self.dead_sets.pop() {
crate::set::finalize_collected_dead_set(addr);
} else if let Some(addr) = self.dead_regexps.pop() {
crate::regex::finalize_collected_dead_regexp(addr);
} else if let Some(addr) = self.dead_buffers.pop() {
crate::buffer::finalize_collected_dead_buffer(addr);
} else if let Some(addr) = self.dead_typed_arrays.pop() {
Expand All @@ -1267,7 +1261,6 @@ impl IncrementalSweepState {
spent += 1;
}
if self.dead_sets.is_empty()
&& self.dead_regexps.is_empty()
&& self.dead_buffers.is_empty()
&& self.dead_typed_arrays.is_empty()
&& self.dead_lazy_arrays.is_empty()
Expand Down
29 changes: 0 additions & 29 deletions crates/perry-runtime/src/gc/prefetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,32 +78,3 @@ pub(super) fn prefetch_boxed_child(bits: u64) {
prefetch_read(addr.saturating_sub(super::GC_HEADER_SIZE));
}
}

/// Pipeline header reads for an existing stable ownership walk.
///
/// The cloned iterator only supplies upcoming addresses to the prefetch
/// instruction. The original iterator still yields every owner exactly once,
/// in its original order. No address vector or registry is created here.
/// Callers must keep the iterator's source unchanged until the walk finishes.
/// The lookahead is shared with the collector's existing header walks.
pub(crate) fn prefetch_gc_owner_headers<I>(owners: I) -> impl Iterator<Item = usize>
where
I: Iterator<Item = usize> + Clone,
{
#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
{
let mut ahead = owners.clone();
for addr in ahead.by_ref().take(PREFETCH_DISTANCE) {
prefetch_read(addr.saturating_sub(super::GC_HEADER_SIZE));
}
owners.inspect(move |_| {
if let Some(addr) = ahead.next() {
prefetch_read(addr.saturating_sub(super::GC_HEADER_SIZE));
}
})
}
#[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))]
{
owners
}
}
78 changes: 65 additions & 13 deletions crates/perry-runtime/src/gc/tests/copying/survival_and_malloc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -929,7 +929,8 @@ fn test_movable_regexp_evacuation_migrates_all_address_owned_state() {
let re = crate::regex::test_alloc_nursery_regexp_for_move("move/source", "gi");
let old_addr = re as usize;
assert!(crate::arena::pointer_in_nursery(old_addr));
assert!(crate::regex::test_regex_pointer_entry_exists(old_addr));
// Identity is the header: the fixture registers the address nowhere.
assert!(crate::regex::is_registered_regex(old_addr));

crate::object::exotic_expando::test_seed_exotic_expando_entry(
old_addr,
Expand All @@ -938,15 +939,20 @@ fn test_movable_regexp_evacuation_migrates_all_address_owned_state() {
);
js_shadow_slot_set(0, ptr_bits(old_addr));

let cycles = crate::gc::copying_minor_cycles();
let _ = gc_collect_minor();
assert!(
crate::gc::copying_minor_cycles() > cycles,
"test premise: a copying minor ran"
);

let new_addr = (js_shadow_slot_get(0) & POINTER_MASK) as usize;
assert_ne!(new_addr, 0, "rooted RegExp must survive the copied minor");
assert_ne!(new_addr, old_addr, "the RegExp must be evacuated");
assert!(crate::regex::regex_header_has_magic(new_addr as *const _));
assert!(crate::regex::is_registered_regex(new_addr));

assert!(crate::regex::test_regex_pointer_entry_exists(new_addr));
assert!(!crate::regex::test_regex_pointer_entry_exists(old_addr));
// The expando owner key moves with the header (`ExoticExpandoOwner`).
assert!(crate::object::exotic_expando::test_exotic_expando_entry_exists(new_addr));
assert!(!crate::object::exotic_expando::test_exotic_expando_entry_exists(old_addr));

Expand Down Expand Up @@ -1023,12 +1029,40 @@ fn test_copied_minor_promotable_census_filtered_walk_matches_unfiltered() {
);
}

/// Set a user property on a RegExp through the production `[[Set]]` path, so
/// the entry is the one a program's `re.tag = v` would create.
fn set_regexp_expando(addr: usize, key: &str, value: crate::value::JSValue) {
assert!(
matches!(
crate::object::exotic_expando::exotic_expando_kind(addr),
Some(crate::object::exotic_expando::ExoticKind::RegExp)
),
"test premise: the header classifies as a RegExp exotic"
);
let receiver = f64::from_bits(ptr_bits(addr));
let stored = unsafe {
crate::object::exotic_expando::exotic_set_property(
addr,
crate::object::exotic_expando::ExoticKind::RegExp,
key,
f64::from_bits(value.bits()),
receiver,
)
};
assert!(stored, "test premise: the RegExp accepted the expando");
assert!(crate::object::exotic_expando::test_exotic_expando_entry_exists(addr));
}

/// #9819 follow-up: `js_regexp_new` allocates the header in the NURSERY. A
/// header that dies young must lose its registry entries and its GC program
/// must be reclaimed by the copied minor — because the from-space
/// flip runs no per-object finalize hooks. Without
/// `finalize_dead_copied_minor_from_space_regexps` the dead address stays in
/// the owner registry and dead programs would remain reachable.
/// header that dies young must lose its address-keyed state and its GC program
/// must be reclaimed by the copied minor — even though the from-space flip runs
/// no per-object finalize hooks.
///
/// #11503: RegExp has no registry or death hook of its own any more. Its only
/// address-keyed state is the user's expandos, and the dead-owner fan-out
/// (`prune_dead_exotic_expando_owners`) is what must drop a dead header's
/// entry. If it did not, a fresh cell recycled at the dead header's address
/// would read the dead RegExp's properties as its own.
#[test]
fn nursery_regexp_that_dies_young_is_finalized_by_the_copied_minor() {
let _guard = CopyingNurseryTestGuard::new(1);
Expand All @@ -1041,8 +1075,8 @@ fn nursery_regexp_that_dies_young_is_finalized_by_the_copied_minor() {
crate::arena::pointer_in_nursery(dead_addr),
"the header must be nursery-allocated"
);
assert!(crate::regex::test_regex_pointer_entry_exists(dead_addr));
assert!(crate::regex::test_regex_source_entry_exists(dead_addr));
set_regexp_expando(dead_addr, "tag", crate::value::JSValue::int32(7));
set_regexp_expando(live_addr, "tag", crate::value::JSValue::int32(42));
fn programs() -> usize {
let mut cursor =
crate::arena::ArenaObjectCursor::new(crate::arena::ArenaWalkOrder::Address);
Expand All @@ -1069,17 +1103,35 @@ fn nursery_regexp_that_dies_young_is_finalized_by_the_copied_minor() {

// Only `live` is rooted; `dead` is garbage.
js_shadow_slot_set(0, ptr_bits(live_addr));
let cycles = crate::gc::copying_minor_cycles();
let _ = gc_collect_minor();
assert!(
crate::gc::copying_minor_cycles() > cycles,
"test premise: a copying minor ran"
);

let live_new = (js_shadow_slot_get(0) & POINTER_MASK) as usize;
assert_ne!(live_new, 0, "the rooted RegExp must survive");
assert_ne!(live_new, live_addr, "the rooted RegExp must be evacuated");
assert!(crate::regex::regex_header_has_magic(live_new as *const _));
assert!(crate::regex::test_regex_pointer_entry_exists(live_new));
assert!(crate::regex::is_registered_regex(live_new));

assert!(
!crate::regex::test_regex_pointer_entry_exists(dead_addr),
"a nursery RegExp that died must be removed from REGEX_POINTERS by the copied minor"
!crate::object::exotic_expando::test_exotic_expando_entry_exists(dead_addr),
"a nursery RegExp that died must lose its expando entry in the copied minor"
);
assert!(
crate::object::exotic_expando::test_exotic_expando_entry_exists(live_new),
"the surviving RegExp's expando must follow it to its new address"
);
assert_eq!(
crate::object::exotic_expando::value_lookup(
crate::object::exotic_expando::ExoticKind::RegExp,
live_new,
"tag",
),
Some(crate::value::JSValue::int32(42).bits()),
"the surviving RegExp keeps its own value, not the dead one's"
);
assert_eq!(
programs(),
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-runtime/src/gc/tests/dead_owner_side_tables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,8 @@ fn test_dom_exception_set_cleared_with_error_side_tables() {
}

mod meta_and_shape_records;
#[cfg(feature = "regex-engine")]
mod regexp_expandos;

// ── FUNCTION_CLASS_IDS (#8040) ──────────────────────────────────────────────
//
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//! #11503: a RegExp's user properties (`re.tag = v`) are its ONLY
//! address-keyed state. `REGEX_SOURCE_TABLE` and the per-type death walks that
//! enumerated it are gone, so the shared dead-owner fan-out
//! (`prune_dead_exotic_expando_owners`) is now the one thing that drops a dead
//! RegExp's expando entry on the non-copying cycle kinds. The copied-minor
//! counterpart is `nursery_regexp_that_dies_young_is_finalized_by_the_copied_minor`.
//!
//! A stale entry is not only a leak: `expando_clear_on_alloc` covers a RegExp
//! or Date recycled at the address, but any other exotic kind born there reads
//! the dead RegExp's properties as its own.

use super::*;

/// A production-constructed RegExp, unrooted once the caller's scope ends.
fn construct_regexp(pattern: &str) -> usize {
let scope = RuntimeHandleScope::new();
let source = scope.root_string_ptr(crate::string::js_string_from_bytes(
pattern.as_ptr(),
pattern.len() as u32,
));
let flags = scope.root_string_ptr(crate::string::js_string_from_bytes(b"g".as_ptr(), 1));
let re = source.with_const_ptr(|source| {
flags.with_const_ptr(|flags| crate::regex::js_regexp_new(source, flags))
});
assert!(
crate::regex::is_registered_regex(re as usize),
"test premise: the header identifies as a RegExp"
);
re as usize
}

#[test]
fn test_dead_regexp_expando_pruned_on_full_gc() {
let _guard = GcTestIsolationGuard::with_realm_bootstrapped();
let addr = construct_regexp("dies-before-the-full-trace");
crate::object::exotic_expando::test_seed_exotic_expando_entry(
addr,
"tag",
crate::value::JSValue::int32(7).bits(),
);
assert!(crate::object::exotic_expando::test_exotic_expando_entry_exists(addr));
// The construction cache holds the compiled program, not the header, but
// evict it anyway so nothing the fixture made is reachable.
crate::regex::perex_cache::clear_for_tests();

// No roots: the RegExp is dead at the full trace.
full_gc_with_no_block_persistence();

assert!(
!crate::object::exotic_expando::test_exotic_expando_entry_exists(addr),
"a dead RegExp's EXOTIC_EXPANDO entry must be pruned by the full \
collection's dead-owner fan-out"
);
}

#[test]
fn test_live_regexp_expando_survives_full_gc() {
let _guard = CopyingNurseryTestGuard::new(1);
let addr = construct_regexp("stays-live-across-the-full-trace");
crate::object::exotic_expando::test_seed_exotic_expando_entry(
addr,
"tag",
crate::value::JSValue::int32(42).bits(),
);
js_shadow_slot_set(0, ptr_bits(addr));

full_gc();

// Full mark-sweep is non-moving: the rooted RegExp keeps its address.
assert_eq!((js_shadow_slot_get(0) & POINTER_MASK) as usize, addr);
assert!(crate::regex::is_registered_regex(addr));
assert_eq!(
crate::object::exotic_expando::value_lookup(
crate::object::exotic_expando::ExoticKind::RegExp,
addr,
"tag",
),
Some(crate::value::JSValue::int32(42).bits()),
"a live RegExp's expando must survive a full GC"
);
js_shadow_slot_set(0, 0);
}
Loading
Loading