Skip to content
Closed
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
1 change: 1 addition & 0 deletions changelog.d/10254-tiny-parse-nursery-cap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **perf(gc): a loop of small `JSON.parse` calls no longer grows the young generation to 48 MB before collecting (#10254).** The tiny-parse guard (#9831) forces a collection at a parse boundary only when total arena in-use reaches its 48 MB floor, and nothing else arms the nursery safepoint for a loop that allocates only inside the parser. The parse boundary now also collects when the young generation reaches its scavenge cap. A minor lowers that quantity to its survivors, so the new arm cannot re-fire until the cap has been refilled. On the quiet bench mini, `small_record:parse` peak RSS fell from 80 to 32 MiB (Node 59) and `object_1k:parse` from 65 to 32 MiB (Node 61), with CPU unchanged. No other row of the 50-row JSON matrix moved beyond noise, and every gated gc-ratchet counter is identical to main.
36 changes: 32 additions & 4 deletions crates/perry-runtime/src/gc/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,24 @@ pub(super) fn tiny_parse_pressure_due_with(
&& in_use >= base.saturating_add(tiny_parse_pressure_headroom_bytes(step))
}

/// Should a tiny-parse boundary collect under the generational collector?
///
/// The priced in-use guard ([`tiny_parse_pressure_due`]) answers "has the
/// arena grown enough to be worth a collection", on total in-use, which a
/// collection cannot lower below the live set (#9831). It never asked the
/// generational question: is the young generation at its scavenge cap? A loop
/// of tiny `JSON.parse` calls allocates only under the parser's suppression
/// window and at bounded inline-object births, so nothing else arms the
/// nursery safepoint for it, and the young generation grew to the guard's
/// 48 MB floor before its first minor: `small_record:parse` peaked at 80 MiB
/// against Node's 59 with 0–9 ‰ of each collection surviving. The nursery cap
/// is safe to consult where the absolute in-use guard was not: a minor lowers
/// the quantity it tests to the survivors, so it cannot fire again until the
/// cap has been refilled.
pub(super) fn tiny_parse_generational_collection_due(in_use: usize, in_use_trigger: usize) -> bool {
tiny_parse_pressure_due(in_use, in_use_trigger) || young_scavenge_cap_due()
}

/// The live [`tiny_parse_pressure_due_with`]: current base and step.
pub(super) fn tiny_parse_pressure_due(in_use: usize, in_use_trigger: usize) -> bool {
#[cfg(test)]
Expand Down Expand Up @@ -1525,7 +1543,12 @@ fn gc_bump_malloc_trigger_inner(collect_now: bool) {
// The guard now also requires the arena to have grown past the
// productivity-priced headroom since the last collection ended.
let in_use = crate::arena::arena_in_use_bytes();
if !tiny_parse_pressure_due(in_use, tiny_parse_in_use_trigger_for_mode()) {
let due = if use_gen_gc {
tiny_parse_generational_collection_due(in_use, tiny_parse_in_use_trigger_for_mode())
} else {
tiny_parse_pressure_due(in_use, tiny_parse_in_use_trigger_for_mode())
};
if !due {
return;
}
if use_gen_gc {
Expand Down Expand Up @@ -1581,7 +1604,12 @@ fn gc_collect_pending_suppressed_parse_slow() {
// boundary collection from stacking a second minor on top of it. A request
// nothing has satisfied is still due and still collects.
let in_use = crate::arena::arena_in_use_bytes();
if !tiny_parse_pressure_due(in_use, tiny_parse_in_use_trigger_for_mode()) {
let due = if gen_gc_enabled() {
tiny_parse_generational_collection_due(in_use, tiny_parse_in_use_trigger_for_mode())
} else {
tiny_parse_pressure_due(in_use, tiny_parse_in_use_trigger_for_mode())
};
if !due {
return;
}
diag_tiny_parse_forced_collection("parse_boundary", in_use);
Expand Down Expand Up @@ -1609,8 +1637,8 @@ pub fn gc_schedule_parse_boundary_collection_if_pressure() {
return;
}
// #9831: priced the same way as the post-parse guard above — see
// `tiny_parse_pressure_due_with`.
if !tiny_parse_pressure_due(
// `tiny_parse_pressure_due_with` — plus the young generation's own cap.
if !tiny_parse_generational_collection_due(
crate::arena::arena_in_use_bytes(),
gc_tiny_parse_in_use_trigger_dyn_bytes(),
) {
Expand Down
40 changes: 40 additions & 0 deletions crates/perry-runtime/src/gc/tests/tiny_parse_pressure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,43 @@ fn a_finished_collection_moves_the_base_to_the_post_collection_reading() {
"the base must be the post-collection `arena_in_use_bytes()` reading"
);
}

/// The nursery cap schedules a tiny-parse boundary collection on its own, well
/// below the priced in-use guard, and only while the cap is actually due.
#[test]
fn a_due_nursery_cap_schedules_the_boundary_collection_below_the_in_use_guard() {
use super::super::policy::{
gc_schedule_parse_boundary_collection_if_pressure, ScavengeNurseryCapTestGuard,
GC_SUPPRESSED_TINY_PARSE_COLLECTION_PENDING,
};
use super::support::*;
let _isolation = GcTestIsolationGuard::new();
let _pacing = crate::gc::policy::force_moving_gc_pacing();
let pending = || GC_SUPPRESSED_TINY_PARSE_COLLECTION_PENDING.with(std::cell::Cell::get);
GC_SUPPRESSED_TINY_PARSE_COLLECTION_PENDING.with(|p| p.set(false));
let filler = [b'j'; 64];
crate::string::js_string_from_bytes(filler.as_ptr(), filler.len() as u32);
let in_use = crate::arena::arena_in_use_bytes();
assert!(
!tiny_parse_pressure_due(in_use, 48 * MB),
"fixture: the priced in-use guard must not be due, or this proves nothing"
);

{
let _cap = ScavengeNurseryCapTestGuard::due_at_bytes(usize::MAX);
gc_schedule_parse_boundary_collection_if_pressure();
assert!(
!pending(),
"neither the guard nor the cap is due: nothing scheduled"
);
}
{
let _cap = ScavengeNurseryCapTestGuard::due_at_bytes(1);
gc_schedule_parse_boundary_collection_if_pressure();
assert!(
pending(),
"a due nursery cap schedules the boundary collection"
);
}
GC_SUPPRESSED_TINY_PARSE_COLLECTION_PENDING.with(|p| p.set(false));
}
Loading
Loading