Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
239d440
perf(gc): young-entry logs for the side-table root scanners
Sep 5, 2026
fa0e5ee
style(gc): rustfmt and split the four files this change pushed past t…
Sep 5, 2026
a6e3fc0
fix(gc): make rule 1 enforceable — the young-log tests could not fail
Sep 5, 2026
06c5a19
test(gc): cover the four `shapes.indices` arm sites the audit found u…
Sep 5, 2026
47e86e8
perf(gc): recycle the young logs' buffers instead of regrowing them e…
Sep 5, 2026
3ac360d
fix(gc): arm the young log from `family_append_fresh` too
Sep 5, 2026
5ea8feb
fix(regex): a built header must carry every program its pattern needs
Sep 5, 2026
54a6c72
docs(regex): correct the coherence test's doc comment to the fix that…
Sep 5, 2026
bda7683
docs(changelog): key the regex coherence fragment to PR 9801
Sep 5, 2026
afe73a7
diag(ic): split every PIC prime by whether the site already held that…
Sep 5, 2026
511bb36
perf(ic): give the full-outline property get the monomorphic hit it n…
Sep 5, 2026
3f98866
fix(ci): number the changelog fragment and gate an import the `warnin…
Sep 5, 2026
98f92b0
fix(codegen): require dense storage for static numeric array proofs
Sep 5, 2026
f611df0
docs: number numeric array proof changelog for PR 9803
Sep 5, 2026
7ae347e
fix(test): export the strategy-aware provider stream constructor
Sep 5, 2026
629f684
fix(test): retain the Response body initialization reset helper
Sep 5, 2026
04d8752
fix(runtime): share hot TLS declaration identities across providers
Sep 5, 2026
548a47d
test(runtime): isolate provider TLS regression probes
Sep 5, 2026
31c1d76
test: synchronize stdin lifecycle input with child readiness
Sep 5, 2026
a3a39a5
fix(runtime): follow complete custom array prototype chains
Sep 5, 2026
0ca0756
fix(runtime): observe class iterator prototype replacements
Sep 5, 2026
5659611
fix(codegen): retain typed-array owners across specialized calls
Sep 5, 2026
0ef943a
fix(gc-ratchet): respect documented counter exclusions during checks
Sep 5, 2026
8db88e8
style: cargo fmt
Sep 5, 2026
47ff45c
fix(gates): re-pin the census window and classify #9754's young logs
Sep 5, 2026
e1cf7d5
fix(gates): narrow the global_guards import and pin utf-8 in the ratc…
Sep 5, 2026
71e6b75
fix(gates): pin utf-8 at the two flagged ratchet-test IO sites
Sep 5, 2026
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
93 changes: 54 additions & 39 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -135,45 +135,45 @@ strip = false
# UI crates must NOT strip — they export #[no_mangle] extern "C" symbols
[profile.release.package.perry-ui-macos]
strip = false
codegen-units = 16
codegen-units = 1

[profile.release.package.perry-ui-gtk4]
strip = false
codegen-units = 16
codegen-units = 1

[profile.release.package.perry-ui-windows]
strip = false
codegen-units = 16
codegen-units = 1

# WinUI scaffold (#4680): re-exports perry-ui-windows; the staticlib bundles
# its #[no_mangle] extern "C" symbols, so it must not be stripped either.
[profile.release.package.perry-ui-windows-winui]
strip = false
codegen-units = 16
codegen-units = 1

[profile.release.package.perry-ui-geisterhand]
strip = false
codegen-units = 16
codegen-units = 1

[profile.release.package.perry-ui-ios]
strip = false
codegen-units = 16
codegen-units = 1

[profile.release.package.perry-ui-visionos]
strip = false
codegen-units = 16
codegen-units = 1

[profile.release.package.perry-ui-tvos]
strip = false
codegen-units = 16
codegen-units = 1

[profile.release.package.perry-ui-android]
strip = false
codegen-units = 16
codegen-units = 1

[profile.release.package.perry-ui-watchos]
strip = false
codegen-units = 16
codegen-units = 1

[profile.release.package.perry-stdlib]
opt-level = "s" # Optimize for size in stdlib
Expand All @@ -190,7 +190,7 @@ opt-level = "s" # Optimize for size in stdlib
# units + no strip keeps the exported C API in the staticlib.
[profile.release.package.perry-ext-events]
strip = false
codegen-units = 16
codegen-units = 1

# Staticlib wrapper crates (#5422). perry-runtime / perry-stdlib are now
# rlib-only; these wrappers re-export their #[no_mangle] C API into
Expand All @@ -200,10 +200,10 @@ codegen-units = 16
# in the archive.
[profile.release.package.perry-runtime-static]
strip = false
codegen-units = 16
codegen-units = 1
[profile.release.package.perry-stdlib-static]
strip = false
codegen-units = 16
codegen-units = 1

# Issue #5928: well-known "shared tokio" wrapper crates (#507) are built in
# the SAME cargo invocation as perry-stdlib-static so cargo unifies their
Expand All @@ -223,17 +223,17 @@ codegen-units = 16
# strip-dedup mechanism assumes same-named codegen units are byte-identical
# and safe to drop duplicates from — an assumption this mismatch violated.
[profile.release.package.perry-ext-fastify]
codegen-units = 16
codegen-units = 1
[profile.release.package.perry-ext-http]
codegen-units = 16
codegen-units = 1
[profile.release.package.perry-ext-ioredis]
codegen-units = 16
codegen-units = 1
[profile.release.package.perry-ext-net]
codegen-units = 16
codegen-units = 1
[profile.release.package.perry-ext-undici]
codegen-units = 16
codegen-units = 1
[profile.release.package.perry-ext-ws]
codegen-units = 16
codegen-units = 1

# Fast developer profile (#5422). Optimized enough for realistic local runs but
# without the distribution-grade settings that dominate compile time, so the
Expand All @@ -243,7 +243,7 @@ codegen-units = 16
[profile.perry-dev]
inherits = "release"
lto = false
codegen-units = 16
codegen-units = 1
incremental = true
strip = false
opt-level = 1
Expand Down Expand Up @@ -278,61 +278,61 @@ opt-level = 3
strip = false
[profile.dist.package.perry-ui-macos]
strip = false
codegen-units = 16
codegen-units = 1
[profile.dist.package.perry-ui-gtk4]
strip = false
codegen-units = 16
codegen-units = 1
[profile.dist.package.perry-ui-windows]
strip = false
codegen-units = 16
codegen-units = 1
[profile.dist.package.perry-ui-windows-winui]
strip = false
codegen-units = 16
codegen-units = 1
[profile.dist.package.perry-ui-geisterhand]
strip = false
codegen-units = 16
codegen-units = 1
[profile.dist.package.perry-ui-ios]
strip = false
codegen-units = 16
codegen-units = 1
[profile.dist.package.perry-ui-visionos]
strip = false
codegen-units = 16
codegen-units = 1
[profile.dist.package.perry-ui-tvos]
strip = false
codegen-units = 16
codegen-units = 1
[profile.dist.package.perry-ui-android]
strip = false
codegen-units = 16
codegen-units = 1
[profile.dist.package.perry-ui-watchos]
strip = false
codegen-units = 16
codegen-units = 1
[profile.dist.package.perry-stdlib]
opt-level = "s"
[profile.dist.package.perry-ext-events]
strip = false
codegen-units = 16
codegen-units = 1
[profile.dist.package.perry-runtime-static]
strip = false
codegen-units = 16
codegen-units = 1
[profile.dist.package.perry-stdlib-static]
strip = false
codegen-units = 16
codegen-units = 1

# Issue #5928: mirrors the [profile.release.package.perry-ext-*] block above
# — see its comment for why matching `codegen-units` across every crate in
# the "shared tokio" (#507) cargo invocation is required.
[profile.dist.package.perry-ext-fastify]
codegen-units = 16
codegen-units = 1
[profile.dist.package.perry-ext-http]
codegen-units = 16
codegen-units = 1
[profile.dist.package.perry-ext-ioredis]
codegen-units = 16
codegen-units = 1
[profile.dist.package.perry-ext-net]
codegen-units = 16
codegen-units = 1
[profile.dist.package.perry-ext-undici]
codegen-units = 16
codegen-units = 1
[profile.dist.package.perry-ext-ws]
codegen-units = 16
codegen-units = 1

[workspace.package]
version = "0.5.1520"
Expand Down Expand Up @@ -532,3 +532,18 @@ perry-codegen-wasm = { path = "crates/perry-codegen-wasm" }
perry-ui-testkit = { path = "crates/perry-ui-testkit" }
perry-audio-miniaudio = { path = "crates/perry-audio-miniaudio" }
perry-updater = { path = "crates/perry-updater" }

# Release codegen with debug assertions ON, for the GC root-scanning guards
# that only exist under `cfg(debug_assertions)` — above all
# `gc::young_log::debug_assert_logged`, rule 2 of the young-entry-log design,
# which re-derives each table's minor-relevant set and panics on any key the
# log does not name. `[profile.release]` leaves debug-assertions off, so a
# release `cargo test` run does not enforce rule 1 at all, and the plain `dev`
# profile is too slow to run the GC suite comfortably.
#
# cargo test --profile gcaudit -p perry-runtime -- --test-threads=1
[profile.gcaudit]
inherits = "release"
debug-assertions = true
lto = false
codegen-units = 1
74 changes: 64 additions & 10 deletions benchmarks/gc_ratchet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,14 @@ sessions x 7 repeats (21 runs per probe), plus 5 traced runs per probe:
The GC accounting family is parsed from `PERRY_GC_DIAG=1` output in a separate,
untimed pass; enabling the trace was verified not to change `heap_used_bytes`,
so the traced pass observes the same collector the untimed pass measures. The
harness takes two traced runs on every invocation and fails if they disagree —
that is the harness proving, each time it runs, that the counters it is about to
gate on really are deterministic.
harness records two traced runs on every invocation. `check` fails if a gated
counter disagrees, even within its tolerance band. Documented probe overrides
apply here too; the measurement keeps both samples and runs every later probe.

Retention and GC accounting are semantic: they are a function of the allocation
sequence and collector policy, not of CPU speed, core count, or machine load.
That is why they can be gated on a shared CI runner and memory and time cannot.
Retention and GC accounting usually transfer across machine classes because
they describe allocation and collector work rather than CPU speed. They must
still demonstrate repeatability: block placement can change the collection
point and therefore the live cohort, as the #9790 investigation below shows.

## A probe may declare the collector it is a probe *of* (the large-Eden arm)

Expand Down Expand Up @@ -332,10 +333,63 @@ carrying a non-deterministic gating cell cannot be *pinned*. Before #7554 the
rule existed only in `tests/test_gc_ratchet.py`, which is why a bad pin could
be committed and only wedge CI afterwards.

The section is currently **empty**, which is the goal state and not an
oversight. Its one entry — `12_large_live_set.heap_used_bytes` — was deleted by
#7558, which removed the *cause* rather than the cell. That is rule 4 working
as designed.
The former `12_large_live_set.heap_used_bytes` entry was deleted by #7558,
which removed its cause. The two current entries exclude only
`07_array_grow_evacuate.copied_bytes` and `.freed_bytes`, with the following
evidence. Removing the placement dependency would allow deleting these entries.

### Array growth: placement changes the collection point (#9790)

Twenty-one executions of one unchanged binary produced two counter tuples.
Every stdout matched Node 26.5.1. Retained heap, arena capacity, minor/step
counts, copied/promoted object counts, and promoted bytes were identical.
The [receipt](evidence/9790-array-growth-pacing.json) records raw byte-counter
samples, stable metrics, and compiler/runtime/probe hashes.

Temporary logging in `move_young` cross-checked the counters against the actual
headers moved. Only the fifth minor's live cohort differed: an 8,208-byte array
with length 640, capacity 1,024, and seed 5,207 was reached through the ring's
remembered edge in one run. The other run instead copied a 144-byte array with
length 1, capacity 16, and seed 5,240 from the native stack. The difference is
exactly **8,064 bytes**, while both runs copy one object. Summing the other
copied headers gives the same result in both runs.

The freed-byte difference also balances against the actual from-space usage:

| Fifth minor | Higher copied bytes | Lower copied bytes |
|---|---:|---:|
| Eden bytes | 16,775,936 | 16,776,080 |
| Active survivor bytes | 517,248 | 517,248 |
| Copied bytes | 525,312 | 517,248 |
| Promoted bytes | 131,328 | 131,328 |
| Freed bytes | 16,636,544 | 16,644,752 |

In each column, freed = Eden + active survivor - copied - promoted; malloc
reclamation is zero. There is no unexplained accounting remainder.

Block-boundary logging located the pacing cause. `arena_cell_alloc` checks GC
pressure when its current block cannot satisfy an allocation. Promotion walks
address-keyed root tables, so equal total promoted bytes can fill individual
old blocks differently. In the higher-copy run, an old block overflowed on a
4,112-byte growth request after young occupancy had crossed the cap. In the
lower-copy run, that old-block rollover occurred earlier, below the cap; the
next nursery block overflow armed collection while starting the next array.
`js_array_grow` can fall back to old allocation when a growth cannot fit the
current nursery block, connecting this workload to that old-block geometry.
The earlier dirty-page statistics also vary with placement; they alone would
not have established the cause.

The two byte counters therefore describe real, placement-dependent work on
this workload. They remain measured and displayed, with their existing bands;
only their ability to fail the gate is excluded. The probe's correctness,
retention, cycle counts, copied/promoted object counts, and promoted bytes
remain gated, as do these byte counters on every other probe. Runtime pacing
and the probe's allocation sequence are unchanged.

The determinism check now runs in `check`, where the baseline's reviewed
overrides are available. An unlisted disagreement still fails even if its
median equals the baseline, and the full measurement artifact survives for
inspection. `assemble` still refuses to pin any nondeterministic gated cell.

### What that probe's non-determinism was, and where it went (#7558)

Expand Down
37 changes: 30 additions & 7 deletions benchmarks/gc_ratchet/baseline/gc-ratchet-v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,12 @@
"job red. Every entry carries evidence that is checked, not merely stored --",
"at least 21 runs (the same number every band above is justified by) and a",
"spread that is actually non-zero, so a cell cannot be excluded on a hunch.",
"The section is EMPTY, and that is the goal state. Its one entry --",
"12_large_live_set.heap_used_bytes, added by #7554 -- was deleted by #7558,",
"which removed the cause rather than the cell: explicit gc() no longer forces",
"the conservative native-stack scan, so that reading is bit-identical again",
"and gates again. An empty section is not a disarmed rule; the evidence",
"checks and the never-gate-nothing rule still fail any entry added back.",
"The former 12_large_live_set.heap_used_bytes exclusion was removed by #7558.",
"#9790 excludes only 07_array_grow_evacuate.copied_bytes and freed_bytes:",
"promotion order changes block packing and when nursery pressure is checked.",
"The receipt records 21 runs and an allocation-level accounting cross-check.",
"Both cells remain measured and reported. All other cells retain their bands.",
"Deleting the placement dependency means deleting these exclusions.",
"",
"#7559 -- A heap_used_bytes band used to NOT be a statement about how much",
"the collector retained. The reading is taken after the probe's own gc(),",
Expand Down Expand Up @@ -295,7 +295,30 @@
"rationale": "GATED HERE ONLY. Worst cross-session spread of medians-of-7 was 0.751% on an idle box (load 1.7-2.0); worst raw within-session spread was 5.3%, which the median-of-7 damps out. 10% is ~13x the cross-session figure and ~2x the worst raw spread, so it will not fire on scheduler jitter but will catch the tens-of-percent slowdown a whole-stack conservative scan would introduce. The 15 ms floor covers the fastest probe (126 ms)."
}
},
"probe_overrides": {}
"probe_overrides": {
"07_array_grow_evacuate": {
"copied_bytes": {
"gating": false,
"rationale": "NOT GATED ON THIS PROBE (#9790). Address-dependent promotion order changes old-block packing and the allocation boundary where nursery pressure is checked. The fifth minor observes a different live cohort: a completed 8,208-byte array or a new 144-byte array. Header-size sums and from-space reclamation account for the byte deltas; this is placement-dependent pacing, not an accounting discrepancy. Keep this cell visible; correctness, retention, cycle counts, object counts and promoted bytes remain gated. See evidence/9790-array-growth-pacing.json and the README investigation.",
"evidence": {
"observed_runs": 21,
"observed_spread": 8064,
"measured_on": "2026-09-05, macOS arm64, Perry 0.5.1520; 21 executions of one unchanged binary. Compiler/runtime hashes and raw counters: benchmarks/gc_ratchet/evidence/9790-array-growth-pacing.json.",
"issue": "https://github.com/PerryTS/perry/issues/9790"
}
},
"freed_bytes": {
"gating": false,
"rationale": "NOT GATED ON THIS PROBE (#9790). Address-dependent promotion order changes old-block packing and the allocation boundary where nursery pressure is checked. The fifth minor observes a different live cohort: a completed 8,208-byte array or a new 144-byte array. Header-size sums and from-space reclamation account for the byte deltas; this is placement-dependent pacing, not an accounting discrepancy. Keep this cell visible; correctness, retention, cycle counts, object counts and promoted bytes remain gated. See evidence/9790-array-growth-pacing.json and the README investigation.",
"evidence": {
"observed_runs": 21,
"observed_spread": 8208,
"measured_on": "2026-09-05, macOS arm64, Perry 0.5.1520; 21 executions of one unchanged binary. Compiler/runtime hashes and raw counters: benchmarks/gc_ratchet/evidence/9790-array-growth-pacing.json.",
"issue": "https://github.com/PerryTS/perry/issues/9790"
}
}
}
}
},
"notes": "Re-pinned for #8122-recover: the allocation census before minor #0 object-denominates the first nursery cap (first cycle fires earlier on small-object workloads), one descriptor lookup per traced object, untraced-promotion threshold 990 -> 980. Every GC-accounting fingerprint shifts; retention improves on 12_large_live_set (-75%) and 13_large_eden_survivors moves +85 KB because its cycle 0 now holds up an in-place promotion at 581 permille (main at cap 49 retains 651 KB the same way).",
"probes": {
Expand Down
Loading
Loading