You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
benchmarks/json_performancerecords_array_8m:scan (parse 8 MB, iterate every record, drop; 10 parses) peaks at 188 MiB RSS against Node/Bun's 110 on current main (1.7×; 163 MiB = 1.48× on the combined parity branch). PERRY_GC_DIAG=1 over the whole run:
The absolute first-crossing arm (old_in_use ≥ 48 MB && baseline < 48 MB) is exempted while GC_MAJOR_PACING_RETAINING is set, and a young generation that survives at 999 ‰ sets it on every minor.
The three instruments that bound the untraced cohort act on evidence this workload never produces: untraced_promotion_budget_bytes (128 MB floor) forces a measuring minor, which measures the current young generation (again ~100 % live, so no contradiction); implied_dead_bytes charges promoted × (1000 − 980)/1000 = 2 % against the 32 MB PROMOTED_DEAD_BUDGET_BYTES, i.e. a full after 1.6 GB of promotions; request_old_reclaim_for_untraced_promotions needs a contradicting measurement.
So the predictor is right about survival (the tree is live at the minor) and wrong about lifetime (it dies right after), and no instrument observes lifetime. The garbage is reclaimed only when arena-growth escalation trips.
Candidate design (GC policy; gc-ratchet corpus before/after, then the cc rows on perrymaster, and both CPU and peak RSS on the JSON matrix — never trade the CPU lead for RSS)
Bound the unverified old-gen cohort: track promoted_since_last_full (bytes promoted by untraced or in-place promotions since the last full) and make old-reclaim due when it exceeds max(floor, k × old_live_at_last_full) with, e.g., floor 64 MB and k = 2. Rationale: the cohort's liveness is assumed, not measured; a full re-establishes it at a cost proportional to the verified live set, so bounding the unverified part by a multiple of the verified part keeps total major work linear (the #7592 argument) while capping residency.
Expected on the rows (from the diag numbers): 8m:scan live after a full ≈ 31 MB → a full every ~2.8 iterations → peak ≈ 31 + 64 + 23 ≈ 118 MiB (Node 110) at roughly +5 ms per iteration on a cell that is currently 6 % ahead on CPU. 20m family: live ≈ 78 MB → band 156 MB → a full every ~2.7 iterations, CPU still ahead of the 208 ms best, RSS roughly unchanged — the 20 MB rows cannot reach RSS parity this way (one dead 58 MB tree resident is already the gap), only a materially cheaper full mark can (a JSON tree marks at ~47 ns/object today; see #10169's design 3).
Not in scope, recorded here so it is not re-diagnosed: small_record:parse peaks at 80 vs 60 MiB because minors fire every ~50 MB — the tiny-parse guard's 48 MB floor from #9831/#9838, which exists to stop the cc minor storms; lowering it is a cc-rig decision, not a JSON-row one.
Symptom
benchmarks/json_performancerecords_array_8m:scan(parse 8 MB, iterate every record, drop; 10 parses) peaks at 188 MiB RSS against Node/Bun's 110 on currentmain(1.7×; 163 MiB = 1.48× on the combined parity branch).PERRY_GC_DIAG=1over the whole run:untraced=truein-place promotions (the cheap path perf(gc): promote a fully-live young generation without tracing it — retain −33.6%, deeplist −43% #7888 built for exactly this: a fully-live young generation);old_in_use=105 MB,arena_total=146 MB— four to five 23 MB trees in old gen of which one is live.Same shape on
records_array_20mparse/scan/sparse (peak 256 vs 220–225, 1.14×) and onrecords_object_8m:parse.Cause
Each iteration's tree is promoted whole and dies one iteration later, in old gen. Nothing then reclaims it:
credit_promoted_bytes_to_old_baselineadds every promoted byte toGC_LAST_OLD_RECLAIM_IN_USE_BYTES(perf(gc): main regressed the retain cluster 2.2-4.8x — retain now runs 2 full collections where it ran none (suspect #7901/#7902) #7965, deliberately — a pinned baseline degenerates the proportional band into a quadratic constant-band pacer onretain). Soold_in_use − baselinestays ≈ 0 and the proportional arm ofold_reclaim_pressure_duenever fires.old_in_use ≥ 48 MB && baseline < 48 MB) is exempted whileGC_MAJOR_PACING_RETAININGis set, and a young generation that survives at 999 ‰ sets it on every minor.untraced_promotion_budget_bytes(128 MB floor) forces a measuring minor, which measures the current young generation (again ~100 % live, so no contradiction);implied_dead_byteschargespromoted × (1000 − 980)/1000= 2 % against the 32 MBPROMOTED_DEAD_BUDGET_BYTES, i.e. a full after 1.6 GB of promotions;request_old_reclaim_for_untraced_promotionsneeds a contradicting measurement.So the predictor is right about survival (the tree is live at the minor) and wrong about lifetime (it dies right after), and no instrument observes lifetime. The garbage is reclaimed only when arena-growth escalation trips.
Candidate design (GC policy; gc-ratchet corpus before/after, then the cc rows on perrymaster, and both CPU and peak RSS on the JSON matrix — never trade the CPU lead for RSS)
Bound the unverified old-gen cohort: track
promoted_since_last_full(bytes promoted by untraced or in-place promotions since the last full) and make old-reclaim due when it exceedsmax(floor, k × old_live_at_last_full)with, e.g., floor 64 MB and k = 2. Rationale: the cohort's liveness is assumed, not measured; a full re-establishes it at a cost proportional to the verified live set, so bounding the unverified part by a multiple of the verified part keeps total major work linear (the #7592 argument) while capping residency.Expected on the rows (from the diag numbers):
8m:scanlive after a full ≈ 31 MB → a full every ~2.8 iterations → peak ≈ 31 + 64 + 23 ≈ 118 MiB (Node 110) at roughly +5 ms per iteration on a cell that is currently 6 % ahead on CPU.20mfamily: live ≈ 78 MB → band 156 MB → a full every ~2.7 iterations, CPU still ahead of the 208 ms best, RSS roughly unchanged — the 20 MB rows cannot reach RSS parity this way (one dead 58 MB tree resident is already the gap), only a materially cheaper full mark can (a JSON tree marks at ~47 ns/object today; see #10169's design 3).Not in scope, recorded here so it is not re-diagnosed:
small_record:parsepeaks at 80 vs 60 MiB because minors fire every ~50 MB — the tiny-parse guard's 48 MB floor from #9831/#9838, which exists to stop the cc minor storms; lowering it is a cc-rig decision, not a JSON-row one.Reproduce
Related: #10169 / #10177 (the stringify-result half of the same picture), #10123, #7965, #7902, #7888.