diff --git a/benchmarks/catch_frames/README.md b/benchmarks/catch_frames/README.md new file mode 100644 index 0000000000..bf08755316 --- /dev/null +++ b/benchmarks/catch_frames/README.md @@ -0,0 +1,140 @@ +# Catch-frame setup + +Grouping the per-depth exception state and inlining its savepoint providers +reduces non-throwing setup by 121 instructions per runtime catch in this probe. +The existing C trampoline still owns `setjmp`. Generated user `try`/`catch` +calls `js_eh_try_push` in `perry-codegen/src/stmt/try_stmt.rs` and shares the +same snapshot capture, although its exception transport uses the system unwinder. + +## Design and correctness + +`exception::TryFrame` stores the jump buffer, handler kind and `CatchSavepoint` +in one fixed, heap-allocated slab. A depth indexes one allocation instead of +separate boxed arrays. The slab never grows, so jump-buffer addresses remain +stable. The 1,024-handler limit and try-depth accounting are unchanged. + +`exception/savepoints.rs` declares each snapshot field together with its +capture provider, restore provider and required real-throw witness. The macro +generates storage, capture, ordered restoration and the test. This removes +the independently maintained lists, including the incomplete legacy test +replay. The small capture providers inline so the optimizer can reuse hot +TLS accesses; live subsystem state stays with its current owner. This is an +incremental grouping change, without a journal or additional live TLS mirrors. + +Every existing restore remains, in its original order. Async-context cleanup +still runs first, using the unchanged handler depth. No callers lose their +catch frames. A future frame-elision optimization needs separate evidence +that its protected operation cannot throw. + +The generated tests establish nonempty state outside a trap, change it again +inside two nested traps, and throw through `js_throw` and the C trampoline. +They require both catches to recover exactly their enclosing state, and also +verify that normal completion does not replay cleanup. The shadow witness +changes both shadow frames and expression temp roots; the dyn-eval witness +changes both interpreter roots and call depth. + +`fault_restore.py` temporarily omits one registered restore at a time in the +production path. All 13 omissions fail their corresponding test at the +enclosing-state assertion; results are in `evidence/fault-results.json`. +Each mutant runs in a separate process. The expected assertion aborts across +the C callback boundary, so its exit is SIGABRT. An unrelated crash or an empty +test filter does not count as detection. The script restores and rebuilds the +original source in `finally`, then requires the exception suite to pass. + +## Instruction measurements, 2026-09-14 + +Baseline: main `eb13fa188d` (0.5.1564), which already includes the #10215 +large-array corruption fix. Host: perrymaster, AMD Ryzen 7 7700X, Linux +6.17.0-23-generic, perf 6.17.13, Rust nightly 2026-08-20 +(`1.100.0-nightly f7d782a3b`), LLVM 22. Node oracle: 26.5.1. + +Before changing runtime code, the promise/async workload pushed 100,007 frames. +Its initial instruction profile attributed 96 of 4,221 samples (2.27%) to +capture. A later independent baseline sample attributed 29 of 1,098 (2.64%). +The JSON allocation workload pushed only six frames, making it a useful +negative control. The hoisted regex test pushed 3,500,011 frames. Uprobe counts +were collected separately, with the probe removed before performance runs. + +| Workload | Baseline instructions | Candidate instructions | Change | +|---|---:|---:|---:| +| Promise/async loop, 100k | 1,098,667,245 | 1,077,465,852 | -1.93% | +| JSON parse/stringify loop, 100k | 3,182,841,469 | 3,171,165,684 | -0.37% | +| Hoisted regex `test`, 1M | 13,816,754,889 | 13,266,297,479 | -3.98% | +| Regex `exec`, 1M | 31,217,404,185 | 30,183,325,430 | -3.31% | +| Runtime catch microprobe, 1M | 472,151,423 | 351,152,420 | -25.63% | +| Generated-handler setup microprobe, 1M | 366,173,508 | 244,175,238 | -33.32% | +| Plain microprobe control, 1M | 27,068,237 | 27,068,989 | +0.00% | + +Subtracting each build's plain loop gives **445.08 → 324.08 instructions per +runtime catch (-27.19%)**, and **339.11 → 217.11 per generated-handler setup +(-35.98%)**. These include frame entry and exit; they do not measure throwing. +The Rust example has its own Cargo link graph, so its absolute per-frame cost +should not be substituted into compiled-program profiles. + +All four TypeScript outputs match Node. The regex probes are copied from +`/root/claude-regex/repro/tonly/{hoist,exec1}.ts`. No cc application claim is +made. The small JSON delta cannot be attributed to its six catch frames: +provider inlining can also change other call sites and compiler output. + +`evidence/instructions.json` records totals, sampled attribution, frame counts +and archive/compiler hashes. Raw stat outputs and gzip-compressed folded +stacks are alongside it. Symbol attribution is approximate under LTO and +sampling. The verdict uses instruction totals. All retained A/B folded stacks +use `perf script --no-inline` consistently. Raw perf data and build logs are +retained under `/root/js-throw-evidence` on perrymaster. + +Integration exposed a Thin LTO identity hazard: separately linked archives +could give `global_this_builtin_noop_thunk` different addresses, breaking +builtin constructor recognition. The final patch gives this address sentinel +one external symbol and a distinct non-inlined body. The unguarded optimized +build fails the EventTarget subclass fixture; changing only its constructor +pointer to the comparison target in GDB makes that fixture pass. The guarded +build passes that fixture and four related constructor fixtures. The final +measurements above and the full parity sweep both use a fresh, frozen build +including this guard. See `evidence/constructor-identity.md` for the witness. + +## Reproduction + +Use isolated baseline and candidate worktrees, each with its own target. +Keep the package selection and build settings identical on both sides: + +```sh +export LLVM_SYS_221_PREFIX=/usr/lib/llvm-22 +export CARGO_BUILD_JOBS=4 CARGO_INCREMENTAL=0 +export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16 +export CARGO_PROFILE_RELEASE_DEBUG=1 CARGO_PROFILE_RELEASE_STRIP=none +cargo build --release -p perry -p perry-runtime-static -p perry-stdlib-static +cargo build --release -p perry-runtime --example catch_frames +export PERRY_RUNTIME_DIR="$PWD/target/release" +python3 benchmarks/catch_frames/measure.py \ + --perry "$PWD/target/release/perry" \ + --micro "$PWD/target/release/examples/catch_frames" --output /tmp/catch-measure +python3 benchmarks/catch_frames/count_frames.py \ + /tmp/catch-measure/promises /tmp/catch-measure/promises.frames +``` + +Put pinned Node 26.5.1 on PATH. Do not override the repository's unwind-table +and x86 frame-pointer flags. Both measurements use thin LTO and the same +16-codegen-unit override; these are not claims about a separate dist build. +Copy the benchmark sources and example into the baseline worktree without +the runtime patch. Freeze the three compiler/archive outputs before building +any extra package set for other validation. + +The measurement script uses `perf stat -r 3 -e instructions:u` and a separate +`perf record -e instructions:u --call-graph dwarf` run. It retains oracle +outputs and folded stacks. Run fault injection only in a disposable worktree, +with no other builds or source copying in progress: + +```sh +python3 benchmarks/catch_frames/fault_restore.py --output /tmp/catch-faults +``` + +## Validation + +The local replay and its baseline comparisons are recorded in +`evidence/validation.md`. GC ratchet measurements use seven repetitions of +each of 14 probes. The candidate passes against a fresh Linux baseline from +the same main commit, with all 126 heap/collector medians identical. This +does not replace the repository's pinned artifact with a new baseline or +claim equivalence to its different-platform capture. GC wall/RSS observations +are retained by the harness but are not performance evidence for this change. diff --git a/benchmarks/catch_frames/count_frames.py b/benchmarks/catch_frames/count_frames.py new file mode 100644 index 0000000000..4ad2fb6fac --- /dev/null +++ b/benchmarks/catch_frames/count_frames.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +"""Count all try-frame pushes with a temporary uprobe, outside perf A/B runs.""" +import argparse +import json +import os +from pathlib import Path +import subprocess + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("binary", type=Path) + parser.add_argument("output", type=Path) + args = parser.parse_args() + symbols = subprocess.check_output(["nm", "--defined-only", str(args.binary)], text=True) + push = [line.split()[-1] for line in symbols.splitlines() if "try_push_with_kind" in line] + assert len(push) == 1, push + event = f"perry_catch_{os.getpid()}:push" + # Use the DWARF function name: an LTO-generated `.llvm.` suffix in + # the linkage name is parsed as probe syntax by perf. + subprocess.run(["perf", "probe", "-x", str(args.binary), "--add", f"{event}=try_push_with_kind"], stdout=subprocess.DEVNULL, check=True) + try: + with args.output.with_suffix(".out").open("w") as stream: + subprocess.run(["perf", "stat", "-x", ";", "-e", event, "-o", str(args.output), "--", str(args.binary)], stdout=stream, check=True) + finally: + subprocess.run(["perf", "probe", "-q", "--del", event], check=True) + count = next(int(line.split(";")[0]) for line in args.output.read_text().splitlines() if event in line) + print(json.dumps({"binary": str(args.binary), "frame_pushes": count})) + + +if __name__ == "__main__": + main() diff --git a/benchmarks/catch_frames/evidence/additional-baseline-checks.json b/benchmarks/catch_frames/evidence/additional-baseline-checks.json new file mode 100644 index 0000000000..6a09bd6334 --- /dev/null +++ b/benchmarks/catch_frames/evidence/additional-baseline-checks.json @@ -0,0 +1,20 @@ +[ + { + "test": "test_issue_4826_array_headers", + "baseline": "node_fail", + "candidate": "node_fail", + "detail": "Node oracle times out: server fixture never closes" + }, + { + "test": "test_issue_4975_http_agent_keep_alive_connection_options", + "baseline": "crash", + "candidate": "crash", + "detail": "Perry prints true and 1250, then exceeds the 10-second shutdown deadline. Existing Linux allowlist entry cites #8841, which was closed when checked on 2026-09-14." + }, + { + "test": "test_parity_stream_web", + "baseline": "crash", + "candidate": "crash", + "detail": "Both print the Web Streams constructor error and exceed the 10-second deadline. Existing module-inventory entry cites closed roadmap #793." + } +] diff --git a/benchmarks/catch_frames/evidence/api-docs-comparison.json b/benchmarks/catch_frames/evidence/api-docs-comparison.json new file mode 100644 index 0000000000..d3766b16dc --- /dev/null +++ b/benchmarks/catch_frames/evidence/api-docs-comparison.json @@ -0,0 +1,12 @@ +[ + { + "path": "docs/src/api/reference.md", + "baseline_generated_equals_candidate_generated": true, + "baseline_generated_equals_committed": false + }, + { + "path": "docs/api/perry.d.ts", + "baseline_generated_equals_candidate_generated": true, + "baseline_generated_equals_committed": false + } +] diff --git a/benchmarks/catch_frames/evidence/baseline-gc-artifact.json.gz b/benchmarks/catch_frames/evidence/baseline-gc-artifact.json.gz new file mode 100644 index 0000000000..c2a1343ee5 Binary files /dev/null and b/benchmarks/catch_frames/evidence/baseline-gc-artifact.json.gz differ diff --git a/benchmarks/catch_frames/evidence/baseline-gc-ratchet.json.gz b/benchmarks/catch_frames/evidence/baseline-gc-ratchet.json.gz new file mode 100644 index 0000000000..fe557844e8 Binary files /dev/null and b/benchmarks/catch_frames/evidence/baseline-gc-ratchet.json.gz differ diff --git a/benchmarks/catch_frames/evidence/baseline-public-freshness.log b/benchmarks/catch_frames/evidence/baseline-public-freshness.log new file mode 100644 index 0000000000..83c413f461 --- /dev/null +++ b/benchmarks/catch_frames/evidence/baseline-public-freshness.log @@ -0,0 +1 @@ +public baseline error: public artifact benchmark inputs changed; regenerate it with ./benchmarks/run_public_baseline.sh diff --git a/benchmarks/catch_frames/evidence/baseline-rechecks.json.gz b/benchmarks/catch_frames/evidence/baseline-rechecks.json.gz new file mode 100644 index 0000000000..71777e09a4 Binary files /dev/null and b/benchmarks/catch_frames/evidence/baseline-rechecks.json.gz differ diff --git a/benchmarks/catch_frames/evidence/baseline/catch.folded.gz b/benchmarks/catch_frames/evidence/baseline/catch.folded.gz new file mode 100644 index 0000000000..e5cf0b2ee4 Binary files /dev/null and b/benchmarks/catch_frames/evidence/baseline/catch.folded.gz differ diff --git a/benchmarks/catch_frames/evidence/baseline/catch.stat b/benchmarks/catch_frames/evidence/baseline/catch.stat new file mode 100644 index 0000000000..64e02e697d --- /dev/null +++ b/benchmarks/catch_frames/evidence/baseline/catch.stat @@ -0,0 +1,3 @@ +# started on Mon Sep 14 04:34:13 2026 + +472151423;;instructions:u;0.00%;45711787;100.00;; diff --git a/benchmarks/catch_frames/evidence/baseline/exec1.expected b/benchmarks/catch_frames/evidence/baseline/exec1.expected new file mode 100644 index 0000000000..7a908ccedf --- /dev/null +++ b/benchmarks/catch_frames/evidence/baseline/exec1.expected @@ -0,0 +1 @@ +5888890 diff --git a/benchmarks/catch_frames/evidence/baseline/exec1.folded.gz b/benchmarks/catch_frames/evidence/baseline/exec1.folded.gz new file mode 100644 index 0000000000..f36cfa49e1 Binary files /dev/null and b/benchmarks/catch_frames/evidence/baseline/exec1.folded.gz differ diff --git a/benchmarks/catch_frames/evidence/baseline/exec1.stat b/benchmarks/catch_frames/evidence/baseline/exec1.stat new file mode 100644 index 0000000000..d96d7a861e --- /dev/null +++ b/benchmarks/catch_frames/evidence/baseline/exec1.stat @@ -0,0 +1,3 @@ +# started on Mon Sep 14 04:32:12 2026 + +31217404185;;instructions:u;0.00%;2411220179;100.00;; diff --git a/benchmarks/catch_frames/evidence/baseline/hoist.expected b/benchmarks/catch_frames/evidence/baseline/hoist.expected new file mode 100644 index 0000000000..354b2529b2 --- /dev/null +++ b/benchmarks/catch_frames/evidence/baseline/hoist.expected @@ -0,0 +1 @@ +500000 diff --git a/benchmarks/catch_frames/evidence/baseline/hoist.folded.gz b/benchmarks/catch_frames/evidence/baseline/hoist.folded.gz new file mode 100644 index 0000000000..cae1c4e7f1 Binary files /dev/null and b/benchmarks/catch_frames/evidence/baseline/hoist.folded.gz differ diff --git a/benchmarks/catch_frames/evidence/baseline/hoist.frames b/benchmarks/catch_frames/evidence/baseline/hoist.frames new file mode 100644 index 0000000000..ef30c01d3f --- /dev/null +++ b/benchmarks/catch_frames/evidence/baseline/hoist.frames @@ -0,0 +1,3 @@ +# started on Mon Sep 14 04:35:34 2026 + +3500011;;perry_catch_201827:push;3490348955;100.00;; diff --git a/benchmarks/catch_frames/evidence/baseline/hoist.stat b/benchmarks/catch_frames/evidence/baseline/hoist.stat new file mode 100644 index 0000000000..d9d6adbd11 --- /dev/null +++ b/benchmarks/catch_frames/evidence/baseline/hoist.stat @@ -0,0 +1,3 @@ +# started on Mon Sep 14 04:30:55 2026 + +13816754889;;instructions:u;0.00%;862134599;100.00;; diff --git a/benchmarks/catch_frames/evidence/baseline/json.expected b/benchmarks/catch_frames/evidence/baseline/json.expected new file mode 100644 index 0000000000..de404fe14d --- /dev/null +++ b/benchmarks/catch_frames/evidence/baseline/json.expected @@ -0,0 +1 @@ +5008938890 diff --git a/benchmarks/catch_frames/evidence/baseline/json.folded.gz b/benchmarks/catch_frames/evidence/baseline/json.folded.gz new file mode 100644 index 0000000000..44ee1c4d29 Binary files /dev/null and b/benchmarks/catch_frames/evidence/baseline/json.folded.gz differ diff --git a/benchmarks/catch_frames/evidence/baseline/json.frames b/benchmarks/catch_frames/evidence/baseline/json.frames new file mode 100644 index 0000000000..93a971d359 --- /dev/null +++ b/benchmarks/catch_frames/evidence/baseline/json.frames @@ -0,0 +1,3 @@ +# started on Mon Sep 14 04:32:53 2026 + +6;;perry_catch_192702:push;228354174;100.00;; diff --git a/benchmarks/catch_frames/evidence/baseline/json.stat b/benchmarks/catch_frames/evidence/baseline/json.stat new file mode 100644 index 0000000000..154d76b12a --- /dev/null +++ b/benchmarks/catch_frames/evidence/baseline/json.stat @@ -0,0 +1,3 @@ +# started on Mon Sep 14 04:29:53 2026 + +3182841469;;instructions:u;0.00%;190945873;100.00;; diff --git a/benchmarks/catch_frames/evidence/baseline/plain.folded.gz b/benchmarks/catch_frames/evidence/baseline/plain.folded.gz new file mode 100644 index 0000000000..03cf1b0556 Binary files /dev/null and b/benchmarks/catch_frames/evidence/baseline/plain.folded.gz differ diff --git a/benchmarks/catch_frames/evidence/baseline/plain.stat b/benchmarks/catch_frames/evidence/baseline/plain.stat new file mode 100644 index 0000000000..31f29bfdce --- /dev/null +++ b/benchmarks/catch_frames/evidence/baseline/plain.stat @@ -0,0 +1,3 @@ +# started on Mon Sep 14 04:34:03 2026 + +27068237;;instructions:u;0.00%;1910641;100.00;; diff --git a/benchmarks/catch_frames/evidence/baseline/promises.expected b/benchmarks/catch_frames/evidence/baseline/promises.expected new file mode 100644 index 0000000000..90ee71a089 --- /dev/null +++ b/benchmarks/catch_frames/evidence/baseline/promises.expected @@ -0,0 +1 @@ +5000050000 diff --git a/benchmarks/catch_frames/evidence/baseline/promises.folded.gz b/benchmarks/catch_frames/evidence/baseline/promises.folded.gz new file mode 100644 index 0000000000..dec63e3cf4 Binary files /dev/null and b/benchmarks/catch_frames/evidence/baseline/promises.folded.gz differ diff --git a/benchmarks/catch_frames/evidence/baseline/promises.frames b/benchmarks/catch_frames/evidence/baseline/promises.frames new file mode 100644 index 0000000000..771d12c385 --- /dev/null +++ b/benchmarks/catch_frames/evidence/baseline/promises.frames @@ -0,0 +1,3 @@ +# started on Mon Sep 14 04:35:33 2026 + +100007;;perry_catch_201756:push;150141188;100.00;; diff --git a/benchmarks/catch_frames/evidence/baseline/promises.stat b/benchmarks/catch_frames/evidence/baseline/promises.stat new file mode 100644 index 0000000000..9116c73379 --- /dev/null +++ b/benchmarks/catch_frames/evidence/baseline/promises.stat @@ -0,0 +1,3 @@ +# started on Mon Sep 14 04:29:25 2026 + +1098667245;;instructions:u;0.00%;106550647;100.00;; diff --git a/benchmarks/catch_frames/evidence/baseline/unwind.folded.gz b/benchmarks/catch_frames/evidence/baseline/unwind.folded.gz new file mode 100644 index 0000000000..dda52eee60 Binary files /dev/null and b/benchmarks/catch_frames/evidence/baseline/unwind.folded.gz differ diff --git a/benchmarks/catch_frames/evidence/baseline/unwind.stat b/benchmarks/catch_frames/evidence/baseline/unwind.stat new file mode 100644 index 0000000000..de638f1e9b --- /dev/null +++ b/benchmarks/catch_frames/evidence/baseline/unwind.stat @@ -0,0 +1,3 @@ +# started on Mon Sep 14 04:34:32 2026 + +366173508;;instructions:u;0.00%;25498367;100.00;; diff --git a/benchmarks/catch_frames/evidence/candidate-artifacts.json b/benchmarks/catch_frames/evidence/candidate-artifacts.json new file mode 100644 index 0000000000..80c7a3f1bd --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate-artifacts.json @@ -0,0 +1,5 @@ +{ + "perry": "08a1aff537958e855fbdf5b9f25ccd22deb3cc3fc1a97c2f3063ed98dd2cbd9d", + "libperry_runtime.a": "2c0b05fbad66bb569241ccf742831504c9ef8c4819a31845c3d92c70d0efaa72", + "libperry_stdlib.a": "86ffd21fcd187c748661fd85015d6824998a43f70610b98b98cb7e8fd86a0206" +} diff --git a/benchmarks/catch_frames/evidence/candidate/catch.folded.gz b/benchmarks/catch_frames/evidence/candidate/catch.folded.gz new file mode 100644 index 0000000000..bce3d191c9 Binary files /dev/null and b/benchmarks/catch_frames/evidence/candidate/catch.folded.gz differ diff --git a/benchmarks/catch_frames/evidence/candidate/catch.profile.out b/benchmarks/catch_frames/evidence/candidate/catch.profile.out new file mode 100644 index 0000000000..ad629e2d59 --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/catch.profile.out @@ -0,0 +1 @@ +499999500000 diff --git a/benchmarks/catch_frames/evidence/candidate/catch.stat b/benchmarks/catch_frames/evidence/candidate/catch.stat new file mode 100644 index 0000000000..798921350e --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/catch.stat @@ -0,0 +1,3 @@ +# started on Mon Sep 14 05:49:25 2026 + +351152420;;instructions:u;0.00%;27154989;100.00;; diff --git a/benchmarks/catch_frames/evidence/candidate/catch.stat.out b/benchmarks/catch_frames/evidence/candidate/catch.stat.out new file mode 100644 index 0000000000..f8922fcfd0 --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/catch.stat.out @@ -0,0 +1,3 @@ +499999500000 +499999500000 +499999500000 diff --git a/benchmarks/catch_frames/evidence/candidate/exec1.expected b/benchmarks/catch_frames/evidence/candidate/exec1.expected new file mode 100644 index 0000000000..7a908ccedf --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/exec1.expected @@ -0,0 +1 @@ +5888890 diff --git a/benchmarks/catch_frames/evidence/candidate/exec1.folded.gz b/benchmarks/catch_frames/evidence/candidate/exec1.folded.gz new file mode 100644 index 0000000000..c36aa4ca3a Binary files /dev/null and b/benchmarks/catch_frames/evidence/candidate/exec1.folded.gz differ diff --git a/benchmarks/catch_frames/evidence/candidate/exec1.out b/benchmarks/catch_frames/evidence/candidate/exec1.out new file mode 100644 index 0000000000..7a908ccedf --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/exec1.out @@ -0,0 +1 @@ +5888890 diff --git a/benchmarks/catch_frames/evidence/candidate/exec1.profile.out b/benchmarks/catch_frames/evidence/candidate/exec1.profile.out new file mode 100644 index 0000000000..7a908ccedf --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/exec1.profile.out @@ -0,0 +1 @@ +5888890 diff --git a/benchmarks/catch_frames/evidence/candidate/exec1.stat b/benchmarks/catch_frames/evidence/candidate/exec1.stat new file mode 100644 index 0000000000..7b0f84974f --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/exec1.stat @@ -0,0 +1,3 @@ +# started on Mon Sep 14 05:47:42 2026 + +30183325430;;instructions:u;0.00%;3248216039;100.00;; diff --git a/benchmarks/catch_frames/evidence/candidate/exec1.stat.out b/benchmarks/catch_frames/evidence/candidate/exec1.stat.out new file mode 100644 index 0000000000..78ac85679b --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/exec1.stat.out @@ -0,0 +1,3 @@ +5888890 +5888890 +5888890 diff --git a/benchmarks/catch_frames/evidence/candidate/hoist.expected b/benchmarks/catch_frames/evidence/candidate/hoist.expected new file mode 100644 index 0000000000..354b2529b2 --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/hoist.expected @@ -0,0 +1 @@ +500000 diff --git a/benchmarks/catch_frames/evidence/candidate/hoist.folded.gz b/benchmarks/catch_frames/evidence/candidate/hoist.folded.gz new file mode 100644 index 0000000000..8f64777ea7 Binary files /dev/null and b/benchmarks/catch_frames/evidence/candidate/hoist.folded.gz differ diff --git a/benchmarks/catch_frames/evidence/candidate/hoist.out b/benchmarks/catch_frames/evidence/candidate/hoist.out new file mode 100644 index 0000000000..354b2529b2 --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/hoist.out @@ -0,0 +1 @@ +500000 diff --git a/benchmarks/catch_frames/evidence/candidate/hoist.profile.out b/benchmarks/catch_frames/evidence/candidate/hoist.profile.out new file mode 100644 index 0000000000..354b2529b2 --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/hoist.profile.out @@ -0,0 +1 @@ +500000 diff --git a/benchmarks/catch_frames/evidence/candidate/hoist.stat b/benchmarks/catch_frames/evidence/candidate/hoist.stat new file mode 100644 index 0000000000..d1a7784571 --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/hoist.stat @@ -0,0 +1,3 @@ +# started on Mon Sep 14 05:46:49 2026 + +13266297479;;instructions:u;0.00%;830465765;100.00;; diff --git a/benchmarks/catch_frames/evidence/candidate/hoist.stat.out b/benchmarks/catch_frames/evidence/candidate/hoist.stat.out new file mode 100644 index 0000000000..010112db02 --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/hoist.stat.out @@ -0,0 +1,3 @@ +500000 +500000 +500000 diff --git a/benchmarks/catch_frames/evidence/candidate/json.expected b/benchmarks/catch_frames/evidence/candidate/json.expected new file mode 100644 index 0000000000..de404fe14d --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/json.expected @@ -0,0 +1 @@ +5008938890 diff --git a/benchmarks/catch_frames/evidence/candidate/json.folded.gz b/benchmarks/catch_frames/evidence/candidate/json.folded.gz new file mode 100644 index 0000000000..fb9e563eef Binary files /dev/null and b/benchmarks/catch_frames/evidence/candidate/json.folded.gz differ diff --git a/benchmarks/catch_frames/evidence/candidate/json.out b/benchmarks/catch_frames/evidence/candidate/json.out new file mode 100644 index 0000000000..de404fe14d --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/json.out @@ -0,0 +1 @@ +5008938890 diff --git a/benchmarks/catch_frames/evidence/candidate/json.profile.out b/benchmarks/catch_frames/evidence/candidate/json.profile.out new file mode 100644 index 0000000000..de404fe14d --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/json.profile.out @@ -0,0 +1 @@ +5008938890 diff --git a/benchmarks/catch_frames/evidence/candidate/json.stat b/benchmarks/catch_frames/evidence/candidate/json.stat new file mode 100644 index 0000000000..4575ca0cd5 --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/json.stat @@ -0,0 +1,3 @@ +# started on Mon Sep 14 05:46:19 2026 + +3171165684;;instructions:u;0.00%;196440960;100.00;; diff --git a/benchmarks/catch_frames/evidence/candidate/json.stat.out b/benchmarks/catch_frames/evidence/candidate/json.stat.out new file mode 100644 index 0000000000..8919afc71c --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/json.stat.out @@ -0,0 +1,3 @@ +5008938890 +5008938890 +5008938890 diff --git a/benchmarks/catch_frames/evidence/candidate/plain.folded.gz b/benchmarks/catch_frames/evidence/candidate/plain.folded.gz new file mode 100644 index 0000000000..b0bdb6e7fe Binary files /dev/null and b/benchmarks/catch_frames/evidence/candidate/plain.folded.gz differ diff --git a/benchmarks/catch_frames/evidence/candidate/plain.profile.out b/benchmarks/catch_frames/evidence/candidate/plain.profile.out new file mode 100644 index 0000000000..ad629e2d59 --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/plain.profile.out @@ -0,0 +1 @@ +499999500000 diff --git a/benchmarks/catch_frames/evidence/candidate/plain.stat b/benchmarks/catch_frames/evidence/candidate/plain.stat new file mode 100644 index 0000000000..93d3ea43c4 --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/plain.stat @@ -0,0 +1,3 @@ +# started on Mon Sep 14 05:49:18 2026 + +27068989;;instructions:u;0.00%;3532580;100.00;; diff --git a/benchmarks/catch_frames/evidence/candidate/plain.stat.out b/benchmarks/catch_frames/evidence/candidate/plain.stat.out new file mode 100644 index 0000000000..f8922fcfd0 --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/plain.stat.out @@ -0,0 +1,3 @@ +499999500000 +499999500000 +499999500000 diff --git a/benchmarks/catch_frames/evidence/candidate/promises.expected b/benchmarks/catch_frames/evidence/candidate/promises.expected new file mode 100644 index 0000000000..90ee71a089 --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/promises.expected @@ -0,0 +1 @@ +5000050000 diff --git a/benchmarks/catch_frames/evidence/candidate/promises.folded.gz b/benchmarks/catch_frames/evidence/candidate/promises.folded.gz new file mode 100644 index 0000000000..0ce633ab1a Binary files /dev/null and b/benchmarks/catch_frames/evidence/candidate/promises.folded.gz differ diff --git a/benchmarks/catch_frames/evidence/candidate/promises.out b/benchmarks/catch_frames/evidence/candidate/promises.out new file mode 100644 index 0000000000..90ee71a089 --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/promises.out @@ -0,0 +1 @@ +5000050000 diff --git a/benchmarks/catch_frames/evidence/candidate/promises.profile.out b/benchmarks/catch_frames/evidence/candidate/promises.profile.out new file mode 100644 index 0000000000..90ee71a089 --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/promises.profile.out @@ -0,0 +1 @@ +5000050000 diff --git a/benchmarks/catch_frames/evidence/candidate/promises.stat b/benchmarks/catch_frames/evidence/candidate/promises.stat new file mode 100644 index 0000000000..4eff7d0354 --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/promises.stat @@ -0,0 +1,3 @@ +# started on Mon Sep 14 05:46:01 2026 + +1077465852;;instructions:u;0.00%;67069381;100.00;; diff --git a/benchmarks/catch_frames/evidence/candidate/promises.stat.out b/benchmarks/catch_frames/evidence/candidate/promises.stat.out new file mode 100644 index 0000000000..0ef867f027 --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/promises.stat.out @@ -0,0 +1,3 @@ +5000050000 +5000050000 +5000050000 diff --git a/benchmarks/catch_frames/evidence/candidate/results.json b/benchmarks/catch_frames/evidence/candidate/results.json new file mode 100644 index 0000000000..5b6c2704b1 --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/results.json @@ -0,0 +1,51 @@ +{ + "promises": { + "instructions": 1077465852.0, + "samples": 1077, + "capture_samples": 4, + "transport_leaf_samples": 0, + "capture_percent": 0.3714020427112349 + }, + "json": { + "instructions": 3171165684.0, + "samples": 3168, + "capture_samples": 0, + "transport_leaf_samples": 0, + "capture_percent": 0.0 + }, + "hoist": { + "instructions": 13266297479.0, + "samples": 13263, + "capture_samples": 447, + "transport_leaf_samples": 483, + "capture_percent": 3.3702782175978285 + }, + "exec1": { + "instructions": 30183325430.0, + "samples": 30182, + "capture_samples": 999, + "transport_leaf_samples": 1080, + "capture_percent": 3.309919819760122 + }, + "plain": { + "instructions": 27068989.0, + "samples": 26, + "capture_samples": 0, + "transport_leaf_samples": 0, + "capture_percent": 0.0 + }, + "catch": { + "instructions": 351152420.0, + "samples": 351, + "capture_samples": 6, + "transport_leaf_samples": 43, + "capture_percent": 1.7094017094017093 + }, + "unwind": { + "instructions": 244175238.0, + "samples": 244, + "capture_samples": 129, + "transport_leaf_samples": 0, + "capture_percent": 52.868852459016395 + } +} diff --git a/benchmarks/catch_frames/evidence/candidate/unwind.folded.gz b/benchmarks/catch_frames/evidence/candidate/unwind.folded.gz new file mode 100644 index 0000000000..6d863c3a45 Binary files /dev/null and b/benchmarks/catch_frames/evidence/candidate/unwind.folded.gz differ diff --git a/benchmarks/catch_frames/evidence/candidate/unwind.profile.out b/benchmarks/catch_frames/evidence/candidate/unwind.profile.out new file mode 100644 index 0000000000..ad629e2d59 --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/unwind.profile.out @@ -0,0 +1 @@ +499999500000 diff --git a/benchmarks/catch_frames/evidence/candidate/unwind.stat b/benchmarks/catch_frames/evidence/candidate/unwind.stat new file mode 100644 index 0000000000..e84c34d0bd --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/unwind.stat @@ -0,0 +1,3 @@ +# started on Mon Sep 14 05:49:46 2026 + +244175238;;instructions:u;0.00%;19181172;100.00;; diff --git a/benchmarks/catch_frames/evidence/candidate/unwind.stat.out b/benchmarks/catch_frames/evidence/candidate/unwind.stat.out new file mode 100644 index 0000000000..f8922fcfd0 --- /dev/null +++ b/benchmarks/catch_frames/evidence/candidate/unwind.stat.out @@ -0,0 +1,3 @@ +499999500000 +499999500000 +499999500000 diff --git a/benchmarks/catch_frames/evidence/clippy-comparison.json b/benchmarks/catch_frames/evidence/clippy-comparison.json new file mode 100644 index 0000000000..35fd8a6655 --- /dev/null +++ b/benchmarks/catch_frames/evidence/clippy-comparison.json @@ -0,0 +1,5 @@ +{ + "normalization": "level, lint code, message and primary filename; source line shifts ignored", + "added": [], + "removed": [] +} diff --git a/benchmarks/catch_frames/evidence/constructor-identity.md b/benchmarks/catch_frames/evidence/constructor-identity.md new file mode 100644 index 0000000000..3a3440294b --- /dev/null +++ b/benchmarks/catch_frames/evidence/constructor-identity.md @@ -0,0 +1,56 @@ +# Constructor marker regression witness + +The catch-frame optimization changes Thin LTO layout. The first optimized +build broke `test_gap_6301_event_target_subclass`: the plain EventTarget worked, +but a subclass had no `addEventListener` or `dispatchEvent`. + +Both baseline and unguarded candidate were built from main `eb13fa188d` with +the same three-package release graph and the instruction-measurement settings +in the parent README. The only subsequent production change for the guarded +build is the seven-line marker guard in `object/global_this/fetch_globals.rs`. + +`nm -C` found three local copies of `global_this_builtin_noop_thunk` in the +unguarded debug fixture. At `js_register_class_parent_dynamic`, GDB observed +that the parent closure used the thunk at image offset `0x6e2b00`. The +constructor classifier compared with image offset `0x41de30`, which had been +merged with `collected_pipeline_error_noop`. Those unequal addresses explain +the missed builtin-constructor classification. + +The two checked-in GDB transcripts use the same binary. `parent.gdb.log` +records the failure and exit 1. `parent-identity.gdb.log` records changing only +the closure function pointer to the classifier's comparison address: the +whole fixture then exits 0 with the expected subclass behavior. No exception +savepoint or transport state is changed by that debugger intervention. + +The guard gives the marker one `#[no_mangle]` symbol, `#[inline(never)]`, and a +body containing `black_box` of its own address. The guarded linked fixture has +one global `T global_this_builtin_noop_thunk`. It passes these existing compiled +regressions, with byte-identical Node/Perry stdout: + +- `test_gap_6301_event_target_subclass` +- `test_gap_6336_class_expr_builtin_parent` +- `test_gap_builtin_alias_construct_7524` +- `test_gap_dynamic_builtin_construct_dispatch` +- `test_gap_node_util_3098_3099_3334` + +The unguarded optimized build fails all five. Clean baseline passes four; +`test_gap_6336_class_expr_builtin_parent` also fails on baseline. This makes +the unguarded build a real guard-omission witness, rather than an assertion +about the guard's spelling. Full baseline and guarded recheck reports are in +`constructor-identity/rechecks.json.gz`; the guarded full parity replay also +includes these fixtures. LTO is necessary to exercise this failure mode; +default Rust unit tests alone do not prove marker identity across archives. + +To repeat the omission: remove only the added guard lines, rebuild the three +packages with the README's settings in an isolated target, freeze that build, +and run `run_parity_tests.sh --filter test_gap_6301_event_target_subclass` with +`PERRY_SKIP_BUILD=1`, `PERRY_BIN` and `PERRY_RUNTIME_DIR` pointing to it, and +`PERRY_WORKSPACE_ROOT` pointing to the matching source. Restore the guard, +rebuild with the same graph, and require the fixture to pass. This witness was +observed with the pinned toolchain; another LTO version may partition code +differently. + +The original binaries, compile logs and unabridged stdout are retained under +`/root/js-throw-evidence/eventtarget-triage` on perrymaster. The unguarded +compiler/archive sets are in the verified `pre-marker-artifacts.tar.gz` beside +that directory. Final performance and integration artifacts include the guard. diff --git a/benchmarks/catch_frames/evidence/constructor-identity/parent-identity.gdb b/benchmarks/catch_frames/evidence/constructor-identity/parent-identity.gdb new file mode 100644 index 0000000000..2db54eea43 --- /dev/null +++ b/benchmarks/catch_frames/evidence/constructor-identity/parent-identity.gdb @@ -0,0 +1,21 @@ +set pagination off +set language c +break js_register_class_parent_dynamic +commands +silent +set $bits = (unsigned long long)$xmm0.v2_int64[0] +printf "register parent: class=%u bits=%llx\n", $edi, $bits +if ($bits >> 48) == 0x7ffd +set $header = $bits & 0xffffffffffff +set $func = *(unsigned long long*)$header +printf "closure function=%llx\n", $func +info symbol $func +set $base = (unsigned long long)&js_register_class_parent_dynamic - 0x747380 +if $func == $base + 0x6e2b00 +set *(unsigned long long*)$header = $base + 0x41de30 +printf "replaced only the duplicated noop identity with the comparison target\n" +end +end +continue +end +run diff --git a/benchmarks/catch_frames/evidence/constructor-identity/parent-identity.gdb.log b/benchmarks/catch_frames/evidence/constructor-identity/parent-identity.gdb.log new file mode 100644 index 0000000000..bf5dd9cf97 --- /dev/null +++ b/benchmarks/catch_frames/evidence/constructor-identity/parent-identity.gdb.log @@ -0,0 +1,67 @@ +Breakpoint 1 at 0x74739d: file crates/perry-runtime/src/object/class_registry/parent_static.rs, line 75. +[Thread debugging using libthread_db enabled] +Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". +plain typeof addEventListener: function +plain typeof removeEventListener: function +plain typeof dispatchEvent: function +plain listener fired +plain dispatch returned: true +Warning: the current language does not match this frame. +register parent: class=1 bits=7ffd0200002742d8 +closure function=555555c36b00 +perry_runtime::object::global_this::fetch_globals::global_this_builtin_noop_thunk in section .text of /root/js-throw-evidence/eventtarget-triage/fix-debug +replaced only the duplicated noop identity with the comparison target +sub typeof addEventListener: function +sub typeof dispatchEvent: function +sub listener fired: go +sub dispatch returned: true +register parent: class=2 bits=7ffd0200002742d8 +closure function=555555971e30 +perry_runtime::node_stream::pipeline::collected_pipeline_error_noop in section .text of /root/js-throw-evidence/eventtarget-triage/fix-debug +two-level typeof dispatchEvent: function +two-level listener fired +register parent: class=4 bits=7ffd0200002742d8 +closure function=555555971e30 +perry_runtime::node_stream::pipeline::collected_pipeline_error_noop in section .text of /root/js-throw-evidence/eventtarget-triage/fix-debug +ctor-subclass listener fired, detail.count: 7 +ctor-subclass field survived super(): 7 +bound-read listener fired +removed listener did not fire +busB has no busA listener +busA listener fired +register parent: class=5 bits=7ffd0200002742d8 +closure function=555555971e30 +perry_runtime::node_stream::pipeline::collected_pipeline_error_noop in section .text of /root/js-throw-evidence/eventtarget-triage/fix-debug +override-subclass inherits addEventListener: function +override ran for: x +override returned: true +once listener call count: 1 +plain instanceof EventTarget: true +sub instanceof EventTarget: true +two-level instanceof EventTarget: true +plain object instanceof EventTarget: false +register parent: class=6 bits=7ffd0200002742d8 +closure function=555555971e30 +perry_runtime::node_stream::pipeline::collected_pipeline_error_noop in section .text of /root/js-throw-evidence/eventtarget-triage/fix-debug +empty-ctor subclass: function function +dynamic-new subclass: function +Reflect.construct subclass: function +register parent: class=12 bits=7ffd0200002742d8 +closure function=555555971e30 +perry_runtime::node_stream::pipeline::collected_pipeline_error_noop in section .text of /root/js-throw-evidence/eventtarget-triage/fix-debug +class-expression subclass: function +class-expression listener fired +register parent: class=13 bits=7ffd0200002742d8 +closure function=555555971e30 +perry_runtime::node_stream::pipeline::collected_pipeline_error_noop in section .text of /root/js-throw-evidence/eventtarget-triage/fix-debug +fn-local subclass: function +fn-local listener fired +register parent: class=7 bits=7ffd0200002742d8 +closure function=555555971e30 +perry_runtime::node_stream::pipeline::collected_pipeline_error_noop in section .text of /root/js-throw-evidence/eventtarget-triage/fix-debug +deleted-own-prop subclass: function function +deleted-own-prop listener fired +frozen subclass: function function +event type: basic cancelable: false +custom event type: custom detail.n: 42 +[Inferior 1 (process 812179) exited normally] diff --git a/benchmarks/catch_frames/evidence/constructor-identity/parent.gdb b/benchmarks/catch_frames/evidence/constructor-identity/parent.gdb new file mode 100644 index 0000000000..6d74e05f37 --- /dev/null +++ b/benchmarks/catch_frames/evidence/constructor-identity/parent.gdb @@ -0,0 +1,16 @@ +set pagination off +set language c +break js_register_class_parent_dynamic +commands +silent +set $bits = (unsigned long long)$xmm0.v2_int64[0] +printf "register parent: class=%u bits=%llx\n", $edi, $bits +if ($bits >> 48) == 0x7ffd +set $header = $bits & 0xffffffffffff +set $func = *(unsigned long long*)$header +printf "closure function=%llx\n", $func +info symbol $func +end +continue +end +run diff --git a/benchmarks/catch_frames/evidence/constructor-identity/parent.gdb.log b/benchmarks/catch_frames/evidence/constructor-identity/parent.gdb.log new file mode 100644 index 0000000000..22b90a1e4a --- /dev/null +++ b/benchmarks/catch_frames/evidence/constructor-identity/parent.gdb.log @@ -0,0 +1,17 @@ +Breakpoint 1 at 0x74739d: file crates/perry-runtime/src/object/class_registry/parent_static.rs, line 75. +[Thread debugging using libthread_db enabled] +Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". +plain typeof addEventListener: function +plain typeof removeEventListener: function +plain typeof dispatchEvent: function +plain listener fired +plain dispatch returned: true +Warning: the current language does not match this frame. +register parent: class=1 bits=7ffd0200002742d8 +closure function=555555c36b00 +perry_runtime::object::global_this::fetch_globals::global_this_builtin_noop_thunk in section .text of /root/js-throw-evidence/eventtarget-triage/fix-debug +sub typeof addEventListener: undefined +sub typeof dispatchEvent: undefined +TypeError: addEventListener is not a function + at makeLocal () +[Inferior 1 (process 782006) exited with code 01] diff --git a/benchmarks/catch_frames/evidence/constructor-identity/rechecks.json.gz b/benchmarks/catch_frames/evidence/constructor-identity/rechecks.json.gz new file mode 100644 index 0000000000..9da2af9caa Binary files /dev/null and b/benchmarks/catch_frames/evidence/constructor-identity/rechecks.json.gz differ diff --git a/benchmarks/catch_frames/evidence/fault-results.json b/benchmarks/catch_frames/evidence/fault-results.json new file mode 100644 index 0000000000..ad17d80912 --- /dev/null +++ b/benchmarks/catch_frames/evidence/fault-results.json @@ -0,0 +1,80 @@ +[ + { + "restore": "shadow", + "test": "exception::savepoints::restore_tests::shadow", + "exit_code": -6, + "detected": true + }, + { + "restore": "runtime_handles", + "test": "exception::savepoints::restore_tests::runtime_handles", + "exit_code": -6, + "detected": true + }, + { + "restore": "call_method", + "test": "exception::savepoints::restore_tests::call_method", + "exit_code": -6, + "detected": true + }, + { + "restore": "pump", + "test": "exception::savepoints::restore_tests::pump", + "exit_code": -6, + "detected": true + }, + { + "restore": "set_foreach", + "test": "exception::savepoints::restore_tests::set_foreach", + "exit_code": -6, + "detected": true + }, + { + "restore": "map_foreach", + "test": "exception::savepoints::restore_tests::map_foreach", + "exit_code": -6, + "detected": true + }, + { + "restore": "prototype_resolution", + "test": "exception::savepoints::restore_tests::prototype_resolution", + "exit_code": -6, + "detected": true + }, + { + "restore": "static_private_owner", + "test": "exception::savepoints::restore_tests::static_private_owner", + "exit_code": -6, + "detected": true + }, + { + "restore": "private_lexical_brand", + "test": "exception::savepoints::restore_tests::private_lexical_brand", + "exit_code": -6, + "detected": true + }, + { + "restore": "derived_super_binding", + "test": "exception::savepoints::restore_tests::derived_super_binding", + "exit_code": -6, + "detected": true + }, + { + "restore": "private_member_access_hints", + "test": "exception::savepoints::restore_tests::private_member_access_hints", + "exit_code": -6, + "detected": true + }, + { + "restore": "regex_factory", + "test": "exception::savepoints::restore_tests::regex_factory", + "exit_code": -6, + "detected": true + }, + { + "restore": "dyn_eval", + "test": "exception::savepoints::restore_tests::dyn_eval", + "exit_code": -6, + "detected": true + } +] diff --git a/benchmarks/catch_frames/evidence/fault-witnesses.txt b/benchmarks/catch_frames/evidence/fault-witnesses.txt new file mode 100644 index 0000000000..f435382458 --- /dev/null +++ b/benchmarks/catch_frames/evidence/fault-witnesses.txt @@ -0,0 +1,132 @@ +Missing-restore fault injections, 2026-09-14, macOS arm64. +Each excerpt is from a separate process; every exit was SIGABRT (-6). + +shadow +running 1 test +test exception::savepoints::restore_tests::shadow ... +thread 'exception::savepoints::restore_tests::shadow' (140782386) panicked at crates/perry-runtime/src/exception/savepoints/tests.rs:51:9: +assertion `left == right` failed: inner catch must preserve its enclosing scope + left: ShadowSavepoint { frame_top: 6, len: 9, temp_roots: 3 } + right: ShadowSavepoint { frame_top: 3, len: 5, temp_roots: 2 } + +runtime_handles +running 1 test +test exception::savepoints::restore_tests::runtime_handles ... +thread 'exception::savepoints::restore_tests::runtime_handles' (140782392) panicked at crates/perry-runtime/src/exception/savepoints/tests.rs:51:9: +assertion `left == right` failed: inner catch must preserve its enclosing scope + left: 3 + right: 2 + +call_method +running 1 test +test exception::savepoints::restore_tests::call_method ... +thread 'exception::savepoints::restore_tests::call_method' (140782397) panicked at crates/perry-runtime/src/exception/savepoints/tests.rs:51:9: +assertion `left == right` failed: inner catch must preserve its enclosing scope + left: 3 + right: 2 + +pump +running 1 test +test exception::savepoints::restore_tests::pump ... +thread 'exception::savepoints::restore_tests::pump' (140782403) panicked at crates/perry-runtime/src/exception/savepoints/tests.rs:51:9: +assertion `left == right` failed: inner catch must preserve its enclosing scope + left: 3 + right: 2 + +set_foreach +running 1 test +test exception::savepoints::restore_tests::set_foreach ... +thread 'exception::savepoints::restore_tests::set_foreach' (140782408) panicked at crates/perry-runtime/src/exception/savepoints/tests.rs:51:9: +assertion `left == right` failed: inner catch must preserve its enclosing scope + left: 3 + right: 2 + +map_foreach +running 1 test +test exception::savepoints::restore_tests::map_foreach ... +thread 'exception::savepoints::restore_tests::map_foreach' (140782417) panicked at crates/perry-runtime/src/exception/savepoints/tests.rs:51:9: +assertion `left == right` failed: inner catch must preserve its enclosing scope + left: 3 + right: 2 + +prototype_resolution +running 1 test +test exception::savepoints::restore_tests::prototype_resolution ... +thread 'exception::savepoints::restore_tests::prototype_resolution' (140782423) panicked at crates/perry-runtime/src/exception/savepoints/tests.rs:51:9: +assertion `left == right` failed: inner catch must preserve its enclosing scope + left: 3 + right: 2 + +static_private_owner +running 1 test +test exception::savepoints::restore_tests::static_private_owner ... +thread 'exception::savepoints::restore_tests::static_private_owner' (140782428) panicked at crates/perry-runtime/src/exception/savepoints/tests.rs:51:9: +assertion `left == right` failed: inner catch must preserve its enclosing scope + left: 3 + right: 2 + +private_lexical_brand +running 1 test +test exception::savepoints::restore_tests::private_lexical_brand ... +thread 'exception::savepoints::restore_tests::private_lexical_brand' (140782432) panicked at crates/perry-runtime/src/exception/savepoints/tests.rs:51:9: +assertion `left == right` failed: inner catch must preserve its enclosing scope + left: 3 + right: 2 + +derived_super_binding +running 1 test +test exception::savepoints::restore_tests::derived_super_binding ... +thread 'exception::savepoints::restore_tests::derived_super_binding' (140782439) panicked at crates/perry-runtime/src/exception/savepoints/tests.rs:51:9: +assertion `left == right` failed: inner catch must preserve its enclosing scope + left: 3 + right: 2 + +private_member_access_hints +running 1 test +test exception::savepoints::restore_tests::private_member_access_hints ... +thread 'exception::savepoints::restore_tests::private_member_access_hints' (140782443) panicked at crates/perry-runtime/src/exception/savepoints/tests.rs:51:9: +assertion `left == right` failed: inner catch must preserve its enclosing scope + left: 3 + right: 2 + +regex_factory +running 1 test +test exception::savepoints::restore_tests::regex_factory ... +thread 'exception::savepoints::restore_tests::regex_factory' (140782449) panicked at crates/perry-runtime/src/exception/savepoints/tests.rs:51:9: +assertion `left == right` failed: inner catch must preserve its enclosing scope + left: 3 + right: 2 + +dyn_eval +running 1 test +test exception::savepoints::restore_tests::dyn_eval ... +thread 'exception::savepoints::restore_tests::dyn_eval' (140782452) panicked at crates/perry-runtime/src/exception/savepoints/tests.rs:51:9: +assertion `left == right` failed: inner catch must preserve its enclosing scope + left: 3298534883331 + right: 2199023255554 + +Restored production source: + +running 20 tests +test exception::savepoints::restore_tests::call_method ... ok +test exception::savepoints::restore_tests::derived_super_binding ... ok +test exception::savepoints::restore_tests::dyn_eval ... ok +test exception::savepoints::restore_tests::map_foreach ... ok +test exception::savepoints::restore_tests::private_lexical_brand ... ok +test exception::savepoints::restore_tests::private_member_access_hints ... ok +test exception::savepoints::restore_tests::prototype_resolution ... ok +test exception::savepoints::restore_tests::pump ... ok +test exception::savepoints::restore_tests::regex_factory ... ok +test exception::savepoints::restore_tests::runtime_handles ... ok +test exception::savepoints::restore_tests::set_foreach ... ok +test exception::savepoints::restore_tests::shadow ... ok +test exception::savepoints::restore_tests::static_private_owner ... ok +test exception::tests::arm_trap_and_run_catches_a_real_throw ... ok +test exception::tests::catch_js_throw_err_clears_exception ... ok +test exception::tests::js_throw_path_restores_runtime_handles_across_unwound_frames ... ok +test exception::tests::js_throw_path_restores_shadow_stack_across_unwound_frames ... ok +test exception::tests::nested_trampoline_arms_unwind_innermost_first ... ok +test exception::tests::the_uncaught_report_carries_the_error_code_and_the_frames ... ok +test exception::tests::try_push_pop_beyond_old_limit_does_not_panic ... ok + +test result: ok. 20 passed; 0 failed; 0 ignored; 0 measured; 3795 filtered out; finished in 0.03s diff --git a/benchmarks/catch_frames/evidence/fix-full-lint.log b/benchmarks/catch_frames/evidence/fix-full-lint.log new file mode 100644 index 0000000000..7d6fed20d2 --- /dev/null +++ b/benchmarks/catch_frames/evidence/fix-full-lint.log @@ -0,0 +1,104 @@ +run_lint_gates: 79 gate commands derived from 45 lint run steps + + ok [Changeset gate self-test] ./scripts/check_changeset_fragment.sh --self-test + skip [Require a changelog.d/ fragment for crates/ changes] ./scripts/check_changeset_fragment.sh "${{ github.repository }}" "${{ github.event.pull_request.number }}" + (needs GitHub API repository/PR context) + ok [Validate benchmark artifact and fallback gates] python3 -m unittest discover -s tests -p 'test_benchmark_*.py' -v + ok [Validate benchmark artifact and fallback gates] ./tests/test_benchmark_peer_fallback.sh + ok [Validate benchmark artifact and fallback gates] ./tests/test_benchmark_output_verifier.sh + ok [Validate benchmark artifact and fallback gates] bash -n benchmarks/compare.sh benchmarks/quick.sh benchmarks/suite/run_benchmarks.sh benchmarks/honest_bench/run.sh benchmarks/honest_bench/harness/run_http_bench.sh tests/test_benchmark_peer_fallback.sh + ok [Validate Windows LLVM runtime npm staging] ./tests/test_stage_npm_windows_llvm.sh + ok [Release pipeline failure-path regressions] python3 tests/test_release_pipeline.py + ok [Release pipeline failure-path regressions] node --test scripts/publish/platform-visibility.test.mjs + ok [Install Rust toolchain] rustup toolchain install nightly-2026-08-20 --profile minimal --component rustfmt --component clippy + ok [Check formatting] cargo fmt --all -- --check + ok [CI plan policy self-test + docs table freshness] python3 scripts/ci_plan.py --self-test + ok [CI plan policy self-test + docs table freshness] python3 scripts/ci_pr_files.py --self-test + ok [CI plan policy self-test + docs table freshness] python3 scripts/ci_cargo_test_shard.py --self-test + skip [CI plan policy self-test + docs table freshness] python3 scripts/ci_cargo_test_shard.py --package perry --total-shards "${{ fromJSON(needs.plan.outputs.plan).cargo_test_perry.total }}" --validate + (needs the CI plan's shard count) + ok [Compiler-output gate liveness] python3 scripts/compiler_output_step_liveness.py --self-test + ok [Compiler-output gate liveness] python3 scripts/compiler_output_step_liveness.py + ok [Audit workspace architecture] python3 scripts/workspace_architecture.py --self-test + ok [Audit workspace architecture] python3 scripts/workspace_architecture.py --check --print-summary + ok [Linux incident collector fixtures] python3 scripts/test_capture_linux_incident.py + ok [Public benchmark evidence freshness] PYTHONPATH=. python3 tests/test_public_baseline.py + FAIL [Public benchmark evidence freshness] python3 benchmarks/ci_public_baseline_check.py + public baseline error: public artifact benchmark inputs changed; regenerate it with ./benchmarks/run_public_baseline.sh + ok [File size limit] ./scripts/check_file_size.sh + ok [Local binding type-proof audit] python3 scripts/local_binding_type_audit.py --self-test + ok [Local binding type-proof audit] python3 scripts/local_binding_type_audit.py + ok [Binding upstream pins (lock-step)] node scripts/binding_pins.mjs --check + ok [Bundled binding governance] python3 scripts/binding_governance.py --check + ok [GC store-site inventory] python3 scripts/gc_store_site_inventory.py --self-test + ok [GC store-site inventory] python3 scripts/gc_store_site_inventory.py + ok [Address-classification audit] python3 scripts/addr_class_inventory.py --self-test + ok [Address-classification audit] python3 scripts/addr_class_inventory.py + ok [String payload-access inventory] python3 scripts/string_payload_access_inventory.py --self-test + ok [String payload-access inventory] python3 scripts/string_payload_access_inventory.py + ok [Class-id collision audit] python3 scripts/class_id_collisions.py + ok [Object-header shape-descriptor census] python3 scripts/shape_descriptor_census.py + ok [GC pin-site custody audit] python3 scripts/gc_pin_sites.py --self-test + ok [GC pin-site custody audit] python3 scripts/gc_pin_sites.py + ok [Cross-thread promise provenance (#9552)] python3 scripts/check_cross_thread_promise_provenance.py --self-test + ok [Cross-thread promise provenance (#9552)] python3 scripts/check_cross_thread_promise_provenance.py + ok [Runtime GC-pointer holder custody audit] python3 scripts/gc_runtime_root_holders.py --self-test + ok [Runtime GC-pointer holder custody audit] python3 scripts/gc_runtime_root_holders.py + ok [Poll-capable reach audit (GC root dominance)] python3 scripts/gc_root_dominance_check.py --self-test + ok [Poll-capable reach audit (GC root dominance)] python3 scripts/gc_root_dominance_check.py --audit-poll-reach + ok [Thread-local GC scanner registration audit] python3 scripts/check_gc_scanner_latches.py --self-test + ok [Thread-local GC scanner registration audit] python3 scripts/check_gc_scanner_latches.py + ok [Rekeyed side-table custody audit] python3 scripts/gc_rekeyed_key_tables.py --self-test + ok [Rekeyed side-table custody audit] python3 scripts/gc_rekeyed_key_tables.py + ok [GC environment-knob drift audit] python3 scripts/check_gc_env_knobs.py --self-test + ok [GC environment-knob drift audit] python3 scripts/check_gc_env_knobs.py + ok [llvm-inprocess corpus currency] python3 scripts/check_llvm_corpus_currency.py --self-test + ok [llvm-inprocess corpus currency] python3 scripts/check_llvm_corpus_currency.py + ok [Statepoint pass source reader] python3 scripts/read_statepoint_rewrite_passes.py --self-test + ok [Statepoint pass source reader] python3 scripts/read_statepoint_rewrite_passes.py --check + ok [GC documentation claim audit] python3 scripts/check_gc_doc_claims.py --self-test + ok [GC documentation claim audit] python3 scripts/check_gc_doc_claims.py + ok [Windows-portable text I/O in the Windows-CI audits] python3 scripts/check_locale_independent_io.py --self-test + ok [Windows-portable text I/O in the Windows-CI audits] python3 scripts/check_locale_independent_io.py + ok [GC evacuation-liveness assert can say no] python3 scripts/gc_evacuation_liveness_assert.py --self-test + ok [Node version consistency] python3 scripts/check_node_version_consistency.py --self-test + ok [Node version consistency] python3 scripts/check_node_version_consistency.py + ok [Raw-handle debt ratchet] python3 scripts/raw_handle_debt.py --self-test + ok [Raw-handle debt ratchet] python3 scripts/raw_handle_debt.py + ok [Unrooted-local shape ratchet] python3 scripts/unrooted_local_shape.py --self-test + ok [Unrooted-local shape ratchet] python3 scripts/unrooted_local_shape.py --check + ok [Unrooted-local shape ratchet vs. merge base] python3 scripts/unrooted_local_shape.py --no-raise-vs "$BASE_SHA" + ok [Raw-handle debt ratchet vs. merge base] python3 scripts/raw_handle_debt.py --no-raise-vs "$BASE_SHA" + ok [Gap snapshot checker self-test] python3 scripts/gap_snapshot.py --self-test + ok [Platform-aware parity allowlist self-test] python3 scripts/parity_known_failures.py --self-test + ok [Parity allowlist ratchet (provenance + stale entries)] python3 scripts/parity_known_failures.py --audit + ok [Moving-GC gate wiring] python3 scripts/gc_gate_wiring_check.py --self-test + ok [Moving-GC gate wiring] python3 scripts/gc_gate_wiring_check.py + ok [GC matrix liveness gate] ./scripts/gc_repsel_matrix.sh --self-test-liveness-parser + ok [GC matrix liveness gate] python3 scripts/gc_repsel_matrix_merge.py --self-test + ok [GC matrix liveness gate] python3 scripts/gc_matrix_liveness_check.py --self-test + ok [GC matrix liveness gate] python3 scripts/gc_matrix_liveness_check.py --check-registry + ok [Test registration (dark tests)] python3 scripts/check_test_registration.py --self-test + ok [Test registration (dark tests)] python3 scripts/check_test_registration.py + ok [Per-test global sinks] python3 scripts/global_sink_isolation.py --self-test + ok [Per-test global sinks] python3 scripts/global_sink_isolation.py + +run_lint_gates: 6 compile commands derived from warnings + check (18 exclude args) + ok [warnings: rustc warnings (product)] RUSTFLAGS="-D warnings" cargo check -p perry --bins + FAIL [warnings: rustc warnings (host-compatible, all targets)] RUSTFLAGS="-D warnings" cargo check --workspace --all-targets --exclude perry-ui-android --exclude perry-ui-gtk4 --exclude perry-ui-ios --exclude perry-ui-macos --exclude perry-ui-tvos --exclude perry-ui-visionos --exclude perry-ui-watchos --exclude perry-ui-windows --exclude perry-ui-windows-winui + --> crates/perry-runtime/src/object/global_this_webassembly.rs:201:15 + | + 201 | pub(crate) fn registered_extern_handle(wrapper: usize, expected_kind: &[u8]) -> Option { + | ^^^^^^^^^^^^^^^^^^^^^^^^ + + error: could not compile `perry-runtime` (lib test) due to 4 previous errors + ok [check: Clippy (product)] cargo clippy -p perry --bins + ok [check: Clippy (host-compatible)] cargo clippy --workspace --exclude perry-ui-android --exclude perry-ui-gtk4 --exclude perry-ui-ios --exclude perry-ui-macos --exclude perry-ui-tvos --exclude perry-ui-visionos --exclude perry-ui-watchos --exclude perry-ui-windows --exclude perry-ui-windows-winui + ok [check: Regenerate API docs] ./scripts/regen_api_docs.sh + FAIL [check: Check for API docs drift] git diff --quiet -- docs/src/api/reference.md docs/api/perry.d.ts + + +run_lint_gates: 3 of 83 FAILED; 2 CI-only skipped + [Public benchmark evidence freshness] python3 benchmarks/ci_public_baseline_check.py + [warnings: rustc warnings (host-compatible, all targets)] RUSTFLAGS="-D warnings" cargo check --workspace --all-targets --exclude perry-ui-android --exclude perry-ui-gtk4 --exclude perry-ui-ios --exclude perry-ui-macos --exclude perry-ui-tvos --exclude perry-ui-visionos --exclude perry-ui-watchos --exclude perry-ui-windows --exclude perry-ui-windows-winui + [check: Check for API docs drift] git diff --quiet -- docs/src/api/reference.md docs/api/perry.d.ts diff --git a/benchmarks/catch_frames/evidence/gap.json.gz b/benchmarks/catch_frames/evidence/gap.json.gz new file mode 100644 index 0000000000..6053e55c9d Binary files /dev/null and b/benchmarks/catch_frames/evidence/gap.json.gz differ diff --git a/benchmarks/catch_frames/evidence/gc-comparison.json b/benchmarks/catch_frames/evidence/gc-comparison.json new file mode 100644 index 0000000000..25ee34e655 --- /dev/null +++ b/benchmarks/catch_frames/evidence/gc-comparison.json @@ -0,0 +1,11 @@ +{ + "baseline_commit": "eb13fa188d", + "probes": 14, + "repeats": 7, + "heap_and_collector_medians_compared": 126, + "changed_medians": [], + "ratchet_profile": "shared_ci", + "ratchet_result": "pass", + "baseline_kind": "fresh same-host main; does not replace the committed artifact", + "candidate_scope": "final guarded three-package artifacts" +} diff --git a/benchmarks/catch_frames/evidence/gc-stress.json.gz b/benchmarks/catch_frames/evidence/gc-stress.json.gz new file mode 100644 index 0000000000..351a03f4d6 Binary files /dev/null and b/benchmarks/catch_frames/evidence/gc-stress.json.gz differ diff --git a/benchmarks/catch_frames/evidence/guard-gc-artifacts.json b/benchmarks/catch_frames/evidence/guard-gc-artifacts.json new file mode 100644 index 0000000000..1536fb3124 --- /dev/null +++ b/benchmarks/catch_frames/evidence/guard-gc-artifacts.json @@ -0,0 +1,7 @@ +{ + "perry": "30410e4e815b93ffd1c7d4bc65aa2ff66111c5b79ff3ce18e466fb3d8a63fbc0", + "libperry_runtime.a": "2c0b05fbad66bb569241ccf742831504c9ef8c4819a31845c3d92c70d0efaa72", + "libperry_stdlib.a": "547c3758646fb5f0b33fd298e6954746b251e2d23dd4cd5c65403431616ac620", + "libperry_ext_net.a": "df818833d44021acd73b33c39c9b23a5b3b90d680c6f0b6af4c275a9abb8340b", + "libperry_ext_http.a": "c64f8d7ce7f9e53fdc09f3ded8f23db12d1a9e509876fa419ef9c5c496ce81ea" +} diff --git a/benchmarks/catch_frames/evidence/guard-gc-ratchet-check.log b/benchmarks/catch_frames/evidence/guard-gc-ratchet-check.log new file mode 100644 index 0000000000..6d8780f371 --- /dev/null +++ b/benchmarks/catch_frames/evidence/guard-gc-ratchet-check.log @@ -0,0 +1,193 @@ +## GC ratchet + +- Profile: `shared_ci` +- Baseline commit: `eb13fa188d` +- Baseline captured: 2026-09-14T04:44:16+00:00 on `linux-x86_64` +- Baseline host: x86_64, 16 cores, load at capture 10.89 + +Only rows marked gating can fail the job. Non-gating rows are recorded so a +drift that is real but unmeasurable on this runner is still visible. + +Probes pinned under a non-default collector configuration: + +- `13_large_eden_survivors` — `PERRY_GC_SCAVENGE_NURSERY_MB=64` +- `14_grow_then_churn` — `PERRY_GC_MAJOR_PACING_FLOOR_MB=1 PERRY_GC_SCAVENGE_NURSERY_MB=1` + +| Probe | Metric | Baseline | Current | Δ | Allowance | Gating | Status | +|-------|--------|---------:|--------:|---:|----------:|:------:|--------| +| `01_nursery_churn` | heap_used_bytes | 438,744 | 438,744 | +0.00% | 65,536 | yes | ok | +| `01_nursery_churn` | heap_total_bytes | 14,680,064 | 14,680,064 | +0.00% | 1,048,576 | yes | ok | +| `01_nursery_churn` | minor_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `01_nursery_churn` | step_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `01_nursery_churn` | copied_objects | 6,690 | 6,690 | +0.00% | 334 | yes | ok | +| `01_nursery_churn` | copied_bytes | 466,392 | 466,392 | +0.00% | 65,536 | yes | ok | +| `01_nursery_churn` | promoted_objects | 0 | 0 | - | 16 | yes | ok | +| `01_nursery_churn` | promoted_bytes | 0 | 0 | - | 65,536 | yes | ok | +| `01_nursery_churn` | freed_bytes | 11,067,816 | 11,067,816 | +0.00% | 1,048,576 | yes | ok | +| `01_nursery_churn` | rss_bytes | 59,191,296 | 60,506,112 | +2.22% | 8,878,694 | no | ok | +| `01_nursery_churn` | peak_rss_bytes | 61,480,960 | 62,861,312 | +2.25% | 9,222,144 | no | ok | +| `01_nursery_churn` | wall_ms | 57 | 69 | +20.64% | 100 | no | ok | +| `02_survivor_promotion` | heap_used_bytes | 2,562,832 | 2,562,832 | +0.00% | 65,536 | yes | ok | +| `02_survivor_promotion` | heap_total_bytes | 15,728,640 | 15,728,640 | +0.00% | 1,048,576 | yes | ok | +| `02_survivor_promotion` | minor_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `02_survivor_promotion` | step_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `02_survivor_promotion` | copied_objects | 37,366 | 37,366 | +0.00% | 1,868 | yes | ok | +| `02_survivor_promotion` | copied_bytes | 1,690,264 | 1,690,264 | +0.00% | 84,513 | yes | ok | +| `02_survivor_promotion` | promoted_objects | 0 | 0 | - | 16 | yes | ok | +| `02_survivor_promotion` | promoted_bytes | 0 | 0 | - | 65,536 | yes | ok | +| `02_survivor_promotion` | freed_bytes | 8,795,408 | 8,795,408 | +0.00% | 1,048,576 | yes | ok | +| `02_survivor_promotion` | rss_bytes | 59,736,064 | 61,808,640 | +3.47% | 8,960,410 | no | ok | +| `02_survivor_promotion` | peak_rss_bytes | 72,052,736 | 69,627,904 | -3.37% | 10,807,910 | no | ok | +| `02_survivor_promotion` | wall_ms | 69 | 87 | +26.70% | 100 | no | ok | +| `03_cross_gen_writes` | heap_used_bytes | 528 | 528 | +0.00% | 65,536 | yes | ok | +| `03_cross_gen_writes` | heap_total_bytes | 12,582,912 | 12,582,912 | +0.00% | 1,048,576 | yes | ok | +| `03_cross_gen_writes` | minor_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `03_cross_gen_writes` | step_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `03_cross_gen_writes` | copied_objects | 4,107 | 4,107 | +0.00% | 205 | yes | ok | +| `03_cross_gen_writes` | copied_bytes | 197,056 | 197,056 | +0.00% | 65,536 | yes | ok | +| `03_cross_gen_writes` | promoted_objects | 0 | 0 | - | 16 | yes | ok | +| `03_cross_gen_writes` | promoted_bytes | 0 | 0 | - | 65,536 | yes | ok | +| `03_cross_gen_writes` | freed_bytes | 9,240,008 | 9,240,008 | +0.00% | 1,048,576 | yes | ok | +| `03_cross_gen_writes` | rss_bytes | 49,836,032 | 41,787,392 | -16.15% | 8,388,608 | no | ok | +| `03_cross_gen_writes` | peak_rss_bytes | 57,798,656 | 50,696,192 | -12.29% | 8,669,798 | no | ok | +| `03_cross_gen_writes` | wall_ms | 36 | 54 | +50.53% | 100 | no | ok | +| `04_dead_after_deep_stack` | heap_used_bytes | 536 | 536 | +0.00% | 65,536 | yes | ok | +| `04_dead_after_deep_stack` | heap_total_bytes | 18,874,368 | 18,874,368 | +0.00% | 1,048,576 | yes | ok | +| `04_dead_after_deep_stack` | minor_cycles | 7 | 7 | +0.00% | 1 | yes | ok | +| `04_dead_after_deep_stack` | step_cycles | 7 | 7 | +0.00% | 1 | yes | ok | +| `04_dead_after_deep_stack` | copied_objects | 2,666 | 2,666 | +0.00% | 133 | yes | ok | +| `04_dead_after_deep_stack` | copied_bytes | 111,336 | 111,336 | +0.00% | 65,536 | yes | ok | +| `04_dead_after_deep_stack` | promoted_objects | 682 | 682 | +0.00% | 34 | yes | ok | +| `04_dead_after_deep_stack` | promoted_bytes | 27,320 | 27,320 | +0.00% | 65,536 | yes | ok | +| `04_dead_after_deep_stack` | freed_bytes | 76,493,952 | 76,493,952 | +0.00% | 3,824,698 | yes | ok | +| `04_dead_after_deep_stack` | rss_bytes | 56,668,160 | 58,011,648 | +2.37% | 8,500,224 | no | ok | +| `04_dead_after_deep_stack` | peak_rss_bytes | 64,937,984 | 62,251,008 | -4.14% | 9,740,698 | no | ok | +| `04_dead_after_deep_stack` | wall_ms | 50 | 67 | +34.21% | 100 | no | ok | +| `05_closure_capture` | heap_used_bytes | 544 | 544 | +0.00% | 65,536 | yes | ok | +| `05_closure_capture` | heap_total_bytes | 14,680,064 | 14,680,064 | +0.00% | 1,048,576 | yes | ok | +| `05_closure_capture` | minor_cycles | 3 | 3 | +0.00% | 1 | yes | ok | +| `05_closure_capture` | step_cycles | 3 | 3 | +0.00% | 1 | yes | ok | +| `05_closure_capture` | copied_objects | 14,285 | 14,285 | +0.00% | 714 | yes | ok | +| `05_closure_capture` | copied_bytes | 534,720 | 534,720 | +0.00% | 65,536 | yes | ok | +| `05_closure_capture` | promoted_objects | 6,154 | 6,154 | +0.00% | 308 | yes | ok | +| `05_closure_capture` | promoted_bytes | 221,624 | 221,624 | +0.00% | 65,536 | yes | ok | +| `05_closure_capture` | freed_bytes | 27,897,872 | 27,897,872 | +0.00% | 1,394,894 | yes | ok | +| `05_closure_capture` | rss_bytes | 52,531,200 | 52,678,656 | +0.28% | 8,388,608 | no | ok | +| `05_closure_capture` | peak_rss_bytes | 60,334,080 | 61,431,808 | +1.82% | 9,050,112 | no | ok | +| `05_closure_capture` | wall_ms | 70 | 77 | +8.58% | 100 | no | ok | +| `06_string_retention` | heap_used_bytes | 1,072 | 1,072 | +0.00% | 65,536 | yes | ok | +| `06_string_retention` | heap_total_bytes | 22,020,096 | 22,020,096 | +0.00% | 1,048,576 | yes | ok | +| `06_string_retention` | minor_cycles | 4 | 4 | +0.00% | 1 | yes | ok | +| `06_string_retention` | step_cycles | 4 | 4 | +0.00% | 1 | yes | ok | +| `06_string_retention` | copied_objects | 270 | 270 | +0.00% | 16 | yes | ok | +| `06_string_retention` | copied_bytes | 70,144 | 70,144 | +0.00% | 65,536 | yes | ok | +| `06_string_retention` | promoted_objects | 14 | 14 | +0.00% | 16 | yes | ok | +| `06_string_retention` | promoted_bytes | 1,072 | 1,072 | +0.00% | 65,536 | yes | ok | +| `06_string_retention` | freed_bytes | 68,136,816 | 68,136,816 | +0.00% | 3,406,841 | yes | ok | +| `06_string_retention` | rss_bytes | 59,215,872 | 50,757,632 | -14.28% | 8,882,381 | no | ok | +| `06_string_retention` | peak_rss_bytes | 67,723,264 | 54,624,256 | -19.34% | 10,158,490 | no | improvement | +| `06_string_retention` | wall_ms | 62 | 137 | +122.65% | 100 | no | ok | +| `07_array_grow_evacuate` | heap_used_bytes | 1,211,232 | 1,211,232 | +0.00% | 65,536 | yes | ok | +| `07_array_grow_evacuate` | heap_total_bytes | 25,165,824 | 25,165,824 | +0.00% | 1,048,576 | yes | ok | +| `07_array_grow_evacuate` | minor_cycles | 5 | 5 | +0.00% | 1 | yes | ok | +| `07_array_grow_evacuate` | step_cycles | 5 | 5 | +0.00% | 1 | yes | ok | +| `07_array_grow_evacuate` | copied_objects | 6,702 | 6,702 | +0.00% | 335 | yes | ok | +| `07_array_grow_evacuate` | copied_bytes | 2,636,392 | 2,636,392 | +0.00% | 131,820 | no | ok | +| `07_array_grow_evacuate` | promoted_objects | 6,154 | 6,154 | +0.00% | 308 | yes | ok | +| `07_array_grow_evacuate` | promoted_bytes | 841,048 | 841,048 | +0.00% | 65,536 | yes | ok | +| `07_array_grow_evacuate` | freed_bytes | 83,570,704 | 83,570,704 | +0.00% | 4,178,535 | no | ok | +| `07_array_grow_evacuate` | rss_bytes | 68,734,976 | 66,523,136 | -3.22% | 10,310,246 | no | ok | +| `07_array_grow_evacuate` | peak_rss_bytes | 78,344,192 | 71,229,440 | -9.08% | 11,751,629 | no | ok | +| `07_array_grow_evacuate` | wall_ms | 42 | 87 | +107.81% | 100 | no | ok | +| `08_map_set_sidetables` | heap_used_bytes | 688 | 688 | +0.00% | 65,536 | yes | ok | +| `08_map_set_sidetables` | heap_total_bytes | 14,680,064 | 14,680,064 | +0.00% | 1,048,576 | yes | ok | +| `08_map_set_sidetables` | minor_cycles | 6 | 6 | +0.00% | 1 | yes | ok | +| `08_map_set_sidetables` | step_cycles | 6 | 6 | +0.00% | 1 | yes | ok | +| `08_map_set_sidetables` | copied_objects | 2,461 | 2,461 | +0.00% | 123 | yes | ok | +| `08_map_set_sidetables` | copied_bytes | 98,472 | 98,472 | +0.00% | 65,536 | yes | ok | +| `08_map_set_sidetables` | promoted_objects | 14 | 14 | +0.00% | 16 | yes | ok | +| `08_map_set_sidetables` | promoted_bytes | 592 | 592 | +0.00% | 65,536 | yes | ok | +| `08_map_set_sidetables` | freed_bytes | 56,598,904 | 56,598,904 | +0.00% | 2,829,945 | yes | ok | +| `08_map_set_sidetables` | rss_bytes | 53,194,752 | 52,191,232 | -1.89% | 8,388,608 | no | ok | +| `08_map_set_sidetables` | peak_rss_bytes | 63,270,912 | 62,234,624 | -1.64% | 9,490,637 | no | ok | +| `08_map_set_sidetables` | wall_ms | 213 | 186 | -12.60% | 100 | no | ok | +| `09_try_catch_roots` | heap_used_bytes | 443,520 | 443,520 | +0.00% | 65,536 | yes | ok | +| `09_try_catch_roots` | heap_total_bytes | 14,680,064 | 14,680,064 | +0.00% | 1,048,576 | yes | ok | +| `09_try_catch_roots` | minor_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `09_try_catch_roots` | step_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `09_try_catch_roots` | copied_objects | 6,673 | 6,673 | +0.00% | 334 | yes | ok | +| `09_try_catch_roots` | copied_bytes | 462,024 | 462,024 | +0.00% | 65,536 | yes | ok | +| `09_try_catch_roots` | promoted_objects | 0 | 0 | - | 16 | yes | ok | +| `09_try_catch_roots` | promoted_bytes | 0 | 0 | - | 65,536 | yes | ok | +| `09_try_catch_roots` | freed_bytes | 10,023,568 | 10,023,568 | +0.00% | 1,048,576 | yes | ok | +| `09_try_catch_roots` | rss_bytes | 61,202,432 | 50,925,568 | -16.79% | 9,180,365 | no | improvement | +| `09_try_catch_roots` | peak_rss_bytes | 63,582,208 | 51,298,304 | -19.32% | 9,537,331 | no | improvement | +| `09_try_catch_roots` | wall_ms | 157 | 213 | +35.52% | 100 | no | ok | +| `10_store_receiver_across_alloc` | heap_used_bytes | 446,776 | 446,776 | +0.00% | 65,536 | yes | ok | +| `10_store_receiver_across_alloc` | heap_total_bytes | 22,020,096 | 22,020,096 | +0.00% | 1,048,576 | yes | ok | +| `10_store_receiver_across_alloc` | minor_cycles | 8 | 8 | +0.00% | 1 | yes | ok | +| `10_store_receiver_across_alloc` | step_cycles | 8 | 8 | +0.00% | 1 | yes | ok | +| `10_store_receiver_across_alloc` | copied_objects | 14,625 | 14,625 | +0.00% | 731 | yes | ok | +| `10_store_receiver_across_alloc` | copied_bytes | 787,792 | 787,792 | +0.00% | 65,536 | yes | ok | +| `10_store_receiver_across_alloc` | promoted_objects | 6,107 | 6,107 | +0.00% | 305 | yes | ok | +| `10_store_receiver_across_alloc` | promoted_bytes | 446,368 | 446,368 | +0.00% | 65,536 | yes | ok | +| `10_store_receiver_across_alloc` | freed_bytes | 88,640,584 | 88,640,584 | +0.00% | 4,432,029 | yes | ok | +| `10_store_receiver_across_alloc` | rss_bytes | 67,842,048 | 65,650,688 | -3.23% | 10,176,307 | no | ok | +| `10_store_receiver_across_alloc` | peak_rss_bytes | 74,194,944 | 71,938,048 | -3.04% | 11,129,242 | no | ok | +| `10_store_receiver_across_alloc` | wall_ms | 203 | 214 | +4.96% | 100 | no | ok | +| `11_collect_at_depth` | heap_used_bytes | 444,720 | 444,720 | +0.00% | 65,536 | yes | ok | +| `11_collect_at_depth` | heap_total_bytes | 12,582,912 | 12,582,912 | +0.00% | 1,048,576 | yes | ok | +| `11_collect_at_depth` | minor_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `11_collect_at_depth` | step_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `11_collect_at_depth` | copied_objects | 6,686 | 6,686 | +0.00% | 334 | yes | ok | +| `11_collect_at_depth` | copied_bytes | 462,544 | 462,544 | +0.00% | 65,536 | yes | ok | +| `11_collect_at_depth` | promoted_objects | 0 | 0 | - | 16 | yes | ok | +| `11_collect_at_depth` | promoted_bytes | 0 | 0 | - | 65,536 | yes | ok | +| `11_collect_at_depth` | freed_bytes | 8,974,576 | 8,974,576 | +0.00% | 1,048,576 | yes | ok | +| `11_collect_at_depth` | rss_bytes | 57,896,960 | 59,953,152 | +3.55% | 8,684,544 | no | ok | +| `11_collect_at_depth` | peak_rss_bytes | 61,542,400 | 62,853,120 | +2.13% | 9,231,360 | no | ok | +| `11_collect_at_depth` | wall_ms | 54 | 58 | +7.71% | 100 | no | ok | +| `12_large_live_set` | heap_used_bytes | 569,448 | 569,448 | +0.00% | 65,536 | yes | ok | +| `12_large_live_set` | heap_total_bytes | 74,448,896 | 74,448,896 | +0.00% | 1,488,978 | yes | ok | +| `12_large_live_set` | minor_cycles | 5 | 5 | +0.00% | 1 | yes | ok | +| `12_large_live_set` | step_cycles | 5 | 5 | +0.00% | 1 | yes | ok | +| `12_large_live_set` | copied_objects | 104,050 | 104,050 | +0.00% | 5,202 | yes | ok | +| `12_large_live_set` | copied_bytes | 4,162,008 | 4,162,008 | +0.00% | 208,100 | yes | ok | +| `12_large_live_set` | promoted_objects | 530,030 | 530,030 | +0.00% | 26,502 | yes | ok | +| `12_large_live_set` | promoted_bytes | 21,201,208 | 21,201,208 | +0.00% | 1,060,060 | yes | ok | +| `12_large_live_set` | freed_bytes | 63,732,784 | 63,732,784 | +0.00% | 3,186,639 | yes | ok | +| `12_large_live_set` | rss_bytes | 149,266,432 | 145,145,856 | -2.76% | 22,389,965 | no | ok | +| `12_large_live_set` | peak_rss_bytes | 165,412,864 | 157,700,096 | -4.66% | 24,811,930 | no | ok | +| `12_large_live_set` | wall_ms | 345 | 403 | +16.93% | 103 | no | ok | +| `13_large_eden_survivors` | heap_used_bytes | 444,976 | 444,976 | +0.00% | 65,536 | yes | ok | +| `13_large_eden_survivors` | heap_total_bytes | 61,865,984 | 61,865,984 | +0.00% | 1,237,320 | no | ok | +| `13_large_eden_survivors` | minor_cycles | 3 | 3 | +0.00% | 1 | yes | ok | +| `13_large_eden_survivors` | step_cycles | 3 | 3 | +0.00% | 1 | yes | ok | +| `13_large_eden_survivors` | copied_objects | 382,868 | 382,868 | +0.00% | 19,143 | yes | ok | +| `13_large_eden_survivors` | copied_bytes | 21,804,904 | 21,804,904 | +0.00% | 1,090,245 | yes | ok | +| `13_large_eden_survivors` | promoted_objects | 387,152 | 387,152 | +0.00% | 19,358 | yes | ok | +| `13_large_eden_survivors` | promoted_bytes | 21,975,560 | 21,975,560 | +0.00% | 1,098,778 | yes | ok | +| `13_large_eden_survivors` | freed_bytes | 108,045,936 | 108,045,936 | +0.00% | 5,402,297 | yes | ok | +| `13_large_eden_survivors` | rss_bytes | 161,558,528 | 156,545,024 | -3.10% | 24,233,779 | no | ok | +| `13_large_eden_survivors` | peak_rss_bytes | 190,017,536 | 176,799,744 | -6.96% | 28,502,630 | no | ok | +| `13_large_eden_survivors` | wall_ms | 978 | 1,148 | +17.37% | 294 | no | ok | +| `14_grow_then_churn` | heap_used_bytes | 429,408 | 429,408 | +0.00% | 65,536 | yes | ok | +| `14_grow_then_churn` | heap_total_bytes | 23,068,672 | 23,068,672 | +0.00% | 1,048,576 | yes | ok | +| `14_grow_then_churn` | minor_cycles | 22 | 22 | +0.00% | 1 | yes | ok | +| `14_grow_then_churn` | step_cycles | 23 | 23 | +0.00% | 1 | yes | ok | +| `14_grow_then_churn` | copied_objects | 330 | 330 | +0.00% | 16 | yes | ok | +| `14_grow_then_churn` | copied_bytes | 1,277,072 | 1,277,072 | +0.00% | 65,536 | yes | ok | +| `14_grow_then_churn` | promoted_objects | 405,282 | 405,282 | +0.00% | 20,264 | yes | ok | +| `14_grow_then_churn` | promoted_bytes | 147,967,848 | 147,967,848 | +0.00% | 7,398,392 | yes | ok | +| `14_grow_then_churn` | freed_bytes | 119,549,744 | 119,549,744 | +0.00% | 5,977,487 | yes | ok | +| `14_grow_then_churn` | rss_bytes | 154,632,192 | 154,738,688 | +0.07% | 23,194,829 | no | ok | +| `14_grow_then_churn` | peak_rss_bytes | 376,299,520 | 361,336,832 | -3.98% | 56,444,928 | no | ok | +| `14_grow_then_churn` | wall_ms | 464 | 997 | +115.05% | 139 | no | drift (informational) | + +### Cells excluded from the gating family by probe override + +- `07_array_grow_evacuate`.copied_bytes — 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. (observed spread 8,064 over 21 runs, 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.; https://github.com/PerryTS/perry/issues/9790) +- `07_array_grow_evacuate`.freed_bytes — 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. (observed spread 8,208 over 21 runs, 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.; https://github.com/PerryTS/perry/issues/9790) +- `13_large_eden_survivors`.heap_total_bytes — NOT GATED ON THIS PROBE (#9829). Total reserved arena capacity varies by 2 MiB across executions of one unchanged binary: 18/21 runs reserve 61865984 bytes and 3/21 reserve 63963136. All 21 report the same 227048 live bytes and identical seven GC counters (including three minors and 377569 copied objects), with Node-equivalent output. js_arena_stats sums reserved block sizes for heapTotal, separately from its exact live-object census for heapUsed. This is spare-capacity/placement variation, not changing live retention or GC work; the precise allocating block/order is not isolated. CI run 33989581881 independently contains the same two capacities. Keep the cell visible, while heapUsed, liveness, copying/promotion/reclamation, and all other probes remain gated. Raw 21-run evidence and binary/source hashes: evidence/9829-recovery.json. (observed spread 2,097,152 over 21 runs, 2026-09-06, linux-x86_64, clean main d36a1af0c compiler and matching runtime/stdlib archives. One unchanged probe binary, 21 traced executions with PERRY_GC_SCAVENGE_NURSERY_MB=64. See evidence/9829-recovery.json capacity_variance.; https://github.com/PerryTS/perry/issues/9829) + +gc-ratchet: OK diff --git a/benchmarks/catch_frames/evidence/guard-gc-ratchet.json.gz b/benchmarks/catch_frames/evidence/guard-gc-ratchet.json.gz new file mode 100644 index 0000000000..90ceee4278 Binary files /dev/null and b/benchmarks/catch_frames/evidence/guard-gc-ratchet.json.gz differ diff --git a/benchmarks/catch_frames/evidence/guard-gc-stress-summary.txt b/benchmarks/catch_frames/evidence/guard-gc-stress-summary.txt new file mode 100644 index 0000000000..3f01226fb8 --- /dev/null +++ b/benchmarks/catch_frames/evidence/guard-gc-stress-summary.txt @@ -0,0 +1,16 @@ +6 moved-objects 27/86 copy-minor 27/86 + +byte-exact vs node 26.5.1: 602/602 cells (the parity property) +summary: PASS=431 UNVER=171 XFAIL=0 FAIL=0 + (pressure=8MB, node 26.5.1, target/release/perry) + UNVER = output matched but the arm was inert here; see #6942 / #6946 / #6950. +json: /root/js-throw-evidence/guard-gc-stress.json + +liveness gate (#7255): every arm must have exercised what it claims + default requires=scavenge live 48/86 counter=2501055 live + safepoint_minor requires=scavenge live 48/86 counter=2499989 live + evac_minor requires=move live 62/86 counter=2568821 live + verify_evac requires=scavenge live 53/86 counter=2861200 live + force_verify requires=move live 86/86 counter=3726742 live + cons_scan_off requires=scavenge live 48/86 counter=2534812 live + shipped_default requires=none live 0/86 counter=0 n/a diff --git a/benchmarks/catch_frames/evidence/guard-gc-stress.json.gz b/benchmarks/catch_frames/evidence/guard-gc-stress.json.gz new file mode 100644 index 0000000000..8f883c8a85 Binary files /dev/null and b/benchmarks/catch_frames/evidence/guard-gc-stress.json.gz differ diff --git a/benchmarks/catch_frames/evidence/host-warnings-comparison.json b/benchmarks/catch_frames/evidence/host-warnings-comparison.json new file mode 100644 index 0000000000..4c0680be68 --- /dev/null +++ b/benchmarks/catch_frames/evidence/host-warnings-comparison.json @@ -0,0 +1,20 @@ +[ + { + "side": "baseline", + "exit_code": 101, + "errors": [ + "error: function `registered_extern_handle` is never used", + "error: function `wasm_memory_descriptor_maximum` is never used", + "error: could not compile `perry-runtime` (lib) due to 2 previous errors" + ] + }, + { + "side": "fix", + "exit_code": 101, + "errors": [ + "error: function `registered_extern_handle` is never used", + "error: function `wasm_memory_descriptor_maximum` is never used", + "error: could not compile `perry-runtime` (lib) due to 2 previous errors" + ] + } +] diff --git a/benchmarks/catch_frames/evidence/instructions.json b/benchmarks/catch_frames/evidence/instructions.json new file mode 100644 index 0000000000..e06fefd73c --- /dev/null +++ b/benchmarks/catch_frames/evidence/instructions.json @@ -0,0 +1,167 @@ +{ + "date": "2026-09-14", + "baseline_commit": "eb13fa188d", + "instruction_event": "instructions:u", + "stat_repeats": 3, + "record_period": 1000003, + "record_call_graph": "dwarf", + "perf_script_inline_expansion": false, + "node_version": "26.5.1", + "workloads": { + "catch": { + "baseline": { + "instructions": 472151423.0, + "samples": 472, + "capture_samples": 15, + "transport_leaf_samples": 112, + "capture_percent": 3.1779661016949152 + }, + "candidate": { + "instructions": 351152420.0, + "samples": 351, + "capture_samples": 6, + "transport_leaf_samples": 43, + "capture_percent": 1.7094017094017093 + }, + "instruction_change_percent": -25.627160505243253, + "net_instructions_per_iteration": { + "baseline": 445.083186, + "candidate": 324.083431, + "change_percent": -27.18587419296491 + } + }, + "exec1": { + "baseline": { + "instructions": 31217404185.0, + "samples": 31217, + "capture_samples": 1559, + "transport_leaf_samples": 1162, + "capture_percent": 4.994073741871416 + }, + "candidate": { + "instructions": 30183325430.0, + "samples": 30182, + "capture_samples": 999, + "transport_leaf_samples": 1080, + "capture_percent": 3.309919819760122 + }, + "instruction_change_percent": -3.3125071798790873 + }, + "hoist": { + "baseline": { + "instructions": 13816754889.0, + "samples": 13816, + "capture_samples": 1342, + "transport_leaf_samples": 422, + "capture_percent": 9.713375796178344 + }, + "candidate": { + "instructions": 13266297479.0, + "samples": 13263, + "capture_samples": 447, + "transport_leaf_samples": 483, + "capture_percent": 3.3702782175978285 + }, + "instruction_change_percent": -3.9839847664825956, + "baseline_frame_pushes": 3500011 + }, + "json": { + "baseline": { + "instructions": 3182841469.0, + "samples": 3182, + "capture_samples": 0, + "transport_leaf_samples": 0, + "capture_percent": 0.0 + }, + "candidate": { + "instructions": 3171165684.0, + "samples": 3168, + "capture_samples": 0, + "transport_leaf_samples": 0, + "capture_percent": 0.0 + }, + "instruction_change_percent": -0.3668352669688013, + "baseline_frame_pushes": 6 + }, + "plain": { + "baseline": { + "instructions": 27068237.0, + "samples": 27, + "capture_samples": 0, + "transport_leaf_samples": 0, + "capture_percent": 0.0 + }, + "candidate": { + "instructions": 27068989.0, + "samples": 26, + "capture_samples": 0, + "transport_leaf_samples": 0, + "capture_percent": 0.0 + }, + "instruction_change_percent": 0.0027781639417412407 + }, + "promises": { + "baseline": { + "instructions": 1098667245.0, + "samples": 1098, + "capture_samples": 29, + "transport_leaf_samples": 3, + "capture_percent": 2.6411657559198543 + }, + "candidate": { + "instructions": 1077465852.0, + "samples": 1077, + "capture_samples": 4, + "transport_leaf_samples": 0, + "capture_percent": 0.3714020427112349 + }, + "instruction_change_percent": -1.929737424728628, + "baseline_frame_pushes": 100007 + }, + "unwind": { + "baseline": { + "instructions": 366173508.0, + "samples": 364, + "capture_samples": 99, + "transport_leaf_samples": 0, + "capture_percent": 27.197802197802197 + }, + "candidate": { + "instructions": 244175238.0, + "samples": 244, + "capture_samples": 129, + "transport_leaf_samples": 0, + "capture_percent": 52.868852459016395 + }, + "instruction_change_percent": -33.317066181641955, + "net_instructions_per_iteration": { + "baseline": 339.105271, + "candidate": 217.106249, + "change_percent": -35.97674009614554 + } + } + }, + "artifacts": { + "baseline": { + "libperry_runtime.a": "66f3bcedf157f483a14bbb7023a0edec65f7ee91b520f20910f0b404266a4d33", + "libperry_stdlib.a": "4b1898b5fff251ec3244a11adce07893cd86dd28d6d352c6fd2b80c811e4d8d9", + "perry": "46cea1d5ccc54556d0d301641567cf6879a794d2707ee01ba25fa8f60cbf48ab" + }, + "candidate": { + "perry": "08a1aff537958e855fbdf5b9f25ccd22deb3cc3fc1a97c2f3063ed98dd2cbd9d", + "libperry_runtime.a": "2c0b05fbad66bb569241ccf742831504c9ef8c4819a31845c3d92c70d0efaa72", + "libperry_stdlib.a": "86ffd21fcd187c748661fd85015d6824998a43f70610b98b98cb7e8fd86a0206" + } + }, + "folded_stack_format": "semicolon-delimited frames, space, sample count", + "host": { + "cpu": "AMD Ryzen 7 7700X 8-Core Processor", + "logical_cpus": 16, + "kernel": "Linux 6.17.0-23-generic", + "perf": "6.17.13", + "rustc": "1.100.0-nightly (f7d782a3b 2026-08-19)", + "llvm": 22 + }, + "source_provenance_note": "Final measurements and full integration use the same frozen three-package compiler/archive build, including the constructor identity guard. Extra GC extension builds were made only after freezing these artifacts.", + "reviewed_crates_patch_sha256": "1f75da852b8a0f725da2977548ff194ca7833f2e76504d3c389d25bcbc6d43b6" +} diff --git a/benchmarks/catch_frames/evidence/integration-allowlist-diff.json b/benchmarks/catch_frames/evidence/integration-allowlist-diff.json new file mode 100644 index 0000000000..fe2a88b974 --- /dev/null +++ b/benchmarks/catch_frames/evidence/integration-allowlist-diff.json @@ -0,0 +1,16 @@ +{ + "platform": "linux", + "unlisted_failures": [ + "test_bun_plugin", + "test_dynamic_import_data_10104", + "test_gap_2899_2779_2777_static_helpers", + "test_gap_disposablestack_2875", + "test_gap_iterator_prototype_next_patch", + "test_issue_10155_textfield_singleline", + "test_issue_336_class_keys_collision", + "test_sock_write_map" + ], + "stale": [], + "schema": [], + "adjudicated": 116 +} diff --git a/benchmarks/catch_frames/evidence/integration-checks.json b/benchmarks/catch_frames/evidence/integration-checks.json new file mode 100644 index 0000000000..4ef81fa642 --- /dev/null +++ b/benchmarks/catch_frames/evidence/integration-checks.json @@ -0,0 +1,77 @@ +[ + { + "check": "merge", + "command": [ + "python3", + "scripts/parity_report_merge.py", + "--expect", + "4", + "--output", + "/root/js-throw-evidence/final-integration/parity.json", + "/root/js-throw-evidence/full-parity-1.json", + "/root/js-throw-evidence/full-parity-2.json", + "/root/js-throw-evidence/full-parity-3.json", + "/root/js-throw-evidence/full-parity-4.json" + ], + "exit_code": 0 + }, + { + "check": "known-failures", + "command": [ + "python3", + "scripts/parity_known_failures.py", + "--report", + "/root/js-throw-evidence/final-integration/parity.json", + "--platform", + "linux" + ], + "exit_code": 1 + }, + { + "check": "gap-snapshot", + "command": [ + "python3", + "scripts/gap_snapshot.py", + "check", + "--report", + "/root/js-throw-evidence/final-integration/gap.json", + "--snapshot", + "test-parity/gap_snapshot.json" + ], + "exit_code": 1 + }, + { + "check": "threshold", + "command": [ + "python3", + "scripts/parity_threshold_gate.py", + "--check", + "--report", + "/root/js-throw-evidence/final-integration/parity.json", + "--output-json", + "/root/js-throw-evidence/final-integration/threshold.json", + "--output-md", + "/root/js-throw-evidence/final-integration/threshold.md" + ], + "exit_code": 0 + }, + { + "check": "matrix-trend", + "command": [ + "python3", + "scripts/parity_matrix_trend.py", + "--check", + "--report", + "/root/js-throw-evidence/final-integration/parity.json", + "--platform", + "linux", + "--output-dir", + "/root/js-throw-evidence/final-integration/output", + "--output-json", + "/root/js-throw-evidence/final-integration/matrix-trend.json", + "--output-md", + "/root/js-throw-evidence/final-integration/matrix-trend.md" + ], + "exit_code": 0 + } +] diff --git a/benchmarks/catch_frames/evidence/integration-coverage.json b/benchmarks/catch_frames/evidence/integration-coverage.json new file mode 100644 index 0000000000..e987328c64 --- /dev/null +++ b/benchmarks/catch_frames/evidence/integration-coverage.json @@ -0,0 +1,26 @@ +{ + "expected_all": 1633, + "reported_all": 1633, + "expected_gap": 785, + "reported_gap": 785, + "parity_summary": { + "parity_pass": 1472, + "parity_fail": 93, + "compile_fail": 29, + "crash_fail": 2, + "node_fail": 5, + "skipped": 32, + "total_run": 1567, + "parity_percentage": 93.9 + }, + "gap_summary": { + "parity_pass": 776, + "parity_fail": 9, + "compile_fail": 0, + "crash_fail": 0, + "node_fail": 0, + "skipped": 0, + "total_run": 785, + "parity_percentage": 98.8 + } +} diff --git a/benchmarks/catch_frames/evidence/integration-gap-snapshot.log b/benchmarks/catch_frames/evidence/integration-gap-snapshot.log new file mode 100644 index 0000000000..042bd5979d --- /dev/null +++ b/benchmarks/catch_frames/evidence/integration-gap-snapshot.log @@ -0,0 +1,9 @@ + +REGRESSIONS — these were expected to pass: + - test_gap_2899_2779_2777_static_helpers: pass -> parity_fail + - test_gap_disposablestack_2875: pass -> parity_fail + - test_gap_iterator_prototype_next_patch: pass -> parity_fail + +Fix the regressions, then accept the rest with: + UPDATE_SNAPSHOT=1 ./scripts/run_gap_tests.sh +and commit test-parity/gap_snapshot.json. diff --git a/benchmarks/catch_frames/evidence/integration-known-failures.log b/benchmarks/catch_frames/evidence/integration-known-failures.log new file mode 100644 index 0000000000..e99f95caa5 --- /dev/null +++ b/benchmarks/catch_frames/evidence/integration-known-failures.log @@ -0,0 +1,9 @@ +NEW FAILURES on linux (not allowed for this platform): + - test_bun_plugin + - test_dynamic_import_data_10104 + - test_gap_2899_2779_2777_static_helpers + - test_gap_disposablestack_2875 + - test_gap_iterator_prototype_next_patch + - test_issue_10155_textfield_singleline + - test_issue_336_class_keys_collision + - test_sock_write_map diff --git a/benchmarks/catch_frames/evidence/integration-matrix-trend.md b/benchmarks/catch_frames/evidence/integration-matrix-trend.md new file mode 100644 index 0000000000..df9bf091c1 --- /dev/null +++ b/benchmarks/catch_frames/evidence/integration-matrix-trend.md @@ -0,0 +1,130 @@ +# Parity Matrix Trend + +| module | status | known | node_lines | perry_lines | diff_lines | +| --- | --- | --- | ---: | ---: | ---: | +| 684_type_only_namespace_collision | pass | no | 2 | 2 | 0 | +| 9023_transitive_class_collections | pass | no | 4 | 4 | 0 | +| 9023_transitive_class_defaults | pass | no | 6 | 6 | 0 | +| argon2 | pass | no | 19 | 7 | 26 | +| array_property_index_set_4150 | pass | no | 1 | 1 | 0 | +| assert | pass | no | 39 | 39 | 0 | +| async_context | pass | no | 49 | 49 | 0 | +| async_gen_await_in_conditional_6728 | pass | no | 4 | 4 | 0 | +| async_hooks | pass | no | 21 | 21 | 0 | +| async_local_storage | pass | no | 9 | 9 | 0 | +| async_semantic_3583 | pass | no | 7 | 7 | 0 | +| async_try_catch_after_await_4036 | pass | no | 7 | 7 | 0 | +| buffer | pass | no | 137 | 137 | 0 | +| builtin_static_name_length_3655 | pass | no | 1 | 1 | 0 | +| chained_cross_module_getter | pass | no | 2 | 2 | 0 | +| child_process | pass | no | 39 | 39 | 0 | +| class_expr_capture_refresh_6604 | pass | no | 7 | 7 | 0 | +| class_field_async_arrow_6728 | pass | no | 7 | 7 | 0 | +| cluster | parity_fail | yes | 17 | 17 | 4 | +| commander | parity_fail | yes | 19 | 8 | 27 | +| cron | parity_fail | yes | 8 | 7 | 15 | +| cross_module_getter | pass | no | 3 | 3 | 0 | +| crypto | parity_fail | yes | 51 | 51 | 6 | +| ctor_method_self_bind_not_field | pass | no | 1 | 1 | 0 | +| ctor_return_override_standalone | pass | no | 1 | 1 | 0 | +| date_fns | parity_fail | yes | 19 | 13 | 32 | +| dayjs | parity_fail | yes | 26 | 26 | 22 | +| decimal | parity_fail | yes | 19 | 30 | 49 | +| defineproperty_prototype_getter_this | pass | no | 1 | 1 | 0 | +| derived_constructor_arrow_new_4150 | pass | no | 1 | 1 | 0 | +| descriptor_decode | pass | no | 14 | 14 | 0 | +| destructuring_iterator_assignment | pass | no | 1 | 1 | 0 | +| dgram | pass | no | 38 | 38 | 0 | +| diagnostics_channel | pass | no | 23 | 23 | 0 | +| dns | pass | no | 110 | 110 | 0 | +| dns_promises | pass | no | 76 | 76 | 0 | +| dotenv | parity_fail | yes | 19 | 9 | 28 | +| effect_number_isfinite_property_path | pass | no | 1 | 1 | 0 | +| ethers | pass | no | 21 | 9 | 28 | +| export_const_static_field_alias | pass | no | 1 | 1 | 0 | +| fetch_binary_body_round_trip | pass | no | 4 | 4 | 0 | +| function_bind_value_reflect_apply | pass | no | 1 | 1 | 0 | +| headers_handle_field_probe_segfault | pass | no | 1 | 1 | 0 | +| headers_iterator_handle_segfault | pass | no | 1 | 1 | 0 | +| http | pass | no | 162 | 162 | 0 | +| http2 | pass | no | 51 | 51 | 0 | +| https | pass | no | 23 | 23 | 0 | +| imported_function_static_property | pass | no | 1 | 1 | 0 | +| imported_inherited_accessor_datatexture | pass | no | 1 | 1 | 0 | +| inherited_ctor_arg_forwarding | pass | no | 1 | 1 | 0 | +| inline_closure_capturing_local | pass | no | 1 | 1 | 0 | +| lodash | parity_fail | yes | 19 | 5 | 24 | +| lru_cache | parity_fail | yes | 19 | 15 | 34 | +| map_set_semantics_3989 | pass | no | 1 | 1 | 0 | +| math_random_bind_4120 | pass | no | 3 | 3 | 0 | +| method_value_snapshot_bind | pass | no | 13 | 1 | 14 | +| module | pass | no | 38 | 38 | 0 | +| moment | parity_fail | yes | 29 | 2 | 31 | +| multi_await_throw_no_orphan_rejection_6728 | pass | no | 2 | 2 | 0 | +| namespace_const_cross_module | pass | no | 27 | 1 | 28 | +| namespace_import_enumerable | pass | no | 1 | 1 | 0 | +| namespace_reexport_binding | pass | no | 1 | 1 | 0 | +| nanoid | parity_fail | yes | 19 | 9 | 28 | +| native_class_semantics_reduced | pass | no | 1 | 1 | 0 | +| native_value_profile | pass | no | 19 | 1 | 20 | +| net | pass | no | 75 | 75 | 0 | +| new_array_index_set_4150 | pass | no | 1 | 1 | 0 | +| numeric_enum_reverse_mapping_4509 | pass | no | 24 | 1 | 25 | +| object_assign_function_source | pass | no | 1 | 1 | 0 | +| object_ctor_inherited_proto_methods | pass | no | 1 | 1 | 0 | +| object_literal_getter_this_binding | pass | no | 1 | 1 | 0 | +| object_shorthand_imported_binding | pass | no | 1 | 1 | 0 | +| optional_chain_double_member_call | pass | no | 6 | 6 | 0 | +| os | pass | no | 10 | 10 | 0 | +| path | pass | no | 42 | 42 | 0 | +| perf_hooks | pass | no | 32 | 32 | 0 | +| process | pass | no | 60 | 60 | 0 | +| proxy_field_ic_miss | pass | no | 1 | 1 | 0 | +| proxy_fused_method_call | pass | no | 1 | 1 | 0 | +| proxy_getprototypeof_instanceof | pass | no | 1 | 1 | 0 | +| proxy_symbol_property | pass | no | 1 | 1 | 0 | +| querystring | pass | no | 16 | 16 | 0 | +| readline | pass | no | 19 | 19 | 0 | +| readline_promises | pass | no | 4 | 4 | 0 | +| regex_has_indices | pass | no | 10 | 10 | 0 | +| regex_replace_fn_lookahead | pass | no | 1 | 1 | 0 | +| request_subclass_body | pass | no | 7 | 7 | 0 | +| request_subclass_stream_body | pass | no | 13 | 4 | 11 | +| sqlite | pass | no | 51 | 51 | 0 | +| sqlite_backup_file | pass | no | 19 | 19 | 0 | +| sqlite_custom_functions_authorizers | pass | no | 34 | 34 | 0 | +| sqlite_database_sync_core | pass | no | 41 | 41 | 0 | +| sqlite_database_sync_extension_controls | pass | no | 12 | 12 | 0 | +| sqlite_sessions_constants | pass | no | 33 | 33 | 0 | +| sqlite_sqltagstore | pass | no | 21 | 21 | 0 | +| sqlite_statement_sync_core | pass | no | 45 | 45 | 0 | +| stream | pass | no | 80 | 80 | 0 | +| stream_consumers | pass | no | 9 | 9 | 0 | +| stream_promises | pass | no | 4 | 4 | 0 | +| stream_web | crash | yes | 43 | 1 | 44 | +| streams_byob | pass | no | 14 | 14 | 0 | +| string_append_surrogate_repair | pass | no | 7 | 7 | 0 | +| string_decoder | pass | no | 12 | 12 | 0 | +| subclass_builtin_field_init | pass | no | 1 | 1 | 0 | +| subclass_own_field_init_after_super | pass | no | 1 | 1 | 0 | +| sys | pass | no | 7 | 7 | 2 | +| test | pass | no | 80 | 80 | 0 | +| timers | pass | no | 20 | 20 | 0 | +| timers_promises | pass | no | 3 | 3 | 0 | +| tls | pass | no | 17 | 17 | 0 | +| try_block_no_leak_on_return | pass | no | 2 | 2 | 0 | +| try_catch_across_await | pass | no | 5 | 5 | 0 | +| undici_from_char_code_apply | pass | no | 1 | 1 | 0 | +| url | pass | no | 59 | 57 | 2 | +| user_method_named_sort | pass | no | 1 | 1 | 0 | +| util | parity_fail | yes | 275 | 283 | 154 | +| uuid | parity_fail | yes | 19 | 16 | 35 | +| v8_lifecycle | pass | no | 19 | 19 | 0 | +| validator | parity_fail | yes | 19 | 2 | 21 | +| vm | pass | no | 2 | 2 | 0 | +| webassembly_graceful_fail_default | pass | no | 5 | 8 | 11 | +| worker_threads | pass | no | 35 | 35 | 0 | +| xmod_empty_imported_derived_arg_forward | pass | no | 7 | 7 | 0 | +| xmod_imported_multilevel_ctor_state | pass | no | 8 | 8 | 0 | +| xmod_imported_zero_arg_super | pass | no | 4 | 4 | 0 | +| zlib | pass | no | 92 | 92 | 0 | diff --git a/benchmarks/catch_frames/evidence/integration-threshold.md b/benchmarks/catch_frames/evidence/integration-threshold.md new file mode 100644 index 0000000000..d72c87726c --- /dev/null +++ b/benchmarks/catch_frames/evidence/integration-threshold.md @@ -0,0 +1,133 @@ +# Parity Threshold Gate + +| category | parity | threshold | pass | parity_fail | compile_fail | node_fail | skipped | source | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- | +| legacy/gap | 98.9% | 98.5% | 776 | 9 | 0 | 0 | 0 | configured | +| legacy/issue | 83.7% | 81.0% | 307 | 43 | 17 | 1 | 10 | configured | +| legacy/other | 88.0% | 87.5% | 279 | 26 | 12 | 4 | 22 | configured | +| parity/684_type_only_namespace_collision | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/9023_transitive_class_collections | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/9023_transitive_class_defaults | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/argon2 | 100.0% | 0.0% | 1 | 0 | 0 | 0 | 0 | configured | +| parity/array_property_index_set_4150 | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/assert | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/async_context | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/async_gen_await_in_conditional_6728 | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/async_hooks | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/async_local_storage | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/async_semantic_3583 | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/async_try_catch_after_await_4036 | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/buffer | 100.0% | 0.0% | 1 | 0 | 0 | 0 | 0 | configured | +| parity/builtin_static_name_length_3655 | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/chained_cross_module_getter | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/child_process | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/class_expr_capture_refresh_6604 | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/class_field_async_arrow_6728 | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/cluster | 0.0% | 0.0% | 0 | 1 | 0 | 0 | 0 | configured | +| parity/commander | 0.0% | 0.0% | 0 | 1 | 0 | 0 | 0 | configured | +| parity/cron | 0.0% | 0.0% | 0 | 1 | 0 | 0 | 0 | configured | +| parity/cross_module_getter | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/crypto | 0.0% | 0.0% | 0 | 1 | 0 | 0 | 0 | configured | +| parity/ctor_method_self_bind_not_field | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/ctor_return_override_standalone | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/date_fns | 0.0% | 0.0% | 0 | 1 | 0 | 0 | 0 | configured | +| parity/dayjs | 0.0% | 0.0% | 0 | 1 | 0 | 0 | 0 | configured | +| parity/decimal | 0.0% | 0.0% | 0 | 1 | 0 | 0 | 0 | configured | +| parity/defineproperty_prototype_getter_this | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/derived_constructor_arrow_new_4150 | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/descriptor_decode | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/destructuring_iterator_assignment | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/dgram | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/diagnostics_channel | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/dns | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/dns_promises | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/dotenv | 0.0% | 0.0% | 0 | 1 | 0 | 0 | 0 | configured | +| parity/effect_number_isfinite_property_path | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/ethers | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/export_const_static_field_alias | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/fetch_binary_body_round_trip | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/function_bind_value_reflect_apply | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/headers_handle_field_probe_segfault | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/headers_iterator_handle_segfault | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/http | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | configured | +| parity/http2 | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | configured | +| parity/https | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | configured | +| parity/imported_function_static_property | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/imported_inherited_accessor_datatexture | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/inherited_ctor_arg_forwarding | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/inline_closure_capturing_local | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/lodash | 0.0% | 0.0% | 0 | 1 | 0 | 0 | 0 | configured | +| parity/lru_cache | 0.0% | 0.0% | 0 | 1 | 0 | 0 | 0 | configured | +| parity/map_set_semantics_3989 | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/math_random_bind_4120 | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/method_value_snapshot_bind | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/module | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/moment | 0.0% | 0.0% | 0 | 1 | 0 | 0 | 0 | configured | +| parity/multi_await_throw_no_orphan_rejection_6728 | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/namespace_const_cross_module | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/namespace_import_enumerable | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/namespace_reexport_binding | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/nanoid | 0.0% | 0.0% | 0 | 1 | 0 | 0 | 0 | configured | +| parity/native_class_semantics_reduced | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/native_value_profile | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/net | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/new_array_index_set_4150 | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/numeric_enum_reverse_mapping_4509 | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/object_assign_function_source | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/object_ctor_inherited_proto_methods | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/object_literal_getter_this_binding | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/object_shorthand_imported_binding | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/optional_chain_double_member_call | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/os | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/path | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/perf_hooks | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/process | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/proxy_field_ic_miss | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/proxy_fused_method_call | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/proxy_getprototypeof_instanceof | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/proxy_symbol_property | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/querystring | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/readline | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/readline_promises | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/regex_has_indices | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/regex_replace_fn_lookahead | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/request_subclass_body | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/request_subclass_stream_body | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/sqlite | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | configured | +| parity/sqlite_backup_file | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/sqlite_custom_functions_authorizers | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/sqlite_database_sync_core | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/sqlite_database_sync_extension_controls | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/sqlite_sessions_constants | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/sqlite_sqltagstore | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/sqlite_statement_sync_core | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/stream | 100.0% | 0.0% | 1 | 0 | 0 | 0 | 0 | configured | +| parity/stream_consumers | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/stream_promises | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/stream_web | | 0.0% | 0 | 0 | 0 | 0 | 0 | configured | +| parity/streams_byob | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/string_append_surrogate_repair | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/string_decoder | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/subclass_builtin_field_init | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/subclass_own_field_init_after_super | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/sys | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/test | 100.0% | 0.0% | 1 | 0 | 0 | 0 | 0 | configured | +| parity/timers | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/timers_promises | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/tls | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/try_block_no_leak_on_return | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/try_catch_across_await | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/undici_from_char_code_apply | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/url | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/user_method_named_sort | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/util | 0.0% | 0.0% | 0 | 1 | 0 | 0 | 0 | configured | +| parity/uuid | 0.0% | 0.0% | 0 | 1 | 0 | 0 | 0 | configured | +| parity/v8_lifecycle | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/validator | 0.0% | 0.0% | 0 | 1 | 0 | 0 | 0 | configured | +| parity/vm | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/webassembly_graceful_fail_default | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/worker_threads | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/xmod_empty_imported_derived_arg_forward | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/xmod_imported_multilevel_ctor_state | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/xmod_imported_zero_arg_super | 100.0% | 100.0% | 1 | 0 | 0 | 0 | 0 | default | +| parity/zlib | 100.0% | 0.0% | 1 | 0 | 0 | 0 | 0 | configured | diff --git a/benchmarks/catch_frames/evidence/integration-unlisted-baseline-comparison.json b/benchmarks/catch_frames/evidence/integration-unlisted-baseline-comparison.json new file mode 100644 index 0000000000..170b095810 --- /dev/null +++ b/benchmarks/catch_frames/evidence/integration-unlisted-baseline-comparison.json @@ -0,0 +1,42 @@ +[ + { + "test": "test_bun_plugin", + "baseline": "parity_fail", + "candidate": "parity_fail" + }, + { + "test": "test_dynamic_import_data_10104", + "baseline": "parity_fail", + "candidate": "parity_fail" + }, + { + "test": "test_gap_2899_2779_2777_static_helpers", + "baseline": "parity_fail", + "candidate": "parity_fail" + }, + { + "test": "test_gap_disposablestack_2875", + "baseline": "parity_fail", + "candidate": "parity_fail" + }, + { + "test": "test_gap_iterator_prototype_next_patch", + "baseline": "parity_fail", + "candidate": "parity_fail" + }, + { + "test": "test_issue_10155_textfield_singleline", + "baseline": "compile_fail", + "candidate": "compile_fail" + }, + { + "test": "test_issue_336_class_keys_collision", + "baseline": "parity_fail", + "candidate": "parity_fail" + }, + { + "test": "test_sock_write_map", + "baseline": "pass", + "candidate": "parity_fail" + } +] diff --git a/benchmarks/catch_frames/evidence/local-checks.json b/benchmarks/catch_frames/evidence/local-checks.json new file mode 100644 index 0000000000..b027f41efc --- /dev/null +++ b/benchmarks/catch_frames/evidence/local-checks.json @@ -0,0 +1,18 @@ +[ + { + "check": "runtime-tests", + "exit": 0 + }, + { + "check": "regex-off", + "exit": 0 + }, + { + "check": "clippy", + "exit": 101 + }, + { + "check": "lint", + "exit": 1 + } +] diff --git a/benchmarks/catch_frames/evidence/mac-gc-baseline-pinned-check.log b/benchmarks/catch_frames/evidence/mac-gc-baseline-pinned-check.log new file mode 100644 index 0000000000..5bce2c49cc --- /dev/null +++ b/benchmarks/catch_frames/evidence/mac-gc-baseline-pinned-check.log @@ -0,0 +1,235 @@ +gc-ratchet: FAILED + - 01_nursery_churn: heap_used_bytes 212,168 -> 438,744 (+106.79%), allowance 65,536 [increase] + - 01_nursery_churn: copied_objects 4,977 -> 6,690 (+34.42%), allowance 249 [either] + - 01_nursery_churn: copied_bytes 234,536 -> 466,392 (+98.86%), allowance 65,536 [either] + - 02_survivor_promotion: copied_objects 34,550 -> 37,366 (+8.15%), allowance 1,728 [either] + - 02_survivor_promotion: copied_bytes 1,450,992 -> 1,690,264 (+16.49%), allowance 72,550 [either] + - 04_dead_after_deep_stack: copied_objects 3,622 -> 2,666 (-26.39%), allowance 181 [either] + - 05_closure_capture: copied_objects 20,079 -> 14,285 (-28.86%), allowance 1,004 [either] + - 05_closure_capture: copied_bytes 743,384 -> 534,720 (-28.07%), allowance 65,536 [either] + - 05_closure_capture: promoted_objects 3,390 -> 6,154 (+81.53%), allowance 170 [either] + - 05_closure_capture: promoted_bytes 122,120 -> 221,624 (+81.48%), allowance 65,536 [either] + - 06_string_retention: copied_objects 298 -> 270 (-9.40%), allowance 16 [either] + - 07_array_grow_evacuate: heap_used_bytes 1,072,456 -> 1,211,232 (+12.94%), allowance 65,536 [increase] + - 07_array_grow_evacuate: copied_objects 4,991 -> 6,702 (+34.28%), allowance 250 [either] + - 07_array_grow_evacuate: promoted_objects 4,879 -> 6,154 (+26.13%), allowance 244 [either] + - 07_array_grow_evacuate: promoted_bytes 615,696 -> 841,048 (+36.60%), allowance 65,536 [either] + - 09_try_catch_roots: heap_used_bytes 212,168 -> 443,520 (+109.04%), allowance 65,536 [increase] + - 09_try_catch_roots: copied_objects 4,962 -> 6,673 (+34.48%), allowance 248 [either] + - 09_try_catch_roots: copied_bytes 230,352 -> 462,024 (+100.57%), allowance 65,536 [either] + - 09_try_catch_roots: freed_bytes 11,112,472 -> 10,023,568 (-9.80%), allowance 1,048,576 [either] + - 10_store_receiver_across_alloc: heap_used_bytes 220,384 -> 446,776 (+102.73%), allowance 65,536 [increase] + - 10_store_receiver_across_alloc: heap_total_bytes 16,777,216 -> 22,020,096 (+31.25%), allowance 1,048,576 [increase] + - 10_store_receiver_across_alloc: copied_objects 18,660 -> 14,625 (-21.62%), allowance 933 [either] + - 10_store_receiver_across_alloc: promoted_objects 4,722 -> 6,107 (+29.33%), allowance 236 [either] + - 10_store_receiver_across_alloc: promoted_bytes 228,440 -> 446,368 (+95.40%), allowance 65,536 [either] + - 11_collect_at_depth: heap_used_bytes 212,168 -> 444,720 (+109.61%), allowance 65,536 [increase] + - 11_collect_at_depth: copied_objects 4,975 -> 6,686 (+34.39%), allowance 249 [either] + - 11_collect_at_depth: copied_bytes 230,872 -> 462,544 (+100.35%), allowance 65,536 [either] + - 11_collect_at_depth: freed_bytes 10,062,936 -> 8,974,576 (-10.82%), allowance 1,048,576 [either] + - 12_large_live_set: copied_objects 59,405 -> 104,050 (+75.15%), allowance 2,970 [either] + - 12_large_live_set: copied_bytes 2,327,008 -> 4,162,008 (+78.86%), allowance 116,350 [either] +## GC ratchet + +- Profile: `shared_ci` +- Baseline commit: `f2ab19447194eeaa3cb47d968877745614024451` +- Baseline captured: 2026-08-16T09:38:07+00:00 on `darwin-arm64` +- Baseline host: Apple M1, 8 cores, load at capture 2.18 + +Only rows marked gating can fail the job. Non-gating rows are recorded so a +drift that is real but unmeasurable on this runner is still visible. + +Probes pinned under a non-default collector configuration: + +- `13_large_eden_survivors` — `PERRY_GC_SCAVENGE_NURSERY_MB=64` +- `14_grow_then_churn` — `PERRY_GC_MAJOR_PACING_FLOOR_MB=1 PERRY_GC_SCAVENGE_NURSERY_MB=1` + +Accepted deterministic baseline deltas: + +- `46` cells measured at `3e3e652e007ac853bf934daa86fa36c870b0e3e2` (2026-09-06T04:12:57+00:00) +- `06e1ab349c8c6e40587bb3ce2ee9f1c58b20522b` / PR #8657 — measured: array growth targets keep their generation +- `22c07ac5b44c5ff6a988257067110c7451da0223` / PR #9833 — measured: included receiver-store workload repair +- `5196d830fd0689937db94598d94662d531d4ee92` / PR #9706 — bracketed: Sep-4 shape storage and array packing +- `aa8d2ade03970ac7eb2065f3eb0e18bb4d2df1c7` / PR #9628 — bracketed: Sep-3 liveness and retention repair +- `b3e88f1fb3378fe30b065ed328b73ea0527ab18a` / PR #8806 — measured: cache exact shape transitions +- `d923b8dcf08f2afed71777c33423362411039cf0` / PR #9373 — measured: short-concat memo changes large-Eden occupancy +- `e18b24c420c7c2f389607537618699ffc6aa9d1d` / PR #8900 — measured: weak shape-transition target edges +- `e2e6f4a1cfded098ca05afe7e1b4e5ce783a5140` / PR #8313 — measured: smaller common objects +- `fc8c9e9a15902f889df2b1aeb280ca6223f6948a` / PR #8887 — bracketed: Aug-27/28 allocation and rooting specializations + +| Probe | Metric | Baseline | Current | Δ | Allowance | Gating | Status | +|-------|--------|---------:|--------:|---:|----------:|:------:|--------| +| `01_nursery_churn` | heap_used_bytes | 212,168 | 438,744 | +106.79% | 65,536 | yes | REGRESSION | +| `01_nursery_churn` | heap_total_bytes | 16,777,216 | 14,680,064 | -12.50% | 1,048,576 | yes | improvement | +| `01_nursery_churn` | minor_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `01_nursery_churn` | step_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `01_nursery_churn` | copied_objects | 4,977 | 6,690 | +34.42% | 249 | yes | REGRESSION | +| `01_nursery_churn` | copied_bytes | 234,536 | 466,392 | +98.86% | 65,536 | yes | REGRESSION | +| `01_nursery_churn` | promoted_objects | 0 | 0 | - | 16 | yes | ok | +| `01_nursery_churn` | promoted_bytes | 0 | 0 | - | 65,536 | yes | ok | +| `01_nursery_churn` | freed_bytes | 11,299,648 | 11,067,816 | -2.05% | 1,048,576 | yes | ok | +| `01_nursery_churn` | rss_bytes | 28,950,528 | 28,508,160 | -1.53% | 8,388,608 | no | ok | +| `01_nursery_churn` | peak_rss_bytes | 29,491,200 | 29,114,368 | -1.28% | 8,388,608 | no | ok | +| `01_nursery_churn` | wall_ms | 54 | 43 | -20.11% | 100 | no | ok | +| `02_survivor_promotion` | heap_used_bytes | 2,656,440 | 2,562,832 | -3.52% | 65,536 | yes | improvement | +| `02_survivor_promotion` | heap_total_bytes | 16,777,216 | 15,728,640 | -6.25% | 1,048,576 | yes | ok | +| `02_survivor_promotion` | minor_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `02_survivor_promotion` | step_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `02_survivor_promotion` | copied_objects | 34,550 | 37,366 | +8.15% | 1,728 | yes | REGRESSION | +| `02_survivor_promotion` | copied_bytes | 1,450,992 | 1,690,264 | +16.49% | 72,550 | yes | REGRESSION | +| `02_survivor_promotion` | promoted_objects | 0 | 0 | - | 16 | yes | ok | +| `02_survivor_promotion` | promoted_bytes | 0 | 0 | - | 65,536 | yes | ok | +| `02_survivor_promotion` | freed_bytes | 9,754,128 | 8,795,408 | -9.83% | 1,048,576 | yes | ok | +| `02_survivor_promotion` | rss_bytes | 32,522,240 | 31,358,976 | -3.58% | 8,388,608 | no | ok | +| `02_survivor_promotion` | peak_rss_bytes | 33,095,680 | 31,932,416 | -3.51% | 8,388,608 | no | ok | +| `02_survivor_promotion` | wall_ms | 57 | 48 | -15.94% | 100 | no | ok | +| `03_cross_gen_writes` | heap_used_bytes | 528 | 528 | +0.00% | 65,536 | yes | ok | +| `03_cross_gen_writes` | heap_total_bytes | 14,680,064 | 12,582,912 | -14.29% | 1,048,576 | yes | improvement | +| `03_cross_gen_writes` | minor_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `03_cross_gen_writes` | step_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `03_cross_gen_writes` | copied_objects | 4,107 | 4,107 | +0.00% | 205 | yes | ok | +| `03_cross_gen_writes` | copied_bytes | 229,824 | 197,056 | -14.26% | 65,536 | yes | ok | +| `03_cross_gen_writes` | promoted_objects | 0 | 0 | - | 16 | yes | ok | +| `03_cross_gen_writes` | promoted_bytes | 0 | 0 | - | 65,536 | yes | ok | +| `03_cross_gen_writes` | freed_bytes | 9,240,008 | 9,240,008 | +0.00% | 1,048,576 | yes | ok | +| `03_cross_gen_writes` | rss_bytes | 21,708,800 | 22,888,448 | +5.43% | 8,388,608 | no | ok | +| `03_cross_gen_writes` | peak_rss_bytes | 26,542,080 | 26,968,064 | +1.60% | 8,388,608 | no | ok | +| `03_cross_gen_writes` | wall_ms | 37 | 29 | -21.51% | 100 | no | ok | +| `04_dead_after_deep_stack` | heap_used_bytes | 536 | 536 | +0.00% | 65,536 | yes | ok | +| `04_dead_after_deep_stack` | heap_total_bytes | 20,971,520 | 18,874,368 | -10.00% | 1,048,576 | yes | improvement | +| `04_dead_after_deep_stack` | minor_cycles | 6 | 7 | +16.67% | 1 | yes | ok | +| `04_dead_after_deep_stack` | step_cycles | 6 | 7 | +16.67% | 1 | yes | ok | +| `04_dead_after_deep_stack` | copied_objects | 3,622 | 2,666 | -26.39% | 181 | yes | REGRESSION | +| `04_dead_after_deep_stack` | copied_bytes | 149,400 | 111,336 | -25.48% | 65,536 | yes | ok | +| `04_dead_after_deep_stack` | promoted_objects | 682 | 682 | +0.00% | 34 | yes | ok | +| `04_dead_after_deep_stack` | promoted_bytes | 440 | 27,320 | +6109.09% | 65,536 | yes | ok | +| `04_dead_after_deep_stack` | freed_bytes | 73,346,416 | 76,493,952 | +4.29% | 3,667,321 | yes | ok | +| `04_dead_after_deep_stack` | rss_bytes | 24,100,864 | 25,100,288 | +4.15% | 8,388,608 | no | ok | +| `04_dead_after_deep_stack` | peak_rss_bytes | 28,950,528 | 29,130,752 | +0.62% | 8,388,608 | no | ok | +| `04_dead_after_deep_stack` | wall_ms | 69 | 38 | -44.42% | 100 | no | ok | +| `05_closure_capture` | heap_used_bytes | 5,664 | 544 | -90.40% | 65,536 | yes | ok | +| `05_closure_capture` | heap_total_bytes | 14,680,064 | 14,680,064 | +0.00% | 1,048,576 | yes | ok | +| `05_closure_capture` | minor_cycles | 3 | 3 | +0.00% | 1 | yes | ok | +| `05_closure_capture` | step_cycles | 3 | 3 | +0.00% | 1 | yes | ok | +| `05_closure_capture` | copied_objects | 20,079 | 14,285 | -28.86% | 1,004 | yes | REGRESSION | +| `05_closure_capture` | copied_bytes | 743,384 | 534,720 | -28.07% | 65,536 | yes | REGRESSION | +| `05_closure_capture` | promoted_objects | 3,390 | 6,154 | +81.53% | 170 | yes | REGRESSION | +| `05_closure_capture` | promoted_bytes | 122,120 | 221,624 | +81.48% | 65,536 | yes | REGRESSION | +| `05_closure_capture` | freed_bytes | 26,862,320 | 27,897,872 | +3.86% | 1,343,116 | yes | ok | +| `05_closure_capture` | rss_bytes | 19,169,280 | 23,265,280 | +21.37% | 8,388,608 | no | ok | +| `05_closure_capture` | peak_rss_bytes | 24,248,320 | 26,853,376 | +10.74% | 8,388,608 | no | ok | +| `05_closure_capture` | wall_ms | 68 | 50 | -26.11% | 100 | no | ok | +| `06_string_retention` | heap_used_bytes | 212,640 | 1,072 | -99.50% | 65,536 | yes | improvement | +| `06_string_retention` | heap_total_bytes | 23,068,672 | 22,020,096 | -4.55% | 1,048,576 | yes | ok | +| `06_string_retention` | minor_cycles | 5 | 4 | -20.00% | 1 | yes | ok | +| `06_string_retention` | step_cycles | 5 | 4 | -20.00% | 1 | yes | ok | +| `06_string_retention` | copied_objects | 298 | 270 | -9.40% | 16 | yes | REGRESSION | +| `06_string_retention` | copied_bytes | 72,288 | 70,144 | -2.97% | 65,536 | yes | ok | +| `06_string_retention` | promoted_objects | 14 | 14 | +0.00% | 16 | yes | ok | +| `06_string_retention` | promoted_bytes | 1,072 | 1,072 | +0.00% | 65,536 | yes | ok | +| `06_string_retention` | freed_bytes | 68,136,816 | 68,136,816 | +0.00% | 3,406,841 | yes | ok | +| `06_string_retention` | rss_bytes | 32,964,608 | 28,459,008 | -13.67% | 8,388,608 | no | ok | +| `06_string_retention` | peak_rss_bytes | 33,488,896 | 33,030,144 | -1.37% | 8,388,608 | no | ok | +| `06_string_retention` | wall_ms | 76 | 46 | -39.42% | 100 | no | ok | +| `07_array_grow_evacuate` | heap_used_bytes | 1,072,456 | 1,211,232 | +12.94% | 65,536 | yes | REGRESSION | +| `07_array_grow_evacuate` | heap_total_bytes | 25,165,824 | 25,165,824 | +0.00% | 1,048,576 | yes | ok | +| `07_array_grow_evacuate` | minor_cycles | 5 | 5 | +0.00% | 1 | yes | ok | +| `07_array_grow_evacuate` | step_cycles | 5 | 5 | +0.00% | 1 | yes | ok | +| `07_array_grow_evacuate` | copied_objects | 4,991 | 6,702 | +34.28% | 250 | yes | REGRESSION | +| `07_array_grow_evacuate` | copied_bytes | 2,671,936 | 2,636,392 | -1.33% | 133,597 | no | ok | +| `07_array_grow_evacuate` | promoted_objects | 4,879 | 6,154 | +26.13% | 244 | yes | REGRESSION | +| `07_array_grow_evacuate` | promoted_bytes | 615,696 | 841,048 | +36.60% | 65,536 | yes | REGRESSION | +| `07_array_grow_evacuate` | freed_bytes | 83,727,056 | 83,570,704 | -0.19% | 4,186,353 | no | ok | +| `07_array_grow_evacuate` | rss_bytes | 34,029,568 | 37,879,808 | +11.31% | 8,388,608 | no | ok | +| `07_array_grow_evacuate` | peak_rss_bytes | 34,553,856 | 38,486,016 | +11.38% | 8,388,608 | no | ok | +| `07_array_grow_evacuate` | wall_ms | 144 | 50 | -65.05% | 100 | no | ok | +| `08_map_set_sidetables` | heap_used_bytes | 672 | 688 | +2.38% | 65,536 | yes | ok | +| `08_map_set_sidetables` | heap_total_bytes | 16,777,216 | 14,680,064 | -12.50% | 1,048,576 | yes | improvement | +| `08_map_set_sidetables` | minor_cycles | 5 | 6 | +20.00% | 1 | yes | ok | +| `08_map_set_sidetables` | step_cycles | 5 | 6 | +20.00% | 1 | yes | ok | +| `08_map_set_sidetables` | copied_objects | 2,489 | 2,461 | -1.12% | 124 | yes | ok | +| `08_map_set_sidetables` | copied_bytes | 144,384 | 98,472 | -31.80% | 65,536 | yes | ok | +| `08_map_set_sidetables` | promoted_objects | 15 | 14 | -6.67% | 16 | yes | ok | +| `08_map_set_sidetables` | promoted_bytes | 576 | 592 | +2.78% | 65,536 | yes | ok | +| `08_map_set_sidetables` | freed_bytes | 57,655,296 | 56,598,904 | -1.83% | 2,882,765 | yes | ok | +| `08_map_set_sidetables` | rss_bytes | 21,037,056 | 19,906,560 | -5.37% | 8,388,608 | no | ok | +| `08_map_set_sidetables` | peak_rss_bytes | 25,329,664 | 24,608,768 | -2.85% | 8,388,608 | no | ok | +| `08_map_set_sidetables` | wall_ms | 171 | 176 | +3.17% | 100 | no | ok | +| `09_try_catch_roots` | heap_used_bytes | 212,168 | 443,520 | +109.04% | 65,536 | yes | REGRESSION | +| `09_try_catch_roots` | heap_total_bytes | 14,680,064 | 14,680,064 | +0.00% | 1,048,576 | yes | ok | +| `09_try_catch_roots` | minor_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `09_try_catch_roots` | step_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `09_try_catch_roots` | copied_objects | 4,962 | 6,673 | +34.48% | 248 | yes | REGRESSION | +| `09_try_catch_roots` | copied_bytes | 230,352 | 462,024 | +100.57% | 65,536 | yes | REGRESSION | +| `09_try_catch_roots` | promoted_objects | 0 | 0 | - | 16 | yes | ok | +| `09_try_catch_roots` | promoted_bytes | 0 | 0 | - | 65,536 | yes | ok | +| `09_try_catch_roots` | freed_bytes | 11,112,472 | 10,023,568 | -9.80% | 1,048,576 | yes | REGRESSION | +| `09_try_catch_roots` | rss_bytes | 30,015,488 | 30,261,248 | +0.82% | 8,388,608 | no | ok | +| `09_try_catch_roots` | peak_rss_bytes | 30,457,856 | 30,736,384 | +0.91% | 8,388,608 | no | ok | +| `09_try_catch_roots` | wall_ms | 744 | 106 | -85.79% | 223 | no | improvement | +| `10_store_receiver_across_alloc` | heap_used_bytes | 220,384 | 446,776 | +102.73% | 65,536 | yes | REGRESSION | +| `10_store_receiver_across_alloc` | heap_total_bytes | 16,777,216 | 22,020,096 | +31.25% | 1,048,576 | yes | REGRESSION | +| `10_store_receiver_across_alloc` | minor_cycles | 9 | 8 | -11.11% | 1 | yes | ok | +| `10_store_receiver_across_alloc` | step_cycles | 9 | 8 | -11.11% | 1 | yes | ok | +| `10_store_receiver_across_alloc` | copied_objects | 18,660 | 14,625 | -21.62% | 933 | yes | REGRESSION | +| `10_store_receiver_across_alloc` | copied_bytes | 825,520 | 787,792 | -4.57% | 65,536 | yes | ok | +| `10_store_receiver_across_alloc` | promoted_objects | 4,722 | 6,107 | +29.33% | 236 | yes | REGRESSION | +| `10_store_receiver_across_alloc` | promoted_bytes | 228,440 | 446,368 | +95.40% | 65,536 | yes | REGRESSION | +| `10_store_receiver_across_alloc` | freed_bytes | 88,858,544 | 88,640,584 | -0.25% | 4,442,927 | yes | ok | +| `10_store_receiver_across_alloc` | rss_bytes | 24,756,224 | 35,045,376 | +41.56% | 8,388,608 | no | drift (informational) | +| `10_store_receiver_across_alloc` | peak_rss_bytes | 25,296,896 | 35,569,664 | +40.61% | 8,388,608 | no | drift (informational) | +| `10_store_receiver_across_alloc` | wall_ms | 43 | 159 | +268.47% | 100 | no | drift (informational) | +| `11_collect_at_depth` | heap_used_bytes | 212,168 | 444,720 | +109.61% | 65,536 | yes | REGRESSION | +| `11_collect_at_depth` | heap_total_bytes | 13,631,488 | 12,582,912 | -7.69% | 1,048,576 | yes | ok | +| `11_collect_at_depth` | minor_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `11_collect_at_depth` | step_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `11_collect_at_depth` | copied_objects | 4,975 | 6,686 | +34.39% | 249 | yes | REGRESSION | +| `11_collect_at_depth` | copied_bytes | 230,872 | 462,544 | +100.35% | 65,536 | yes | REGRESSION | +| `11_collect_at_depth` | promoted_objects | 0 | 0 | - | 16 | yes | ok | +| `11_collect_at_depth` | promoted_bytes | 0 | 0 | - | 65,536 | yes | ok | +| `11_collect_at_depth` | freed_bytes | 10,062,936 | 8,974,576 | -10.82% | 1,048,576 | yes | REGRESSION | +| `11_collect_at_depth` | rss_bytes | 26,525,696 | 26,656,768 | +0.49% | 8,388,608 | no | ok | +| `11_collect_at_depth` | peak_rss_bytes | 26,984,448 | 27,295,744 | +1.15% | 8,388,608 | no | ok | +| `11_collect_at_depth` | wall_ms | 48 | 46 | -4.41% | 100 | no | ok | +| `12_large_live_set` | heap_used_bytes | 1,104,848 | 569,448 | -48.46% | 65,536 | yes | improvement | +| `12_large_live_set` | heap_total_bytes | 82,837,504 | 74,448,896 | -10.13% | 1,656,750 | yes | improvement | +| `12_large_live_set` | minor_cycles | 5 | 5 | +0.00% | 1 | yes | ok | +| `12_large_live_set` | step_cycles | 5 | 5 | +0.00% | 1 | yes | ok | +| `12_large_live_set` | copied_objects | 59,405 | 104,050 | +75.15% | 2,970 | yes | REGRESSION | +| `12_large_live_set` | copied_bytes | 2,327,008 | 4,162,008 | +78.86% | 116,350 | yes | REGRESSION | +| `12_large_live_set` | promoted_objects | 529,075 | 530,030 | +0.18% | 26,454 | yes | ok | +| `12_large_live_set` | promoted_bytes | 21,201,208 | 21,201,208 | +0.00% | 1,060,060 | yes | ok | +| `12_large_live_set` | freed_bytes | 63,732,784 | 63,732,784 | +0.00% | 3,186,639 | yes | ok | +| `12_large_live_set` | rss_bytes | 122,044,416 | 117,211,136 | -3.96% | 18,306,662 | no | ok | +| `12_large_live_set` | peak_rss_bytes | 124,665,856 | 120,307,712 | -3.50% | 18,699,878 | no | ok | +| `12_large_live_set` | wall_ms | 1,955 | 287 | -85.33% | 586 | no | improvement | +| `13_large_eden_survivors` | heap_used_bytes | 499,576 | 444,976 | -10.93% | 65,536 | yes | ok | +| `13_large_eden_survivors` | heap_total_bytes | 59,768,832 | 61,865,984 | +3.51% | 1,195,377 | no | drift (informational) | +| `13_large_eden_survivors` | minor_cycles | 3 | 3 | +0.00% | 1 | yes | ok | +| `13_large_eden_survivors` | step_cycles | 3 | 3 | +0.00% | 1 | yes | ok | +| `13_large_eden_survivors` | copied_objects | 377,569 | 382,868 | +1.40% | 18,878 | yes | ok | +| `13_large_eden_survivors` | copied_bytes | 21,429,712 | 21,804,904 | +1.75% | 1,071,486 | yes | ok | +| `13_large_eden_survivors` | promoted_objects | 386,271 | 387,152 | +0.23% | 19,314 | yes | ok | +| `13_large_eden_survivors` | promoted_bytes | 21,777,792 | 21,975,560 | +0.91% | 1,088,890 | yes | ok | +| `13_large_eden_survivors` | freed_bytes | 108,243,696 | 108,045,936 | -0.18% | 5,412,185 | yes | ok | +| `13_large_eden_survivors` | rss_bytes | 145,522,688 | 143,179,776 | -1.61% | 21,828,403 | no | ok | +| `13_large_eden_survivors` | peak_rss_bytes | 145,850,368 | 143,687,680 | -1.48% | 21,877,555 | no | ok | +| `13_large_eden_survivors` | wall_ms | 965 | 873 | -9.58% | 290 | no | ok | +| `14_grow_then_churn` | heap_used_bytes | 402,968 | 429,408 | +6.56% | 65,536 | yes | ok | +| `14_grow_then_churn` | heap_total_bytes | 23,068,672 | 23,068,672 | +0.00% | 1,048,576 | yes | ok | +| `14_grow_then_churn` | minor_cycles | 21 | 22 | +4.76% | 1 | yes | ok | +| `14_grow_then_churn` | step_cycles | 22 | 23 | +4.55% | 1 | yes | ok | +| `14_grow_then_churn` | copied_objects | 330 | 330 | +0.00% | 16 | yes | ok | +| `14_grow_then_churn` | copied_bytes | 1,222,176 | 1,277,072 | +4.49% | 65,536 | yes | ok | +| `14_grow_then_churn` | promoted_objects | 404,022 | 405,282 | +0.31% | 20,201 | yes | ok | +| `14_grow_then_churn` | promoted_bytes | 147,516,064 | 147,967,848 | +0.31% | 7,375,803 | yes | ok | +| `14_grow_then_churn` | freed_bytes | 118,903,288 | 119,549,744 | +0.54% | 5,945,164 | yes | ok | +| `14_grow_then_churn` | rss_bytes | 286,490,624 | 297,271,296 | +3.76% | 42,973,594 | no | ok | +| `14_grow_then_churn` | peak_rss_bytes | 286,834,688 | 297,779,200 | +3.82% | 43,025,203 | no | ok | +| `14_grow_then_churn` | wall_ms | 403 | 575 | +42.59% | 121 | no | drift (informational) | + +### Cells excluded from the gating family by probe override + +- `07_array_grow_evacuate`.copied_bytes — 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. (observed spread 8,064 over 21 runs, 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.; https://github.com/PerryTS/perry/issues/9790) +- `07_array_grow_evacuate`.freed_bytes — 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. (observed spread 8,208 over 21 runs, 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.; https://github.com/PerryTS/perry/issues/9790) +- `13_large_eden_survivors`.heap_total_bytes — NOT GATED ON THIS PROBE (#9829). Total reserved arena capacity varies by 2 MiB across executions of one unchanged binary: 18/21 runs reserve 61865984 bytes and 3/21 reserve 63963136. All 21 report the same 227048 live bytes and identical seven GC counters (including three minors and 377569 copied objects), with Node-equivalent output. js_arena_stats sums reserved block sizes for heapTotal, separately from its exact live-object census for heapUsed. This is spare-capacity/placement variation, not changing live retention or GC work; the precise allocating block/order is not isolated. CI run 33989581881 independently contains the same two capacities. Keep the cell visible, while heapUsed, liveness, copying/promotion/reclamation, and all other probes remain gated. Raw 21-run evidence and binary/source hashes: evidence/9829-recovery.json. (observed spread 2,097,152 over 21 runs, 2026-09-06, linux-x86_64, clean main d36a1af0c compiler and matching runtime/stdlib archives. One unchanged probe binary, 21 traced executions with PERRY_GC_SCAVENGE_NURSERY_MB=64. See evidence/9829-recovery.json capacity_variance.; https://github.com/PerryTS/perry/issues/9829) diff --git a/benchmarks/catch_frames/evidence/mac-gc-baseline.json.gz b/benchmarks/catch_frames/evidence/mac-gc-baseline.json.gz new file mode 100644 index 0000000000..2fbc106971 Binary files /dev/null and b/benchmarks/catch_frames/evidence/mac-gc-baseline.json.gz differ diff --git a/benchmarks/catch_frames/evidence/mac-gc-guard-pinned-check.log b/benchmarks/catch_frames/evidence/mac-gc-guard-pinned-check.log new file mode 100644 index 0000000000..fe71e4e222 --- /dev/null +++ b/benchmarks/catch_frames/evidence/mac-gc-guard-pinned-check.log @@ -0,0 +1,235 @@ +gc-ratchet: FAILED + - 01_nursery_churn: heap_used_bytes 212,168 -> 438,744 (+106.79%), allowance 65,536 [increase] + - 01_nursery_churn: copied_objects 4,977 -> 6,690 (+34.42%), allowance 249 [either] + - 01_nursery_churn: copied_bytes 234,536 -> 466,392 (+98.86%), allowance 65,536 [either] + - 02_survivor_promotion: copied_objects 34,550 -> 37,366 (+8.15%), allowance 1,728 [either] + - 02_survivor_promotion: copied_bytes 1,450,992 -> 1,690,264 (+16.49%), allowance 72,550 [either] + - 04_dead_after_deep_stack: copied_objects 3,622 -> 2,666 (-26.39%), allowance 181 [either] + - 05_closure_capture: copied_objects 20,079 -> 14,285 (-28.86%), allowance 1,004 [either] + - 05_closure_capture: copied_bytes 743,384 -> 534,720 (-28.07%), allowance 65,536 [either] + - 05_closure_capture: promoted_objects 3,390 -> 6,154 (+81.53%), allowance 170 [either] + - 05_closure_capture: promoted_bytes 122,120 -> 221,624 (+81.48%), allowance 65,536 [either] + - 06_string_retention: copied_objects 298 -> 270 (-9.40%), allowance 16 [either] + - 07_array_grow_evacuate: heap_used_bytes 1,072,456 -> 1,211,232 (+12.94%), allowance 65,536 [increase] + - 07_array_grow_evacuate: copied_objects 4,991 -> 6,702 (+34.28%), allowance 250 [either] + - 07_array_grow_evacuate: promoted_objects 4,879 -> 6,154 (+26.13%), allowance 244 [either] + - 07_array_grow_evacuate: promoted_bytes 615,696 -> 841,048 (+36.60%), allowance 65,536 [either] + - 09_try_catch_roots: heap_used_bytes 212,168 -> 443,520 (+109.04%), allowance 65,536 [increase] + - 09_try_catch_roots: copied_objects 4,962 -> 6,673 (+34.48%), allowance 248 [either] + - 09_try_catch_roots: copied_bytes 230,352 -> 462,024 (+100.57%), allowance 65,536 [either] + - 09_try_catch_roots: freed_bytes 11,112,472 -> 10,023,568 (-9.80%), allowance 1,048,576 [either] + - 10_store_receiver_across_alloc: heap_used_bytes 220,384 -> 446,776 (+102.73%), allowance 65,536 [increase] + - 10_store_receiver_across_alloc: heap_total_bytes 16,777,216 -> 22,020,096 (+31.25%), allowance 1,048,576 [increase] + - 10_store_receiver_across_alloc: copied_objects 18,660 -> 14,625 (-21.62%), allowance 933 [either] + - 10_store_receiver_across_alloc: promoted_objects 4,722 -> 6,107 (+29.33%), allowance 236 [either] + - 10_store_receiver_across_alloc: promoted_bytes 228,440 -> 446,368 (+95.40%), allowance 65,536 [either] + - 11_collect_at_depth: heap_used_bytes 212,168 -> 444,720 (+109.61%), allowance 65,536 [increase] + - 11_collect_at_depth: copied_objects 4,975 -> 6,686 (+34.39%), allowance 249 [either] + - 11_collect_at_depth: copied_bytes 230,872 -> 462,544 (+100.35%), allowance 65,536 [either] + - 11_collect_at_depth: freed_bytes 10,062,936 -> 8,974,576 (-10.82%), allowance 1,048,576 [either] + - 12_large_live_set: copied_objects 59,405 -> 104,050 (+75.15%), allowance 2,970 [either] + - 12_large_live_set: copied_bytes 2,327,008 -> 4,162,008 (+78.86%), allowance 116,350 [either] +## GC ratchet + +- Profile: `shared_ci` +- Baseline commit: `f2ab19447194eeaa3cb47d968877745614024451` +- Baseline captured: 2026-08-16T09:38:07+00:00 on `darwin-arm64` +- Baseline host: Apple M1, 8 cores, load at capture 2.18 + +Only rows marked gating can fail the job. Non-gating rows are recorded so a +drift that is real but unmeasurable on this runner is still visible. + +Probes pinned under a non-default collector configuration: + +- `13_large_eden_survivors` — `PERRY_GC_SCAVENGE_NURSERY_MB=64` +- `14_grow_then_churn` — `PERRY_GC_MAJOR_PACING_FLOOR_MB=1 PERRY_GC_SCAVENGE_NURSERY_MB=1` + +Accepted deterministic baseline deltas: + +- `46` cells measured at `3e3e652e007ac853bf934daa86fa36c870b0e3e2` (2026-09-06T04:12:57+00:00) +- `06e1ab349c8c6e40587bb3ce2ee9f1c58b20522b` / PR #8657 — measured: array growth targets keep their generation +- `22c07ac5b44c5ff6a988257067110c7451da0223` / PR #9833 — measured: included receiver-store workload repair +- `5196d830fd0689937db94598d94662d531d4ee92` / PR #9706 — bracketed: Sep-4 shape storage and array packing +- `aa8d2ade03970ac7eb2065f3eb0e18bb4d2df1c7` / PR #9628 — bracketed: Sep-3 liveness and retention repair +- `b3e88f1fb3378fe30b065ed328b73ea0527ab18a` / PR #8806 — measured: cache exact shape transitions +- `d923b8dcf08f2afed71777c33423362411039cf0` / PR #9373 — measured: short-concat memo changes large-Eden occupancy +- `e18b24c420c7c2f389607537618699ffc6aa9d1d` / PR #8900 — measured: weak shape-transition target edges +- `e2e6f4a1cfded098ca05afe7e1b4e5ce783a5140` / PR #8313 — measured: smaller common objects +- `fc8c9e9a15902f889df2b1aeb280ca6223f6948a` / PR #8887 — bracketed: Aug-27/28 allocation and rooting specializations + +| Probe | Metric | Baseline | Current | Δ | Allowance | Gating | Status | +|-------|--------|---------:|--------:|---:|----------:|:------:|--------| +| `01_nursery_churn` | heap_used_bytes | 212,168 | 438,744 | +106.79% | 65,536 | yes | REGRESSION | +| `01_nursery_churn` | heap_total_bytes | 16,777,216 | 14,680,064 | -12.50% | 1,048,576 | yes | improvement | +| `01_nursery_churn` | minor_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `01_nursery_churn` | step_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `01_nursery_churn` | copied_objects | 4,977 | 6,690 | +34.42% | 249 | yes | REGRESSION | +| `01_nursery_churn` | copied_bytes | 234,536 | 466,392 | +98.86% | 65,536 | yes | REGRESSION | +| `01_nursery_churn` | promoted_objects | 0 | 0 | - | 16 | yes | ok | +| `01_nursery_churn` | promoted_bytes | 0 | 0 | - | 65,536 | yes | ok | +| `01_nursery_churn` | freed_bytes | 11,299,648 | 11,067,816 | -2.05% | 1,048,576 | yes | ok | +| `01_nursery_churn` | rss_bytes | 28,950,528 | 28,540,928 | -1.41% | 8,388,608 | no | ok | +| `01_nursery_churn` | peak_rss_bytes | 29,491,200 | 29,097,984 | -1.33% | 8,388,608 | no | ok | +| `01_nursery_churn` | wall_ms | 54 | 57 | +5.70% | 100 | no | ok | +| `02_survivor_promotion` | heap_used_bytes | 2,656,440 | 2,562,832 | -3.52% | 65,536 | yes | improvement | +| `02_survivor_promotion` | heap_total_bytes | 16,777,216 | 15,728,640 | -6.25% | 1,048,576 | yes | ok | +| `02_survivor_promotion` | minor_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `02_survivor_promotion` | step_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `02_survivor_promotion` | copied_objects | 34,550 | 37,366 | +8.15% | 1,728 | yes | REGRESSION | +| `02_survivor_promotion` | copied_bytes | 1,450,992 | 1,690,264 | +16.49% | 72,550 | yes | REGRESSION | +| `02_survivor_promotion` | promoted_objects | 0 | 0 | - | 16 | yes | ok | +| `02_survivor_promotion` | promoted_bytes | 0 | 0 | - | 65,536 | yes | ok | +| `02_survivor_promotion` | freed_bytes | 9,754,128 | 8,795,408 | -9.83% | 1,048,576 | yes | ok | +| `02_survivor_promotion` | rss_bytes | 32,522,240 | 31,375,360 | -3.53% | 8,388,608 | no | ok | +| `02_survivor_promotion` | peak_rss_bytes | 33,095,680 | 31,899,648 | -3.61% | 8,388,608 | no | ok | +| `02_survivor_promotion` | wall_ms | 57 | 54 | -5.68% | 100 | no | ok | +| `03_cross_gen_writes` | heap_used_bytes | 528 | 528 | +0.00% | 65,536 | yes | ok | +| `03_cross_gen_writes` | heap_total_bytes | 14,680,064 | 12,582,912 | -14.29% | 1,048,576 | yes | improvement | +| `03_cross_gen_writes` | minor_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `03_cross_gen_writes` | step_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `03_cross_gen_writes` | copied_objects | 4,107 | 4,107 | +0.00% | 205 | yes | ok | +| `03_cross_gen_writes` | copied_bytes | 229,824 | 197,056 | -14.26% | 65,536 | yes | ok | +| `03_cross_gen_writes` | promoted_objects | 0 | 0 | - | 16 | yes | ok | +| `03_cross_gen_writes` | promoted_bytes | 0 | 0 | - | 65,536 | yes | ok | +| `03_cross_gen_writes` | freed_bytes | 9,240,008 | 9,240,008 | +0.00% | 1,048,576 | yes | ok | +| `03_cross_gen_writes` | rss_bytes | 21,708,800 | 22,921,216 | +5.58% | 8,388,608 | no | ok | +| `03_cross_gen_writes` | peak_rss_bytes | 26,542,080 | 26,984,448 | +1.67% | 8,388,608 | no | ok | +| `03_cross_gen_writes` | wall_ms | 37 | 30 | -18.35% | 100 | no | ok | +| `04_dead_after_deep_stack` | heap_used_bytes | 536 | 536 | +0.00% | 65,536 | yes | ok | +| `04_dead_after_deep_stack` | heap_total_bytes | 20,971,520 | 18,874,368 | -10.00% | 1,048,576 | yes | improvement | +| `04_dead_after_deep_stack` | minor_cycles | 6 | 7 | +16.67% | 1 | yes | ok | +| `04_dead_after_deep_stack` | step_cycles | 6 | 7 | +16.67% | 1 | yes | ok | +| `04_dead_after_deep_stack` | copied_objects | 3,622 | 2,666 | -26.39% | 181 | yes | REGRESSION | +| `04_dead_after_deep_stack` | copied_bytes | 149,400 | 111,336 | -25.48% | 65,536 | yes | ok | +| `04_dead_after_deep_stack` | promoted_objects | 682 | 682 | +0.00% | 34 | yes | ok | +| `04_dead_after_deep_stack` | promoted_bytes | 440 | 27,320 | +6109.09% | 65,536 | yes | ok | +| `04_dead_after_deep_stack` | freed_bytes | 73,346,416 | 76,493,952 | +4.29% | 3,667,321 | yes | ok | +| `04_dead_after_deep_stack` | rss_bytes | 24,100,864 | 25,182,208 | +4.49% | 8,388,608 | no | ok | +| `04_dead_after_deep_stack` | peak_rss_bytes | 28,950,528 | 29,229,056 | +0.96% | 8,388,608 | no | ok | +| `04_dead_after_deep_stack` | wall_ms | 69 | 48 | -30.67% | 100 | no | ok | +| `05_closure_capture` | heap_used_bytes | 5,664 | 544 | -90.40% | 65,536 | yes | ok | +| `05_closure_capture` | heap_total_bytes | 14,680,064 | 14,680,064 | +0.00% | 1,048,576 | yes | ok | +| `05_closure_capture` | minor_cycles | 3 | 3 | +0.00% | 1 | yes | ok | +| `05_closure_capture` | step_cycles | 3 | 3 | +0.00% | 1 | yes | ok | +| `05_closure_capture` | copied_objects | 20,079 | 14,285 | -28.86% | 1,004 | yes | REGRESSION | +| `05_closure_capture` | copied_bytes | 743,384 | 534,720 | -28.07% | 65,536 | yes | REGRESSION | +| `05_closure_capture` | promoted_objects | 3,390 | 6,154 | +81.53% | 170 | yes | REGRESSION | +| `05_closure_capture` | promoted_bytes | 122,120 | 221,624 | +81.48% | 65,536 | yes | REGRESSION | +| `05_closure_capture` | freed_bytes | 26,862,320 | 27,897,872 | +3.86% | 1,343,116 | yes | ok | +| `05_closure_capture` | rss_bytes | 19,169,280 | 23,265,280 | +21.37% | 8,388,608 | no | ok | +| `05_closure_capture` | peak_rss_bytes | 24,248,320 | 26,836,992 | +10.68% | 8,388,608 | no | ok | +| `05_closure_capture` | wall_ms | 68 | 59 | -14.31% | 100 | no | ok | +| `06_string_retention` | heap_used_bytes | 212,640 | 1,072 | -99.50% | 65,536 | yes | improvement | +| `06_string_retention` | heap_total_bytes | 23,068,672 | 22,020,096 | -4.55% | 1,048,576 | yes | ok | +| `06_string_retention` | minor_cycles | 5 | 4 | -20.00% | 1 | yes | ok | +| `06_string_retention` | step_cycles | 5 | 4 | -20.00% | 1 | yes | ok | +| `06_string_retention` | copied_objects | 298 | 270 | -9.40% | 16 | yes | REGRESSION | +| `06_string_retention` | copied_bytes | 72,288 | 70,144 | -2.97% | 65,536 | yes | ok | +| `06_string_retention` | promoted_objects | 14 | 14 | +0.00% | 16 | yes | ok | +| `06_string_retention` | promoted_bytes | 1,072 | 1,072 | +0.00% | 65,536 | yes | ok | +| `06_string_retention` | freed_bytes | 68,136,816 | 68,136,816 | +0.00% | 3,406,841 | yes | ok | +| `06_string_retention` | rss_bytes | 32,964,608 | 28,459,008 | -13.67% | 8,388,608 | no | ok | +| `06_string_retention` | peak_rss_bytes | 33,488,896 | 33,079,296 | -1.22% | 8,388,608 | no | ok | +| `06_string_retention` | wall_ms | 76 | 53 | -30.77% | 100 | no | ok | +| `07_array_grow_evacuate` | heap_used_bytes | 1,072,456 | 1,211,232 | +12.94% | 65,536 | yes | REGRESSION | +| `07_array_grow_evacuate` | heap_total_bytes | 25,165,824 | 25,165,824 | +0.00% | 1,048,576 | yes | ok | +| `07_array_grow_evacuate` | minor_cycles | 5 | 5 | +0.00% | 1 | yes | ok | +| `07_array_grow_evacuate` | step_cycles | 5 | 5 | +0.00% | 1 | yes | ok | +| `07_array_grow_evacuate` | copied_objects | 4,991 | 6,702 | +34.28% | 250 | yes | REGRESSION | +| `07_array_grow_evacuate` | copied_bytes | 2,671,936 | 2,636,392 | -1.33% | 133,597 | no | ok | +| `07_array_grow_evacuate` | promoted_objects | 4,879 | 6,154 | +26.13% | 244 | yes | REGRESSION | +| `07_array_grow_evacuate` | promoted_bytes | 615,696 | 841,048 | +36.60% | 65,536 | yes | REGRESSION | +| `07_array_grow_evacuate` | freed_bytes | 83,727,056 | 83,570,704 | -0.19% | 4,186,353 | no | ok | +| `07_array_grow_evacuate` | rss_bytes | 34,029,568 | 37,928,960 | +11.46% | 8,388,608 | no | ok | +| `07_array_grow_evacuate` | peak_rss_bytes | 34,553,856 | 38,535,168 | +11.52% | 8,388,608 | no | ok | +| `07_array_grow_evacuate` | wall_ms | 144 | 63 | -56.20% | 100 | no | ok | +| `08_map_set_sidetables` | heap_used_bytes | 672 | 688 | +2.38% | 65,536 | yes | ok | +| `08_map_set_sidetables` | heap_total_bytes | 16,777,216 | 14,680,064 | -12.50% | 1,048,576 | yes | improvement | +| `08_map_set_sidetables` | minor_cycles | 5 | 6 | +20.00% | 1 | yes | ok | +| `08_map_set_sidetables` | step_cycles | 5 | 6 | +20.00% | 1 | yes | ok | +| `08_map_set_sidetables` | copied_objects | 2,489 | 2,461 | -1.12% | 124 | yes | ok | +| `08_map_set_sidetables` | copied_bytes | 144,384 | 98,472 | -31.80% | 65,536 | yes | ok | +| `08_map_set_sidetables` | promoted_objects | 15 | 14 | -6.67% | 16 | yes | ok | +| `08_map_set_sidetables` | promoted_bytes | 576 | 592 | +2.78% | 65,536 | yes | ok | +| `08_map_set_sidetables` | freed_bytes | 57,655,296 | 56,598,904 | -1.83% | 2,882,765 | yes | ok | +| `08_map_set_sidetables` | rss_bytes | 21,037,056 | 19,955,712 | -5.14% | 8,388,608 | no | ok | +| `08_map_set_sidetables` | peak_rss_bytes | 25,329,664 | 24,559,616 | -3.04% | 8,388,608 | no | ok | +| `08_map_set_sidetables` | wall_ms | 171 | 205 | +19.92% | 100 | no | ok | +| `09_try_catch_roots` | heap_used_bytes | 212,168 | 443,520 | +109.04% | 65,536 | yes | REGRESSION | +| `09_try_catch_roots` | heap_total_bytes | 14,680,064 | 14,680,064 | +0.00% | 1,048,576 | yes | ok | +| `09_try_catch_roots` | minor_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `09_try_catch_roots` | step_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `09_try_catch_roots` | copied_objects | 4,962 | 6,673 | +34.48% | 248 | yes | REGRESSION | +| `09_try_catch_roots` | copied_bytes | 230,352 | 462,024 | +100.57% | 65,536 | yes | REGRESSION | +| `09_try_catch_roots` | promoted_objects | 0 | 0 | - | 16 | yes | ok | +| `09_try_catch_roots` | promoted_bytes | 0 | 0 | - | 65,536 | yes | ok | +| `09_try_catch_roots` | freed_bytes | 11,112,472 | 10,023,568 | -9.80% | 1,048,576 | yes | REGRESSION | +| `09_try_catch_roots` | rss_bytes | 30,015,488 | 30,195,712 | +0.60% | 8,388,608 | no | ok | +| `09_try_catch_roots` | peak_rss_bytes | 30,457,856 | 30,670,848 | +0.70% | 8,388,608 | no | ok | +| `09_try_catch_roots` | wall_ms | 744 | 155 | -79.10% | 223 | no | improvement | +| `10_store_receiver_across_alloc` | heap_used_bytes | 220,384 | 446,776 | +102.73% | 65,536 | yes | REGRESSION | +| `10_store_receiver_across_alloc` | heap_total_bytes | 16,777,216 | 22,020,096 | +31.25% | 1,048,576 | yes | REGRESSION | +| `10_store_receiver_across_alloc` | minor_cycles | 9 | 8 | -11.11% | 1 | yes | ok | +| `10_store_receiver_across_alloc` | step_cycles | 9 | 8 | -11.11% | 1 | yes | ok | +| `10_store_receiver_across_alloc` | copied_objects | 18,660 | 14,625 | -21.62% | 933 | yes | REGRESSION | +| `10_store_receiver_across_alloc` | copied_bytes | 825,520 | 787,792 | -4.57% | 65,536 | yes | ok | +| `10_store_receiver_across_alloc` | promoted_objects | 4,722 | 6,107 | +29.33% | 236 | yes | REGRESSION | +| `10_store_receiver_across_alloc` | promoted_bytes | 228,440 | 446,368 | +95.40% | 65,536 | yes | REGRESSION | +| `10_store_receiver_across_alloc` | freed_bytes | 88,858,544 | 88,640,584 | -0.25% | 4,442,927 | yes | ok | +| `10_store_receiver_across_alloc` | rss_bytes | 24,756,224 | 35,061,760 | +41.63% | 8,388,608 | no | drift (informational) | +| `10_store_receiver_across_alloc` | peak_rss_bytes | 25,296,896 | 35,586,048 | +40.67% | 8,388,608 | no | drift (informational) | +| `10_store_receiver_across_alloc` | wall_ms | 43 | 196 | +354.30% | 100 | no | drift (informational) | +| `11_collect_at_depth` | heap_used_bytes | 212,168 | 444,720 | +109.61% | 65,536 | yes | REGRESSION | +| `11_collect_at_depth` | heap_total_bytes | 13,631,488 | 12,582,912 | -7.69% | 1,048,576 | yes | ok | +| `11_collect_at_depth` | minor_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `11_collect_at_depth` | step_cycles | 1 | 1 | +0.00% | 1 | yes | ok | +| `11_collect_at_depth` | copied_objects | 4,975 | 6,686 | +34.39% | 249 | yes | REGRESSION | +| `11_collect_at_depth` | copied_bytes | 230,872 | 462,544 | +100.35% | 65,536 | yes | REGRESSION | +| `11_collect_at_depth` | promoted_objects | 0 | 0 | - | 16 | yes | ok | +| `11_collect_at_depth` | promoted_bytes | 0 | 0 | - | 65,536 | yes | ok | +| `11_collect_at_depth` | freed_bytes | 10,062,936 | 8,974,576 | -10.82% | 1,048,576 | yes | REGRESSION | +| `11_collect_at_depth` | rss_bytes | 26,525,696 | 26,492,928 | -0.12% | 8,388,608 | no | ok | +| `11_collect_at_depth` | peak_rss_bytes | 26,984,448 | 27,131,904 | +0.55% | 8,388,608 | no | ok | +| `11_collect_at_depth` | wall_ms | 48 | 43 | -10.60% | 100 | no | ok | +| `12_large_live_set` | heap_used_bytes | 1,104,848 | 569,448 | -48.46% | 65,536 | yes | improvement | +| `12_large_live_set` | heap_total_bytes | 82,837,504 | 74,448,896 | -10.13% | 1,656,750 | yes | improvement | +| `12_large_live_set` | minor_cycles | 5 | 5 | +0.00% | 1 | yes | ok | +| `12_large_live_set` | step_cycles | 5 | 5 | +0.00% | 1 | yes | ok | +| `12_large_live_set` | copied_objects | 59,405 | 104,050 | +75.15% | 2,970 | yes | REGRESSION | +| `12_large_live_set` | copied_bytes | 2,327,008 | 4,162,008 | +78.86% | 116,350 | yes | REGRESSION | +| `12_large_live_set` | promoted_objects | 529,075 | 530,030 | +0.18% | 26,454 | yes | ok | +| `12_large_live_set` | promoted_bytes | 21,201,208 | 21,201,208 | +0.00% | 1,060,060 | yes | ok | +| `12_large_live_set` | freed_bytes | 63,732,784 | 63,732,784 | +0.00% | 3,186,639 | yes | ok | +| `12_large_live_set` | rss_bytes | 122,044,416 | 117,047,296 | -4.09% | 18,306,662 | no | ok | +| `12_large_live_set` | peak_rss_bytes | 124,665,856 | 120,176,640 | -3.60% | 18,699,878 | no | ok | +| `12_large_live_set` | wall_ms | 1,955 | 325 | -83.35% | 586 | no | improvement | +| `13_large_eden_survivors` | heap_used_bytes | 499,576 | 444,976 | -10.93% | 65,536 | yes | ok | +| `13_large_eden_survivors` | heap_total_bytes | 59,768,832 | 61,865,984 | +3.51% | 1,195,377 | no | drift (informational) | +| `13_large_eden_survivors` | minor_cycles | 3 | 3 | +0.00% | 1 | yes | ok | +| `13_large_eden_survivors` | step_cycles | 3 | 3 | +0.00% | 1 | yes | ok | +| `13_large_eden_survivors` | copied_objects | 377,569 | 382,868 | +1.40% | 18,878 | yes | ok | +| `13_large_eden_survivors` | copied_bytes | 21,429,712 | 21,804,904 | +1.75% | 1,071,486 | yes | ok | +| `13_large_eden_survivors` | promoted_objects | 386,271 | 387,152 | +0.23% | 19,314 | yes | ok | +| `13_large_eden_survivors` | promoted_bytes | 21,777,792 | 21,975,560 | +0.91% | 1,088,890 | yes | ok | +| `13_large_eden_survivors` | freed_bytes | 108,243,696 | 108,045,936 | -0.18% | 5,412,185 | yes | ok | +| `13_large_eden_survivors` | rss_bytes | 145,522,688 | 142,540,800 | -2.05% | 21,828,403 | no | ok | +| `13_large_eden_survivors` | peak_rss_bytes | 145,850,368 | 143,048,704 | -1.92% | 21,877,555 | no | ok | +| `13_large_eden_survivors` | wall_ms | 965 | 887 | -8.06% | 290 | no | ok | +| `14_grow_then_churn` | heap_used_bytes | 402,968 | 429,408 | +6.56% | 65,536 | yes | ok | +| `14_grow_then_churn` | heap_total_bytes | 23,068,672 | 23,068,672 | +0.00% | 1,048,576 | yes | ok | +| `14_grow_then_churn` | minor_cycles | 21 | 22 | +4.76% | 1 | yes | ok | +| `14_grow_then_churn` | step_cycles | 22 | 23 | +4.55% | 1 | yes | ok | +| `14_grow_then_churn` | copied_objects | 330 | 330 | +0.00% | 16 | yes | ok | +| `14_grow_then_churn` | copied_bytes | 1,222,176 | 1,277,072 | +4.49% | 65,536 | yes | ok | +| `14_grow_then_churn` | promoted_objects | 404,022 | 405,282 | +0.31% | 20,201 | yes | ok | +| `14_grow_then_churn` | promoted_bytes | 147,516,064 | 147,967,848 | +0.31% | 7,375,803 | yes | ok | +| `14_grow_then_churn` | freed_bytes | 118,903,288 | 119,549,744 | +0.54% | 5,945,164 | yes | ok | +| `14_grow_then_churn` | rss_bytes | 286,490,624 | 297,287,680 | +3.77% | 42,973,594 | no | ok | +| `14_grow_then_churn` | peak_rss_bytes | 286,834,688 | 297,779,200 | +3.82% | 43,025,203 | no | ok | +| `14_grow_then_churn` | wall_ms | 403 | 492 | +21.98% | 121 | no | ok | + +### Cells excluded from the gating family by probe override + +- `07_array_grow_evacuate`.copied_bytes — 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. (observed spread 8,064 over 21 runs, 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.; https://github.com/PerryTS/perry/issues/9790) +- `07_array_grow_evacuate`.freed_bytes — 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. (observed spread 8,208 over 21 runs, 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.; https://github.com/PerryTS/perry/issues/9790) +- `13_large_eden_survivors`.heap_total_bytes — NOT GATED ON THIS PROBE (#9829). Total reserved arena capacity varies by 2 MiB across executions of one unchanged binary: 18/21 runs reserve 61865984 bytes and 3/21 reserve 63963136. All 21 report the same 227048 live bytes and identical seven GC counters (including three minors and 377569 copied objects), with Node-equivalent output. js_arena_stats sums reserved block sizes for heapTotal, separately from its exact live-object census for heapUsed. This is spare-capacity/placement variation, not changing live retention or GC work; the precise allocating block/order is not isolated. CI run 33989581881 independently contains the same two capacities. Keep the cell visible, while heapUsed, liveness, copying/promotion/reclamation, and all other probes remain gated. Raw 21-run evidence and binary/source hashes: evidence/9829-recovery.json. (observed spread 2,097,152 over 21 runs, 2026-09-06, linux-x86_64, clean main d36a1af0c compiler and matching runtime/stdlib archives. One unchanged probe binary, 21 traced executions with PERRY_GC_SCAVENGE_NURSERY_MB=64. See evidence/9829-recovery.json capacity_variance.; https://github.com/PerryTS/perry/issues/9829) diff --git a/benchmarks/catch_frames/evidence/mac-gc-guard.json.gz b/benchmarks/catch_frames/evidence/mac-gc-guard.json.gz new file mode 100644 index 0000000000..ad3f25de52 Binary files /dev/null and b/benchmarks/catch_frames/evidence/mac-gc-guard.json.gz differ diff --git a/benchmarks/catch_frames/evidence/mac-gc-harness-tests.log b/benchmarks/catch_frames/evidence/mac-gc-harness-tests.log new file mode 100644 index 0000000000..e32875dca2 --- /dev/null +++ b/benchmarks/catch_frames/evidence/mac-gc-harness-tests.log @@ -0,0 +1,23 @@ +...........................gc-ratchet error: 01_probe: heap_used_bytes summary is inconsistent with its samples +.....gc-ratchet: UNFIT PINNED CELL `01_probe.heap_used_bytes` — spread 6768 when pinned, but its band is justified by bit-identity, not by a noise allowance. Either re-pin on a quiet host, or take this one cell out of the gating family with a probe_overrides entry that records the evidence. +gc-ratchet: 1 unfit cell(s) deferred to `check` (--scope structural); they will fail the job there, after the probes run +gc-ratchet: UNFIT PINNED CELL `01_probe.heap_used_bytes` — spread 6768 when pinned, but its band is justified by bit-identity, not by a noise allowance. Either re-pin on a quiet host, or take this one cell out of the gating family with a probe_overrides entry that records the evidence. +gc-ratchet: /var/folders/8v/09vl257j2_v70v2k77gh01n00000gn/T/tmpnp401gxv/artifact.json has 1 unfit cell(s); re-pin them or record a probe_overrides entry +gc-ratchet: UNFIT PINNED PROBE `01_probe` — baseline was pinned without a passing Node oracle diff (status='unchecked'), so this probe's rows are not evidence about the collector +gc-ratchet: 1 unfit cell(s) deferred to `check` (--scope structural); they will fail the job there, after the probes run +gc-ratchet: UNFIT PINNED PROBE `01_probe` — baseline was pinned without a passing Node oracle diff (status='unchecked'), so this probe's rows are not evidence about the collector +gc-ratchet: /var/folders/8v/09vl257j2_v70v2k77gh01n00000gn/T/tmpoa3t5n78/artifact.json has 1 unfit cell(s); re-pin them or record a probe_overrides entry +gc-ratchet: UNFIT PINNED PROBE `01_probe` — baseline pinned a probe that ran no minor collection, so there is no evacuating-minor behaviour here to ratchet against +gc-ratchet: 1 unfit cell(s) deferred to `check` (--scope structural); they will fail the job there, after the probes run +gc-ratchet: UNFIT PINNED PROBE `01_probe` — baseline pinned a probe that ran no minor collection, so there is no evacuating-minor behaviour here to ratchet against +gc-ratchet: /var/folders/8v/09vl257j2_v70v2k77gh01n00000gn/T/tmpmkazs2vz/artifact.json has 1 unfit cell(s); re-pin them or record a probe_overrides entry +..gc-ratchet: UNFIT PINNED CELL `01_probe.heap_used_bytes` — spread 6768 when pinned, but its band is justified by bit-identity, not by a noise allowance. Either re-pin on a quiet host, or take this one cell out of the gating family with a probe_overrides entry that records the evidence. +gc-ratchet: /var/folders/8v/09vl257j2_v70v2k77gh01n00000gn/T/tmpnpoq5boi/artifact.json has 1 unfit cell(s); re-pin them or record a probe_overrides entry +................................................................................. +---------------------------------------------------------------------- +Ran 115 tests in 8.432s + +OK +gc-ratchet: /var/folders/8v/09vl257j2_v70v2k77gh01n00000gn/T/tmpnp401gxv/artifact.json is structurally valid +gc-ratchet: /var/folders/8v/09vl257j2_v70v2k77gh01n00000gn/T/tmpoa3t5n78/artifact.json is structurally valid +gc-ratchet: /var/folders/8v/09vl257j2_v70v2k77gh01n00000gn/T/tmpmkazs2vz/artifact.json is structurally valid diff --git a/benchmarks/catch_frames/evidence/mac-gc-structural.log b/benchmarks/catch_frames/evidence/mac-gc-structural.log new file mode 100644 index 0000000000..35b22ce55f --- /dev/null +++ b/benchmarks/catch_frames/evidence/mac-gc-structural.log @@ -0,0 +1 @@ +gc-ratchet: /Users/amlug/projects/perry/wt-js-throw-catch-frames/benchmarks/gc_ratchet/baseline/gc-ratchet-v1.json is structurally valid diff --git a/benchmarks/catch_frames/evidence/matrix-trend.json.gz b/benchmarks/catch_frames/evidence/matrix-trend.json.gz new file mode 100644 index 0000000000..1575c09f05 Binary files /dev/null and b/benchmarks/catch_frames/evidence/matrix-trend.json.gz differ diff --git a/benchmarks/catch_frames/evidence/parity.json.gz b/benchmarks/catch_frames/evidence/parity.json.gz new file mode 100644 index 0000000000..805859d725 Binary files /dev/null and b/benchmarks/catch_frames/evidence/parity.json.gz differ diff --git a/benchmarks/catch_frames/evidence/pinned-gc-comparison.json b/benchmarks/catch_frames/evidence/pinned-gc-comparison.json new file mode 100644 index 0000000000..60775d342d --- /dev/null +++ b/benchmarks/catch_frames/evidence/pinned-gc-comparison.json @@ -0,0 +1,24 @@ +{ + "baseline_commit": "eb13fa188d", + "platform": "darwin-arm64", + "probes": 14, + "repeats": 7, + "compared_medians": 126, + "changed_medians": [], + "pinned_profile": "shared_ci", + "baseline_pinned_check_exit": 1, + "candidate_pinned_check_exit": 1, + "pinned_failing_cells": 30, + "added_pinned_failures": [], + "removed_pinned_failures": [], + "candidate_artifacts": { + "perry": "37f3f494caac2742a17ab1122776bd54ffa889ff32d2cbc6c4dd9955a18558f8", + "libperry_runtime.a": "e0b5b127f5754c803192b2b744adde6095990f5a8d40a4f8eee00ad75463fc6d", + "libperry_stdlib.a": "d00b623797ab1b2bdd7722a4c5c14866bdf9aa320e4dadf45806cbb0ed5b75cf" + }, + "baseline_artifacts": { + "perry": "955e2354482630b1df7643ef7005790d506e8cba0e0f3e4fdbaf35ba3359bea8", + "libperry_runtime.a": "d852c771df1faaa2f0c5d0288dddf3f7a48b63ff181756e093f71aee3db94f5d", + "libperry_stdlib.a": "ebd25d57c55ac9ae7c53aeb66751468faa6e5cfeb9dc803e796ff7f7d342f76e" + } +} diff --git a/benchmarks/catch_frames/evidence/pre-edit/promises.expected b/benchmarks/catch_frames/evidence/pre-edit/promises.expected new file mode 100644 index 0000000000..90ee71a089 --- /dev/null +++ b/benchmarks/catch_frames/evidence/pre-edit/promises.expected @@ -0,0 +1 @@ +5000050000 diff --git a/benchmarks/catch_frames/evidence/pre-edit/promises.folded.gz b/benchmarks/catch_frames/evidence/pre-edit/promises.folded.gz new file mode 100644 index 0000000000..92d8a62821 Binary files /dev/null and b/benchmarks/catch_frames/evidence/pre-edit/promises.folded.gz differ diff --git a/benchmarks/catch_frames/evidence/pre-edit/promises.frames b/benchmarks/catch_frames/evidence/pre-edit/promises.frames new file mode 100644 index 0000000000..24e7eeb3a6 --- /dev/null +++ b/benchmarks/catch_frames/evidence/pre-edit/promises.frames @@ -0,0 +1,3 @@ +# started on Mon Sep 14 04:30:40 2026 + +100007;;perrycatch_probe:push;148213065;100.00;; diff --git a/benchmarks/catch_frames/evidence/pre-edit/promises.no-inline.folded.gz b/benchmarks/catch_frames/evidence/pre-edit/promises.no-inline.folded.gz new file mode 100644 index 0000000000..32f103fd88 Binary files /dev/null and b/benchmarks/catch_frames/evidence/pre-edit/promises.no-inline.folded.gz differ diff --git a/benchmarks/catch_frames/evidence/pre-edit/promises.stat b/benchmarks/catch_frames/evidence/pre-edit/promises.stat new file mode 100644 index 0000000000..3aa5b4036a --- /dev/null +++ b/benchmarks/catch_frames/evidence/pre-edit/promises.stat @@ -0,0 +1,3 @@ +# started on Mon Sep 14 04:27:37 2026 + +1098663515;;instructions:u;0.00%;68669358;100.00;; diff --git a/benchmarks/catch_frames/evidence/pre-marker-gc-artifacts.json b/benchmarks/catch_frames/evidence/pre-marker-gc-artifacts.json new file mode 100644 index 0000000000..925e82f3c6 --- /dev/null +++ b/benchmarks/catch_frames/evidence/pre-marker-gc-artifacts.json @@ -0,0 +1,7 @@ +{ + "perry": "3116bf752bd6f6be928e93a06620f2a6f1b39bf69888320a8d101a21141c4095", + "libperry_runtime.a": "044596ad11d74720b294fb1f09695221e775a510f9a5c3333f740a4011be6fa7", + "libperry_stdlib.a": "5b54dfdffca610b5c3d0316c4acaaaf97823722be3187f45e11f55d2667c05f2", + "libperry_ext_net.a": "789e289cffd69bfac735332fb6cbe0cbc96cc72e35f793ad049d01aaf25c4faa", + "libperry_ext_http.a": "1c31949243aa69f59eaa82f433249b0052003f72e1bb7563d67c405c29f7fc84" +} diff --git a/benchmarks/catch_frames/evidence/socket-isolated-output.txt b/benchmarks/catch_frames/evidence/socket-isolated-output.txt new file mode 100644 index 0000000000..116d65ca45 --- /dev/null +++ b/benchmarks/catch_frames/evidence/socket-isolated-output.txt @@ -0,0 +1,4 @@ +connected +phase0 ok: direct small write works +phase1 ok: map large write works +OK diff --git a/benchmarks/catch_frames/evidence/socket-isolated-recheck.json b/benchmarks/catch_frames/evidence/socket-isolated-recheck.json new file mode 100644 index 0000000000..8420226dbb --- /dev/null +++ b/benchmarks/catch_frames/evidence/socket-isolated-recheck.json @@ -0,0 +1,23 @@ +{ + "generated_at": "2026-09-14T06:47:22Z", + "platform": "linux", + "summary": { + "parity_pass": 1, + "parity_fail": 0, + "compile_fail": 0, + "crash_fail": 0, + "node_fail": 0, + "skipped": 0, + "total_run": 1, + "parity_percentage": 100.0 + }, + "failures": { + "parity": [""] +, + "compile": [""] +, + "crash": [""] + + }, + "results": [{"id":"test_sock_write_map","status":"pass"}] +} diff --git a/benchmarks/catch_frames/evidence/socket-recheck.md b/benchmarks/catch_frames/evidence/socket-recheck.md new file mode 100644 index 0000000000..3058f646e6 --- /dev/null +++ b/benchmarks/catch_frames/evidence/socket-recheck.md @@ -0,0 +1,20 @@ +# Socket fixture isolation + +The complete four-shard report retains `test_sock_write_map: parity_fail`. +Its Node output was `ERROR: Error: connect ECONNREFUSED 127.0.0.1:17891`; +Perry printed `ERROR: [object Object]`. The required echo service was absent. + +`run_parity_tests.sh` starts an echo server on the same fixed port in every +shard. Only one process can own that port, while every shard kills its own +helper on exit. The owner shard completed before this late fixture. This +explains the lost prerequisite; it is not evidence of broken socket writes. + +The official filtered fixture passes in isolation on both the frozen main +and final guarded builds, with the harness starting a live echo server. +The final guarded Node/Perry stdout is byte-identical and retained in +`socket-isolated-output.txt`; its report is `socket-isolated-recheck.json`. +The baseline report is included in `baseline-rechecks.json.gz`. + +The original full-suite report is unchanged. No allowlist entry, snapshot +change, harness patch or replacement aggregate result is used to hide this +infrastructure failure. The separate replay adjudicates this one result. diff --git a/benchmarks/catch_frames/evidence/source-files.json b/benchmarks/catch_frames/evidence/source-files.json new file mode 100644 index 0000000000..f3f2b89cc8 --- /dev/null +++ b/benchmarks/catch_frames/evidence/source-files.json @@ -0,0 +1,22 @@ +{ + "crates/perry-runtime/examples/catch_frames.rs": "a56c24682abcdc3c15b904e8d9d402bd8f8e292ebca58003c09fc5bde096af11", + "crates/perry-runtime/src/dyn_eval/mod.rs": "7d1749e6eb6b074de04e1553342c9ebbab0c4c55ca0d141180b4bc5dba2672af", + "crates/perry-runtime/src/exception.rs": "1675e8cba4426084b4f3161c6759ae7cd5a85a1781f8f6685ce10cea0eadbe2b", + "crates/perry-runtime/src/exception/savepoints.rs": "097ca04482f9c60f19e1ea84d51bc34735ccf09d2ca44292494533a0eeabc935", + "crates/perry-runtime/src/exception/savepoints/tests.rs": "fe3240c95ea615be25e39f6b1c55c6ca5e544c41aa48bd5343bb90753c8d4d73", + "crates/perry-runtime/src/gc/roots/runtime_handles.rs": "89166341c2f9ba2bbe34fe0cb9fa696a7d2b9d93fe9569f29c23ba1545f4a5e4", + "crates/perry-runtime/src/gc/roots/shadow_stack.rs": "1f3fb282f689cfaef5056fd5dd44b7e2500335aae51f86e98abc744862b53ae6", + "crates/perry-runtime/src/gc/roots/temp_roots.rs": "1ae85e82efb0681c5a7f6f046d4720536c36c0c80c3cddedb99436cd36f4156b", + "crates/perry-runtime/src/lib.rs": "29b6ca1f63f71956604fa5522c5aa0bac968fe11a894e2ee09b3910b63ce8757", + "crates/perry-runtime/src/map.rs": "2742eea647e7bb7a3155fdfd47df939e0ea71c8fbed94431a26b364e62ca49dd", + "crates/perry-runtime/src/object/call_method_depth.rs": "c0e43291902afea6d246c2c39c122dc145e874ba1ec9c5556fff3776134a1ebd", + "crates/perry-runtime/src/object/field_get_set.rs": "78fc01621c179f3daca4736fb73d9e57b11a56463d4a7aee4ce5ab40e9a4c522", + "crates/perry-runtime/src/object/field_get_set/ic_miss.rs": "8ab60c821fbc4c401d5fed73ec60aa856b58d807c9646d890b5c2b8efe506493", + "crates/perry-runtime/src/object/field_get_set/ic_miss/private_member_access.rs": "b84b873c2f8f123f9bce302bbb8fd7e4c33a3ea01c633660f44157ff08d7bcb2", + "crates/perry-runtime/src/object/global_this/fetch_globals.rs": "60b8d8b9f9b4f1687f19d14ad16a5369ac85671acbc36c9997a3b210e08b81bb", + "crates/perry-runtime/src/object/mod.rs": "a9b479cfffc4db4b90ebf0034645a63a78b3b03cbc65cc23bb2b5d68cf0b2bb9", + "crates/perry-runtime/src/object/prototype_chain.rs": "1b62b7ac5e4ef6b1262d60fab119002255f35c1b4db5d932bd3e652a093c82d4", + "crates/perry-runtime/src/object/this_binding.rs": "18ddf21ec48672677455b3f200b96c8465141f79b04a508b198ec44864336ecf", + "crates/perry-runtime/src/regex/site_test.rs": "ce85fdc11ad1e22276847d6501293f1092b83d1db1df0e18013db8bcc4ac3618", + "crates/perry-runtime/src/set.rs": "901d46b0476265ada2316830db2d139d22ec04f71fa9366aa33ab399d5dbe60b" +} diff --git a/benchmarks/catch_frames/evidence/threshold.json.gz b/benchmarks/catch_frames/evidence/threshold.json.gz new file mode 100644 index 0000000000..22cc2fac85 Binary files /dev/null and b/benchmarks/catch_frames/evidence/threshold.json.gz differ diff --git a/benchmarks/catch_frames/evidence/validation.md b/benchmarks/catch_frames/evidence/validation.md new file mode 100644 index 0000000000..d34f1296f2 --- /dev/null +++ b/benchmarks/catch_frames/evidence/validation.md @@ -0,0 +1,148 @@ +# Local validation replay + +Baseline: main `eb13fa188d0690fb964cd6df329593e05aa55617`, version 0.5.1564. +This commit already contains the #10215 large-array fix. All edits and builds +used session-owned worktrees and targets; the user's dirty checkout was not +modified. The feature branch does not bump the workspace version. Landing +requires the repository's merge train and a replay on the assembled train. + +## Runtime and static gates + +The final source, including the constructor-identity guard, was checked on +macOS arm64 with nightly-2026-08-20. Default runtime features include both +regex-engine and dyn-eval, so all 13 registered restore witnesses run. + +| Check | Result | +|---|---| +| `cargo fmt --all -- --check` | Pass | +| `cargo check -p perry-runtime --no-default-features --features full` | Pass | +| `cargo test -p perry-runtime --lib -- --test-threads=1` | 3,811 passed, 4 ignored; baseline 3,798 passed, 4 ignored | +| One omitted production restore at a time | All 13 detected at the nested enclosing-state assertion; restored exception suite: 20 passed | +| `cargo clippy -p perry-runtime --lib --tests --message-format=json` compared with main | No added or removed diagnostics; both return 101 for the same existing errors | +| `scripts/run_lint_gates.sh` | 80/83 pass; three baseline-reproduced failures, two CI-only skips | + +Clippy diagnostics are compared as multisets of level, lint code, message and +primary filename, ignoring source-line movement. The baseline has 12 +`clippy::approx_constant` errors; `clippy-comparison.json` records the diff. +The user-listed `native_stack` test passes on this host. + +The final complete lint replay ran 83 gates: 80 passed and three failed, +with two CI-only checks skipped by the script. Each failure was independently +reproduced on the clean baseline: + +- Public benchmark evidence freshness is stale on main. +- Host-compatible all-targets `RUSTFLAGS="-D warnings" cargo check` rejects + unused WebAssembly helpers in unchanged `global_this_webassembly.rs`. +- Generated API documentation differs from committed docs. Baseline and + candidate generated Markdown and declarations are byte-identical, including + missing committed bun-pty entries. Generated documentation was restored + after checking; no baseline or warning suppression is included in the patch. + +Product strict-warning, product clippy and workspace clippy gates pass. +The full lint transcript and baseline comparisons are retained beside this +file. The GC custody, root inventory and wiring checks are part of that replay. + +## GC replay + +The catch-frame patch before the seven-line constructor marker guard passed +the full Linux matrix: 86 corpus fixtures × 22 arms = 1,892 cells, with 1,533 +PASS, 359 UNVER, zero XFAIL and zero FAIL. Every one of the 21 arms with a +liveness requirement exercised its required collector operation. The shipped +control has no liveness requirement. The 359 UNVER cells are not counted as +passing checks; they are retained by the official matrix harness. + +This matrix used a coherent release build of `perry`, `perry-runtime-static`, +`perry-stdlib-static`, `perry-ext-net`, and `perry-ext-http`, pinned Node 26.5.1, +`PERRY_NO_AUTO_OPTIMIZE=1`, and the full runtime archives. The explicit +extension packages avoid mixing archives with a different runtime source +stamp. GC representation flags still vary across the matrix's compile groups. +The full JSON report is `gc-stress.json.gz`. The final guarded PR-arm replay also passes: 86 fixtures × seven arms = +602/602 byte-exact Node matches, 431 PASS, 171 UNVER, zero XFAIL and zero +FAIL. All six required liveness checks pass. Its complete report is +`guard-gc-stress.json.gz`, with liveness counters in +`guard-gc-stress-summary.txt`. + +The final guarded Linux ratchet ran seven repetitions of all 14 probes against Node 26.5.1. +Its same-host baseline was built from the stated main commit. All 126 +heap/collector medians match exactly and `check --profile shared_ci` passes +against that fresh comparison artifact. The artifact is evidence only; the +repository's pinned baseline is unchanged. + +The exact pinned ratchet was also replayed on macOS with the workflow's +three-package release graph and default one-codegen-unit release profile. +The GC harness unit tests and `validate --scope structural` pass. Baseline and +final guarded candidate have identical values for all 126 heap/collector +medians; both fail the pinned artifact at the same 30 cells. The ordered +failure diagnostics are identical (`pinned-gc-comparison.json`). Wall time and RSS are observations only, not performance claims. + +## Compiled integration + +The final Linux replay completed all four disjoint +`run_parity_tests.sh --shard N/4` shards with the frozen guarded three-package +artifacts used for instruction measurements. Auto-optimized extension +variants requested by the harness used the matching guarded source tree. +The official merge required all four reports and verified disjoint test IDs. + +| Complete inventory | Result | +|---|---| +| All parity fixtures | 1,633 entries: 1,472 pass, 93 output mismatches, 29 compile failures, two timeouts classified as crashes, five Node oracle failures, 32 skips | +| Gap subset | All 785 entries: 776 pass, nine output mismatches, no crashes/compile failures/oracle failures/skips | +| Aggregate threshold | Pass, 93.9% by the harness's formula | +| Matrix trend | Pass | +| Gap snapshot | Three baseline-reproduced mismatches | +| Platform known-failure gate | Eight unlisted results: seven reproduce on main; the eighth passes an isolated candidate replay after restoring its echo-server prerequisite | + +Every `test_gap_` fixture ran as part of the full sweep. The extracted gap +report was checked with the same `gap_snapshot.py check` used by +`scripts/run_gap_tests.sh`. Both complete reports, coverage assertions and +all gate commands/results are retained. No allowlist or snapshot is relaxed. + +The seven unlisted baseline-reproduced results are: + +- `test_bun_plugin` and `test_dynamic_import_data_10104` (oracle/import behavior). +- `test_gap_2899_2779_2777_static_helpers`, `test_gap_disposablestack_2875`, and + `test_gap_iterator_prototype_next_patch` (also the three gap-gate failures). +- `test_issue_10155_textfield_singleline` (compile failure). +- `test_issue_336_class_keys_collision` (Node rejects namespace syntax). + +The eighth, `test_sock_write_map`, encountered a missing echo server during +the parallel sweep: both Node and Perry received connection-refused errors. +The official isolated fixture passes on both main and the guarded candidate. +`socket-recheck.md` records the cause and successful replay; the original +aggregate report is retained without rewriting its failure to a pass. + +Both crashes are 10-second timeouts and reproduce on main: +`test_issue_4975_http_agent_keep_alive_connection_options` and +`test_parity_stream_web`. Their existing failure records cite closed #8841 +and #793 respectively; that stale tracking is not presented as a new open +issue. See `additional-baseline-checks.json` for the observed behavior. + +The five Node oracle failures are long-lived server/channel fixtures: +`test_node_http_basic`, `test_issue_4826_array_headers`, +`test_node_http_post_body`, `test_message_channel_global`, and +`test_node_http_ws_upgrade`. Perry was not tested by those entries. The array +headers oracle timeout was also reproduced with the clean baseline harness. +The test sources and harness are unchanged by this PR. These entries and the +32 skips remain explicit gaps in this replay's execution coverage. + +Constructor-marker regression and guard-omission evidence are recorded in +`constructor-identity.md`. All five targeted guarded rechecks pass, and all +five also pass in the complete sweep. + +## Evidence provenance + +`instructions.json` records the SHA-256 of the reviewed +`git diff eb13fa188d -- crates` patch and the frozen three-package artifacts. +`source-files.json` hashes all 20 changed or added Rust source files; each +matches the Linux validation source byte for byte. Final instruction +measurements and full integration include the constructor guard. The older +full 22-arm matrix precedes only that guard; its scope is stated above. The +final source passes the seven-arm PR matrix and matches main on both GC +ratchet measurements. + +Full logs and binaries are retained under `/root/js-throw-evidence` on +perrymaster. Local unit, lint, clippy and pinned GC logs are under +`/tmp/perry-catch-evidence`. Repository evidence contains compact JSON, raw +instruction-stat outputs, folded stacks, fault transcripts and gate reports. +Superseded incomplete integration runs, including an early source-stamp +mismatch, are excluded from the final results. diff --git a/benchmarks/catch_frames/exec1.ts b/benchmarks/catch_frames/exec1.ts new file mode 100644 index 0000000000..c2f866a24e --- /dev/null +++ b/benchmarks/catch_frames/exec1.ts @@ -0,0 +1 @@ +const vals: string[] = []; for (let i = 0; i < 1000000; i++) vals.push((i % 2 ? "record_" : "!bad_") + i); let c = 0; const re = /([a-z]+)_([0-9]+)/; for (let i = 0; i < vals.length; i++) { const m = re.exec(vals[i]); if (m !== null) c += m[2].length; } console.log(c); diff --git a/benchmarks/catch_frames/fault_restore.py b/benchmarks/catch_frames/fault_restore.py new file mode 100644 index 0000000000..7d2819d535 --- /dev/null +++ b/benchmarks/catch_frames/fault_restore.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Temporarily omit each registered production restore; require its real-throw test to fail.""" +import argparse +import json +import os +from pathlib import Path +import re +import resource +import subprocess + + +def build(root, log): + command = ["cargo", "test", "-p", "perry-runtime", "--lib", "--no-run", "--message-format=json"] + result = subprocess.run(command, cwd=root, text=True, capture_output=True) + log.with_suffix(".stderr").write_text(result.stderr) + log.write_text(result.stdout) + result.check_returncode() + artifacts = [json.loads(line) for line in result.stdout.splitlines() if line.startswith("{")] + return next(a["executable"] for a in artifacts if a.get("reason") == "compiler-artifact" and a.get("executable") and a["target"]["name"] == "perry_runtime") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + args.output.mkdir(parents=True, exist_ok=True) + resource.setrlimit(resource.RLIMIT_CORE, (0, 0)) + root = Path(__file__).resolve().parents[2] + path = root / "crates/perry-runtime/src/exception/savepoints.rs" + original = path.read_text() + needle = "$($(#[$attr])* $restore(self.$name);)*" + assert original.count(needle) == 1 + names = re.findall(r"^ (\w+): [^\n]+ = ", original, flags=re.MULTILINE) + captures = re.findall(r"^ capture: ", original, flags=re.MULTILINE) + assert names and len(names) == len(set(names)) == len(captures), names + mutant = original.replace(needle, '''$($(#[$attr])* { + if std::env::var("PERRY_TEST_SKIP_CATCH_RESTORE").as_deref() != Ok(stringify!($name)) { + $restore(self.$name); + } + })*''') + results = [] + try: + path.write_text(mutant) + executable = build(root, args.output / "mutant-build.jsonl") + for name in names: + test = f"exception::savepoints::restore_tests::{name}" + result = subprocess.run([executable, "--exact", test, "--test-threads=1", "--nocapture"], env=dict(os.environ, PERRY_TEST_SKIP_CATCH_RESTORE=name), text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + (args.output / (name + ".log")).write_text(result.stdout) + # A filter matching zero tests, or an unrelated startup failure, + # must not count as a detected missing restore. + detected = result.returncode != 0 and "inner catch must preserve its enclosing scope" in result.stdout + results.append({"restore": name, "test": test, "exit_code": result.returncode, "detected": detected}) + print(name, result.returncode, detected, flush=True) + (args.output / "fault-results.json").write_text(json.dumps(results, indent=2) + "\n") + assert all(r["detected"] for r in results), results + finally: + path.write_text(original) + # The source AND executable must be restored; a later green run must + # not accidentally exercise an environment-controlled mutant. + executable = build(root, args.output / "restored-build.jsonl") + with (args.output / "restored-tests.log").open("w") as stream: + subprocess.run([executable, "exception::", "--test-threads=1"], stdout=stream, stderr=subprocess.STDOUT, check=True) + + +if __name__ == "__main__": + main() diff --git a/benchmarks/catch_frames/hoist.ts b/benchmarks/catch_frames/hoist.ts new file mode 100644 index 0000000000..07c34ce7c5 --- /dev/null +++ b/benchmarks/catch_frames/hoist.ts @@ -0,0 +1 @@ +const vals: string[] = []; for (let i = 0; i < 1000000; i++) vals.push((i % 2 ? "record_" : "!bad_") + i); let c = 0; const re = /^[a-z]+_[0-9]+$/; for (let i = 0; i < vals.length; i++) if (re.test(vals[i])) c++; console.log(c); diff --git a/benchmarks/catch_frames/json.ts b/benchmarks/catch_frames/json.ts new file mode 100644 index 0000000000..cd21e41159 --- /dev/null +++ b/benchmarks/catch_frames/json.ts @@ -0,0 +1,10 @@ +const source = '{"name":"a-long-heap-string-payload","count":42,"items":[1,2,3],"child":{"ok":true}}'; +let checksum = 0; +for (let i = 0; i < 100000; i++) { + const value = JSON.parse(source); + value.count = i; + const encoded = JSON.stringify(value); + const decoded = JSON.parse(encoded); + checksum += encoded.length + decoded.count + decoded.items[2]; +} +console.log(checksum); diff --git a/benchmarks/catch_frames/measure.py b/benchmarks/catch_frames/measure.py new file mode 100644 index 0000000000..7a21c3b8e5 --- /dev/null +++ b/benchmarks/catch_frames/measure.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Build/check probes, count user instructions, and retain folded DWARF stacks.""" +import argparse +import collections +import json +import os +from pathlib import Path +import re +import subprocess + + +def run(args, output, env=None): + with output.open("w") as stream: + subprocess.run(args, stdout=stream, stderr=subprocess.STDOUT, env=env, check=True) + + +def fold(text): + stacks = collections.Counter() + frames = [] + for line in text.splitlines() + [""]: + if not line.strip(): + if frames: + stacks[";".join(reversed(frames))] += 1 + frames = [] + elif line[0].isspace(): + parts = line.strip().split(None, 1) + if len(parts) == 2: + symbol = parts[1].rsplit(" (", 1)[0] + frames.append(re.sub(r"\+0x[0-9a-f]+$", "", symbol)) + return stacks + + +def summarize(instructions, stacks): + # LTO can inline capture into either exported entry point. Counting only + # the private helper would miss those frames. Never count a callback body + # just because catch_js_throw / perry_sjlj_try appears above it. + capture_pattern = re.compile(r"\b(?:try_push_with_kind|js_try_push|js_eh_try_push)\b|CatchSavepoint::capture") + capture = sum(n for s, n in stacks.items() if capture_pattern.search(s)) + transport = sum(n for s, n in stacks.items() if re.search(r"perry_sjlj_try|arm_trap_and_run|sigsetjmp|__sigjmp_save|js_try_end|catch_js_throw", s.split(";")[-1])) + total = sum(stacks.values()) + return {"instructions": instructions, "samples": total, "capture_samples": capture, "transport_leaf_samples": transport, "capture_percent": 100 * capture / total} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--perry", type=Path, required=True) + parser.add_argument("--micro", type=Path) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--period", type=int, default=1000003) + parser.add_argument("--probes", nargs="*", default=["promises", "json", "hoist", "exec1"]) + args = parser.parse_args() + args.output.mkdir(parents=True, exist_ok=True) + env = dict(os.environ, PERRY_KEEP_SYMBOLS="1", PERRY_NO_AUTO_OPTIMIZE="1", PERRY_NO_TELEMETRY="1") + results = {} + jobs = [] + for name in args.probes: + source = Path(__file__).resolve().parent / (name + ".ts") + binary = args.output / name + run([str(args.perry), "compile", str(source), "--no-auto-optimize", "-o", str(binary)], args.output / (name + ".compile.log"), env) + expected = subprocess.check_output(["node", str(source)], env=env) + actual = subprocess.check_output([str(binary)], env=env) + (args.output / (name + ".expected")).write_bytes(expected) + (args.output / (name + ".out")).write_bytes(actual) + assert actual == expected, name + jobs.append((name, [str(binary)])) + if args.micro: + for mode in ["plain", "catch", "unwind"]: + jobs.append((mode, [str(args.micro), mode, "1000000"])) + for name, command in jobs: + stat = args.output / (name + ".stat") + run(["perf", "stat", "-x", ";", "-r", "3", "-e", "instructions:u", "-o", str(stat), "--"] + command, args.output / (name + ".stat.out"), env) + instructions = next(float(line.split(";")[0]) for line in stat.read_text().splitlines() if "instructions:u" in line) + data = args.output / (name + ".perf.data") + run(["perf", "record", "-q", "-m", "64M", "-e", "instructions:u", "-c", str(args.period), "--call-graph", "dwarf", "-o", str(data), "--"] + command, args.output / (name + ".profile.out"), env) + stacks = fold(subprocess.check_output(["perf", "script", "--no-inline", "-i", str(data)], text=True)) + (args.output / (name + ".folded")).write_text("".join(f"{s} {n}\n" for s, n in stacks.most_common())) + results[name] = summarize(instructions, stacks) + print(name, results[name], flush=True) + (args.output / "results.json").write_text(json.dumps(results, indent=2) + "\n") + + +if __name__ == "__main__": + main() diff --git a/benchmarks/catch_frames/promises.ts b/benchmarks/catch_frames/promises.ts new file mode 100644 index 0000000000..bafee1b950 --- /dev/null +++ b/benchmarks/catch_frames/promises.ts @@ -0,0 +1,9 @@ +async function run(): Promise { + let checksum = 0; + for (let i = 0; i < 100000; i++) { + const value = await Promise.resolve(i).then((n: number) => n + 1); + checksum += value; + } + console.log(checksum); +} +run(); diff --git a/changelog.d/10257-catch-frames.md b/changelog.d/10257-catch-frames.md new file mode 100644 index 0000000000..c1b269f8ba --- /dev/null +++ b/changelog.d/10257-catch-frames.md @@ -0,0 +1,5 @@ +Group JavaScript exception frames and inline savepoint capture providers to +reduce catch setup overhead. A shared declaration now couples each subsystem's +capture, restore and nested real-throw regression test, while runtime traps +continue to use the C `setjmp` trampoline. +Preserve builtin constructor marker identity across Thin LTO runtime archives. diff --git a/crates/perry-runtime/examples/catch_frames.rs b/crates/perry-runtime/examples/catch_frames.rs new file mode 100644 index 0000000000..52a5a4962a --- /dev/null +++ b/crates/perry-runtime/examples/catch_frames.rs @@ -0,0 +1,26 @@ +//! Instruction-count probe: compare `catch` with its `plain` loop control. +use perry_runtime::exception::{catch_js_throw, js_eh_try_push, js_try_end}; +use std::hint::black_box; + +fn main() { + let mode = std::env::args().nth(1).unwrap_or_else(|| "catch".into()); + let count: u64 = std::env::args() + .nth(2) + .map(|n| n.parse().unwrap()) + .unwrap_or(1_000_000); + let mut sum = 0; + for i in 0..count { + sum += match mode.as_str() { + "catch" => catch_js_throw(|| black_box(i)).unwrap(), + "unwind" => { + js_eh_try_push(); + let value = black_box(i); + js_try_end(); + value + } + "plain" => black_box(i), + _ => panic!("expected catch, unwind or plain"), + }; + } + println!("{sum}"); +} diff --git a/crates/perry-runtime/src/dyn_eval/mod.rs b/crates/perry-runtime/src/dyn_eval/mod.rs index ad93c0ea7b..3fb7695df2 100644 --- a/crates/perry-runtime/src/dyn_eval/mod.rs +++ b/crates/perry-runtime/src/dyn_eval/mod.rs @@ -271,6 +271,7 @@ pub(crate) fn root_set(idx: usize, value: f64) { ROOTS.with(|r| r.borrow_mut()[idx] = value.to_bits()); } +#[inline] pub(crate) fn roots_len() -> usize { ROOTS.with(|r| r.borrow().len()) } @@ -292,6 +293,7 @@ pub(crate) fn roots_truncate(len: usize) { /// epilogues, so `js_throw` restores both from the savepoint of the catching /// `try` — exactly like the shadow-stack savepoint (#1830) and the /// method-depth savepoint (#5591). +#[inline] pub(crate) fn interp_savepoint() -> u64 { let len = roots_len() as u64; let depth = CALL_DEPTH.with(|c| c.get()) as u64; diff --git a/crates/perry-runtime/src/exception.rs b/crates/perry-runtime/src/exception.rs index 6b861b697a..ef1e4c7283 100644 --- a/crates/perry-runtime/src/exception.rs +++ b/crates/perry-runtime/src/exception.rs @@ -6,7 +6,7 @@ //! `js_throw` raises through the system unwinder //! (`_Unwind_RaiseException`; `RaiseException` on Windows) and the //! frame's `landingpad`/`catchpad` receives control — see `crate::eh`. -//! * **Rust-side boundary traps** (`js_try_push` + `ffi::setjmp`, +//! * **Rust-side boundary traps** (`js_try_push` + the C trampoline, //! `HandlerKind::Setjmp`): runtime helpers that drive user JS from a //! Rust-owned context (`js_call_catching`, promise combinators, iterator //! trampolines) catch via `longjmp` — Rust cannot catch a foreign @@ -42,38 +42,19 @@ impl JmpBuf { } } -use crate::gc::{ - runtime_handle_stack_restore, runtime_handle_stack_savepoint, shadow_stack_restore, - shadow_stack_savepoint, ShadowSavepoint, -}; +mod savepoints; +use savepoints::CatchSavepoint; extern "C" { fn longjmp(env: *mut i32, val: i32) -> !; } -// Maximum nesting depth for try blocks. Backed by fixed-size per-thread -// arrays (see ExceptionState), so this directly sizes thread-local memory: -// jump_buffers is MAX_TRY_DEPTH * sizeof(JmpBuf) (256 B each). 1024 covers -// deep-but-legal recursion-through-try; genuinely unbounded recursion hits a -// native stack overflow well before this. Raised from 128 (#5065): 128 -// aborted the process via panic on legal deeply-nested try/catch. +// Maximum nesting depth for try blocks. All per-depth state lives in one +// fixed heap slab per thread. The slab keeps jump-buffer addresses stable and +// avoids ld64's 64KB inline initialized-TLS limit on arm64_32. Raised from 128 +// (#5065): legal recursion through try/catch must reach 1024 open handlers. const MAX_TRY_DEPTH: usize = 1024; -/// Per-thread exception state. Exception handling uses setjmp/longjmp, -/// and a jmp_buf captured by setjmp on thread A is meaningless on thread -/// B (its stack frame doesn't exist there) — so the buffers, the depth -/// counter, the current exception, and the finally-flag all have to -/// live in TLS once `perry/thread` workers can run user code that -/// throws. Previously this state was process-wide `static mut` data and would -/// corrupt under any concurrent throw. -// arm64_32 fix: the per-depth arrays are HEAP-allocated (`Box<[..]>`) -// instead of stored inline in TLS. At MAX_TRY_DEPTH=1024 they are ~280KB of -// initialized thread-local data (`jump_buffers` alone is 1024 * 256B = 256KB), -// which overflows ld64's 64KB `__thread_data` cap for arm64_32 (and the ILP32 -// TLS layout generally). Boxing leaves only fat pointers + scalars inline in -// TLS; the arrays live on the heap. `[T]` indexing on `Box<[T]>` is -// unchanged, so the accessors below need no edits. (Mirrors the -// TRANSITION_CACHE / VTABLE_IC / INTERN_TABLE boxing.) /// How a handler-stack entry catches (#7302). /// /// `Setjmp`: the handler frame armed a `jmp_buf` (generated setjmp-based @@ -93,68 +74,25 @@ enum HandlerKind { Unwind, } +/// One depth indexes one allocation and one bounds check. The jump buffer +/// stays in a fixed slab, so its address remains valid while a trap is armed. +#[derive(Clone, Copy)] +struct TryFrame { + jump_buffer: JmpBuf, + kind: HandlerKind, + savepoint: CatchSavepoint, +} + +impl TryFrame { + const EMPTY: Self = Self { + jump_buffer: JmpBuf::new(), + kind: HandlerKind::Setjmp, + savepoint: CatchSavepoint::EMPTY, + }; +} + struct ExceptionState { - jump_buffers: Box<[JmpBuf]>, - /// Catch mechanism per open handler, in lockstep with `jump_buffers` - /// (whose slot is simply unused for `Unwind` entries). - handler_kinds: Box<[HandlerKind]>, - /// Shadow-stack depth captured when each `try` block was pushed, so the - /// unwind path can drop the orphaned frames `longjmp` leaves behind (see - /// `js_throw` / issue #1830). Indexed by try-depth, in lockstep with - /// `jump_buffers`. - shadow_savepoints: Box<[ShadowSavepoint]>, - /// Runtime-handle stack depth captured with each `try`. A `longjmp` skips - /// `RuntimeHandleScope` drops, so stale roots must be removed before the - /// catch path can allocate or trigger GC. - runtime_handle_savepoints: Box<[usize]>, - /// `js_native_call_method` recursion depth captured when each `try` was - /// pushed. A throw `longjmp`s past the in-flight method frames, skipping - /// their `CallMethodDepthGuard` `Drop`s; the unwind path restores this so - /// the counter doesn't leak (see `js_throw` / `crate::object`'s - /// `call_method_depth_*`). Indexed by try-depth, in lockstep with - /// `jump_buffers`. - call_method_depths: Box<[u32]>, - /// Re-entrant stdlib-pump depth at handler entry. A callback can throw - /// across `js_run_stdlib_pump`, skipping its `PumpDepthGuard`; restore the - /// counter so the next top-level pump still runs tick-begin lifecycle - /// hooks such as the native-handle quarantine drain. - pump_depths: Box<[u32]>, - /// Active Set/Map `forEach` walks. Their normal epilogues re-enable - /// backing-store compaction, but a caught throw skips those epilogues. - set_foreach_depths: Box<[usize]>, - map_foreach_depths: Box<[usize]>, - /// Recorded-prototype lookup stack depth. A getter can throw while - /// `resolve_inherited_field` is recursively walking; longjmp skips its - /// guard drops, so restore the stack to this try-entry savepoint. - prototype_resolution_depths: Box<[usize]>, - /// Static private-environment dispatch depth at each handler. A throw can - /// bypass a static method/accessor's normal pop, so catch entry restores - /// the stack to its handler-entry state. - static_private_owner_depths: Box<[usize]>, - /// Lexical private-brand dispatch stack depth at each handler. Generated - /// throws bypass normal method epilogues, so the catch path truncates the - /// orphaned entries exactly like the shadow and runtime-handle stacks. - private_lexical_brand_depths: Box<[usize]>, - /// Active derived-constructor binding cells at handler entry. A caught - /// throw can skip an inline constructor's normal scope pop. - derived_super_binding_depths: Box<[usize]>, - /// Pending private-member dispatch hints at handler entry. A throw while - /// evaluating the right-hand side of a guarded private write skips the - /// normal consumer, so catch entry must discard the orphaned hint. - private_member_access_hint_depths: Box<[usize]>, - /// Active allocation-free regex-factory sites at handler entry. A - /// non-literal replacement callee can throw before the wrapper's normal - /// pop, so catch entry discards the orphaned identity frame. - #[cfg(feature = "regex-engine")] - regex_factory_site_depths: Box<[usize]>, - /// #6559: dyn-eval interpreter state (rooted-stack length + interpreter - /// call depth, packed) captured when each `try` was pushed. A throw - /// `longjmp`s past interpreter Rust frames without running their - /// epilogues; the unwind path restores the interpreter's rooted value - /// stack so caught throws neither leak roots nor leave the depth counter - /// wedged. Same savepoint pattern as the two fields above. - #[cfg(feature = "dyn-eval")] - dyn_eval_savepoints: Box<[u64]>, + frames: Box<[TryFrame]>, try_depth: usize, current_exception: f64, has_exception: bool, @@ -162,27 +100,11 @@ struct ExceptionState { } impl ExceptionState { - // No longer `const`: `vec!` builds the arrays directly on the heap (no large - // stack temporary), so first access lazily allocates ~280KB off the TLS. fn new() -> Self { - ExceptionState { - jump_buffers: vec![JmpBuf::new(); MAX_TRY_DEPTH].into_boxed_slice(), - handler_kinds: vec![HandlerKind::Setjmp; MAX_TRY_DEPTH].into_boxed_slice(), - shadow_savepoints: vec![ShadowSavepoint::EMPTY; MAX_TRY_DEPTH].into_boxed_slice(), - runtime_handle_savepoints: vec![0usize; MAX_TRY_DEPTH].into_boxed_slice(), - call_method_depths: vec![0u32; MAX_TRY_DEPTH].into_boxed_slice(), - pump_depths: vec![0u32; MAX_TRY_DEPTH].into_boxed_slice(), - set_foreach_depths: vec![0usize; MAX_TRY_DEPTH].into_boxed_slice(), - map_foreach_depths: vec![0usize; MAX_TRY_DEPTH].into_boxed_slice(), - prototype_resolution_depths: vec![0usize; MAX_TRY_DEPTH].into_boxed_slice(), - static_private_owner_depths: vec![0usize; MAX_TRY_DEPTH].into_boxed_slice(), - private_lexical_brand_depths: vec![0usize; MAX_TRY_DEPTH].into_boxed_slice(), - derived_super_binding_depths: vec![0usize; MAX_TRY_DEPTH].into_boxed_slice(), - private_member_access_hint_depths: vec![0usize; MAX_TRY_DEPTH].into_boxed_slice(), - #[cfg(feature = "regex-engine")] - regex_factory_site_depths: vec![0usize; MAX_TRY_DEPTH].into_boxed_slice(), - #[cfg(feature = "dyn-eval")] - dyn_eval_savepoints: vec![0u64; MAX_TRY_DEPTH].into_boxed_slice(), + Self { + // Build directly on the heap; a slab this large must never be an + // inline TLS initializer or a native-stack temporary (arm64_32). + frames: vec![TryFrame::EMPTY; MAX_TRY_DEPTH].into_boxed_slice(), try_depth: 0, current_exception: 0.0, has_exception: false, @@ -235,43 +157,12 @@ fn try_push_with_kind(kind: HandlerKind) -> *mut i32 { panic!("Try block nesting too deep"); } let depth = (*s).try_depth; - (*s).handler_kinds[depth] = kind; - // Capture the shadow-stack depth now, before the protected region - // can push any callee frames, so the unwind path can restore to - // exactly this point and drop the frames `longjmp` orphans (#1830). - (*s).shadow_savepoints[depth] = shadow_stack_savepoint(); - (*s).runtime_handle_savepoints[depth] = runtime_handle_stack_savepoint(); - // Capture the method-dispatch recursion depth too, so a throw caught by - // this `try` can restore it — `longjmp` skips the `CallMethodDepthGuard` - // `Drop`s of the method frames it unwinds (#5591). - (*s).call_method_depths[depth] = crate::object::call_method_depth_savepoint(); - (*s).pump_depths[depth] = crate::stdlib_pump::pump_depth_savepoint(); - (*s).set_foreach_depths[depth] = crate::set::set_foreach_stack_savepoint(); - (*s).map_foreach_depths[depth] = crate::map::map_foreach_stack_savepoint(); - (*s).prototype_resolution_depths[depth] = - crate::object::prototype_chain::resolution_stack_savepoint(); - (*s).static_private_owner_depths[depth] = - crate::object::static_private_owner_stack_savepoint(); - (*s).private_lexical_brand_depths[depth] = - crate::object::private_lexical_brand_stack_savepoint(); - (*s).derived_super_binding_depths[depth] = - crate::object::derived_super_binding_stack_savepoint(); - (*s).private_member_access_hint_depths[depth] = - crate::object::private_member_access_hints_savepoint(); - #[cfg(feature = "regex-engine")] - { - (*s).regex_factory_site_depths[depth] = - crate::regex::site_test::active_factory_stack_savepoint(); - } - // #6559: capture the dyn-eval interpreter's rooted-stack length + - // call depth, so a caught throw restores interpreter state exactly - // like the shadow stack. - #[cfg(feature = "dyn-eval")] - { - (*s).dyn_eval_savepoints[depth] = crate::dyn_eval::interp_savepoint(); - } + let savepoint = CatchSavepoint::capture(); + let frame = &mut (*s).frames[depth]; + frame.kind = kind; + frame.savepoint = savepoint; (*s).try_depth += 1; - (*s).jump_buffers[depth].as_mut_ptr() + frame.jump_buffer.as_mut_ptr() }) } @@ -468,52 +359,16 @@ pub extern "C-unwind" fn js_throw(value: f64) -> ! { // restore code this longjmp skips (#788). Pure thread-local state // swaps — no JS runs and nothing allocates. crate::async_context::unwind_context_guards(depth); - // Drop the shadow-stack frames of the functions we are about to - // unwind past. `longjmp` skips their epilogues (and therefore their - // `js_shadow_frame_pop` calls), so without this the next GC would - // scan — and the copying collector would rewrite — slots living in - // already-unwound stack frames (#1830). Restore to the depth captured - // when this `try` was pushed. - shadow_stack_restore((*s).shadow_savepoints[depth]); - runtime_handle_stack_restore((*s).runtime_handle_savepoints[depth]); - // Restore the method-dispatch recursion depth captured when this `try` - // was pushed. The direct and longjmp transports skip the guards' - // `Drop`s. A system-unwinder fallback does run them, but the guards use - // their entry depths to make cleanup after this eager restore a no-op; - // otherwise caught throws wrap the counter below zero and wedge every - // later method call into the depth-guard fallback (#5591). - crate::object::call_method_depth_restore((*s).call_method_depths[depth]); - crate::stdlib_pump::pump_depth_restore((*s).pump_depths[depth]); - crate::set::set_foreach_stack_restore((*s).set_foreach_depths[depth]); - crate::map::map_foreach_stack_restore((*s).map_foreach_depths[depth]); - crate::object::prototype_chain::resolution_stack_restore( - (*s).prototype_resolution_depths[depth], - ); - crate::object::static_private_owner_stack_restore((*s).static_private_owner_depths[depth]); - crate::object::private_lexical_brand_stack_restore( - (*s).private_lexical_brand_depths[depth], - ); - crate::object::derived_super_binding_stack_restore( - (*s).derived_super_binding_depths[depth], - ); - crate::object::private_member_access_hints_restore( - (*s).private_member_access_hint_depths[depth], - ); - #[cfg(feature = "regex-engine")] - crate::regex::site_test::active_factory_stack_restore( - (*s).regex_factory_site_depths[depth], - ); - // #6559: restore the dyn-eval interpreter's rooted stack + call depth - // (interpreter Rust frames unwound by this longjmp never run their - // truncate/decrement epilogues). - #[cfg(feature = "dyn-eval")] - crate::dyn_eval::interp_restore((*s).dyn_eval_savepoints[depth]); + // Restore every subsystem before transporting the exception. The + // registration generates capture and restore together, including the + // feature-gated entries; the test replay uses this exact path too. + (*s).frames[depth].savepoint.restore(); // The savepoint restores above are transport-independent: the unwind // path skips Rust cleanups exactly like longjmp does (the runtime is // built panic=abort; see crate::eh), so restoring at throw time is // correct for both. - match (*s).handler_kinds[depth] { - HandlerKind::Setjmp => (*s).jump_buffers[depth].as_mut_ptr(), + match (*s).frames[depth].kind { + HandlerKind::Setjmp => (*s).frames[depth].jump_buffer.as_mut_ptr(), HandlerKind::Unwind => std::ptr::null_mut(), } }); @@ -850,31 +705,14 @@ pub(crate) fn test_try_depth() -> usize { with_exception_state(|s| unsafe { (*s).try_depth }) } -/// Replay the shadow-stack restore that `js_throw` performs for the -/// innermost open `try`, without the `longjmp` (which can't return in a -/// unit test). Lets tests exercise the real #1830 savepoint/restore path -/// recorded by `js_try_push`. +/// Replay the production savepoint restoration without transporting an +/// exception. Legacy subsystem tests use this; each registration additionally +/// has a nested real-throw test in `savepoints::restore_tests`. #[cfg(test)] pub(crate) fn test_unwind_innermost_shadow_restore() { with_exception_state(|s| unsafe { assert!((*s).try_depth > 0, "no open try to unwind"); - let depth = (*s).try_depth - 1; - shadow_stack_restore((*s).shadow_savepoints[depth]); - runtime_handle_stack_restore((*s).runtime_handle_savepoints[depth]); - crate::object::call_method_depth_restore((*s).call_method_depths[depth]); - crate::stdlib_pump::pump_depth_restore((*s).pump_depths[depth]); - crate::set::set_foreach_stack_restore((*s).set_foreach_depths[depth]); - crate::map::map_foreach_stack_restore((*s).map_foreach_depths[depth]); - crate::object::prototype_chain::resolution_stack_restore( - (*s).prototype_resolution_depths[depth], - ); - crate::object::private_member_access_hints_restore( - (*s).private_member_access_hint_depths[depth], - ); - #[cfg(feature = "regex-engine")] - crate::regex::site_test::active_factory_stack_restore( - (*s).regex_factory_site_depths[depth], - ); + (*s).frames[(*s).try_depth - 1].savepoint.restore(); }); } @@ -951,7 +789,7 @@ mod tests { // Relative to the entry depth so it's robust under shared TLS // (`--test-threads=1`) alongside the other tests in this module. let base = current_try_depth(); - let pushes = (MAX_TRY_DEPTH - base) - 1; + let pushes = MAX_TRY_DEPTH - base; assert!( pushes > 128, "expected room for >128 frames beyond the old limit" diff --git a/crates/perry-runtime/src/exception/savepoints.rs b/crates/perry-runtime/src/exception/savepoints.rs new file mode 100644 index 0000000000..5193a62d55 --- /dev/null +++ b/crates/perry-runtime/src/exception/savepoints.rs @@ -0,0 +1,97 @@ +//! One declaration per catch savepoint: storage, capture, restore and a real +//! throw witness are generated together. The order here is also restore order. +//! +//! Capture providers are inlined so their HotKey accesses share the existing +//! hot TLS cache. No live subsystem state is mirrored or journaled, and the +//! cold restore path continues to use each subsystem's own cleanup rules. + +use crate::gc::ShadowSavepoint; + +macro_rules! catch_savepoints { + ($($(#[$attr:meta])* $name:ident: $ty:ty = $empty:expr, + capture: $capture:path, restore: $restore:path;)*) => { + #[derive(Clone, Copy)] + pub(super) struct CatchSavepoint { + $($(#[$attr])* $name: $ty,)* + } + + impl CatchSavepoint { + pub(super) const EMPTY: Self = Self { + $($(#[$attr])* $name: $empty,)* + }; + + #[inline(always)] + pub(super) fn capture() -> Self { + Self { $($(#[$attr])* $name: $capture(),)* } + } + + pub(super) fn restore(self) { + $($(#[$attr])* $restore(self.$name);)* + } + } + + #[cfg(test)] + mod restore_tests { + use super::tests; + $($(#[$attr])* + #[test] + fn $name() { + tests::assert_nested_restore($capture, $restore, tests::$name); + } + )* + } + }; +} + +catch_savepoints! { + // #1830, #6951: both shadow frames and expression temp roots. + shadow: ShadowSavepoint = ShadowSavepoint::EMPTY, + capture: crate::gc::shadow_stack_savepoint, + restore: crate::gc::shadow_stack_restore; + // Longjmp skips RuntimeHandleScope drops. + runtime_handles: usize = 0, + capture: crate::gc::runtime_handle_stack_savepoint, + restore: crate::gc::runtime_handle_stack_restore; + // #5591: skipped method guards must not wedge the recursion counter. + call_method: u32 = 0, + capture: crate::object::call_method_depth_savepoint, + restore: crate::object::call_method_depth_restore; + // Includes removal of the process-wide outer-pump contribution. + pump: u32 = 0, + capture: crate::stdlib_pump::pump_depth_savepoint, + restore: crate::stdlib_pump::pump_depth_restore; + // #9082: abandoned walks must no longer inhibit compaction. + set_foreach: usize = 0, + capture: crate::set::set_foreach_stack_savepoint, + restore: crate::set::set_foreach_stack_restore; + map_foreach: usize = 0, + capture: crate::map::map_foreach_stack_savepoint, + restore: crate::map::map_foreach_stack_restore; + prototype_resolution: usize = 0, + capture: crate::object::prototype_chain::resolution_stack_savepoint, + restore: crate::object::prototype_chain::resolution_stack_restore; + static_private_owner: usize = 0, + capture: crate::object::static_private_owner_stack_savepoint, + restore: crate::object::static_private_owner_stack_restore; + private_lexical_brand: usize = 0, + capture: crate::object::private_lexical_brand_stack_savepoint, + restore: crate::object::private_lexical_brand_stack_restore; + derived_super_binding: usize = 0, + capture: crate::object::derived_super_binding_stack_savepoint, + restore: crate::object::derived_super_binding_stack_restore; + private_member_access_hints: usize = 0, + capture: crate::object::private_member_access_hints_savepoint, + restore: crate::object::private_member_access_hints_restore; + #[cfg(feature = "regex-engine")] + regex_factory: usize = 0, + capture: crate::regex::site_test::active_factory_stack_savepoint, + restore: crate::regex::site_test::active_factory_stack_restore; + // #6559: rooted interpreter values AND the packed call depth. + #[cfg(feature = "dyn-eval")] + dyn_eval: u64 = 0, + capture: crate::dyn_eval::interp_savepoint, + restore: crate::dyn_eval::interp_restore; +} + +#[cfg(test)] +mod tests; diff --git a/crates/perry-runtime/src/exception/savepoints/tests.rs b/crates/perry-runtime/src/exception/savepoints/tests.rs new file mode 100644 index 0000000000..0f0865f133 --- /dev/null +++ b/crates/perry-runtime/src/exception/savepoints/tests.rs @@ -0,0 +1,136 @@ +use crate::exception::{catch_js_throw, current_try_depth, js_has_exception, js_throw}; +use std::fmt::Debug; + +/// Every registered subsystem must supply a witness that changes real state. +/// Test both nested landings with a nonempty outer scope, and run through the +/// production js_throw + C trampoline, never a test-only replay of cleanup. +pub(super) fn assert_nested_restore( + capture: fn() -> T, + restore: fn(T), + push: fn(u32), +) { + struct Reset { + value: T, + restore: fn(T), + } + impl Drop for Reset { + fn drop(&mut self) { + (self.restore)(self.value); + } + } + + let _lock = crate::gc::global_side_table_test_lock(); + let base = capture(); + // This guard lives outside every abandoned frame, including on a failing + // assertion in the catch path. It also keeps injected faults isolated. + let _reset = Reset { + value: base, + restore, + }; + let try_base = current_try_depth(); + push(1); + let outer = capture(); + assert_ne!(outer, base, "fixture must establish live outer state"); + let outcome: Result<(), f64> = catch_js_throw(|| { + push(2); + let inner = capture(); + assert_ne!( + inner, outer, + "fixture must change state inside the outer trap" + ); + let nested: Result<(), f64> = catch_js_throw(|| { + push(3); + assert_ne!( + capture(), + inner, + "fixture must change state inside the inner trap" + ); + js_throw(31.0) + }); + assert_eq!(nested, Err(31.0)); + assert_eq!( + capture(), + inner, + "inner catch must preserve its enclosing scope" + ); + assert_eq!(current_try_depth(), try_base + 1); + js_throw(47.0) + }); + assert_eq!(outcome, Err(47.0)); + assert_eq!( + capture(), + outer, + "outer catch must preserve the preexisting scope" + ); + assert_eq!(current_try_depth(), try_base); + assert_eq!(js_has_exception(), 0); + assert_eq!(catch_js_throw(|| push(4)), Ok(())); + assert_ne!( + capture(), + outer, + "normal completion must not replay cleanup" + ); + assert_eq!(current_try_depth(), try_base); +} + +pub(super) fn shadow(marker: u32) { + crate::gc::js_shadow_frame_push(marker); + crate::gc::js_shadow_slot_set(0, (marker as f64).to_bits()); + crate::gc::js_gc_temp_root_push((marker as f64).to_bits()); +} + +pub(super) fn runtime_handles(marker: u32) { + let scope = crate::gc::RuntimeHandleScope::new(); + scope.root_nanbox_f64(marker as f64); + std::mem::forget(scope); +} + +pub(super) fn call_method(_: u32) { + crate::object::test_enter_catch_method(); +} + +pub(super) fn pump(_: u32) { + crate::stdlib_pump::test_enter_catch_pump(); +} + +pub(super) fn set_foreach(_: u32) { + crate::set::test_enter_catch_foreach(); +} + +pub(super) fn map_foreach(_: u32) { + crate::map::test_enter_catch_foreach(); +} + +pub(super) fn prototype_resolution(_: u32) { + let owner = crate::object::js_object_alloc(0, 0); + assert!(crate::object::prototype_chain::test_resolution_stack_enter_and_forget(owner as usize)); +} + +pub(super) fn static_private_owner(marker: u32) { + crate::object::static_private_owner_push(marker as f64); +} + +pub(super) fn private_lexical_brand(marker: u32) { + crate::object::private_lexical_brand_push(marker as f64); +} + +pub(super) fn derived_super_binding(_: u32) { + // The production stack stores native alloca addresses. No binding is + // evaluated here, so an empty address suffices for the lifetime witness. + crate::object::js_derived_super_scope_push(std::ptr::null_mut()); +} + +pub(super) fn private_member_access_hints(marker: u32) { + crate::object::test_push_catch_private_hint(marker); +} + +#[cfg(feature = "regex-engine")] +pub(super) fn regex_factory(marker: u32) { + crate::regex::site_test::test_enter_catch_factory(marker); +} + +#[cfg(feature = "dyn-eval")] +pub(super) fn dyn_eval(marker: u32) { + crate::dyn_eval::root_push(marker as f64); + crate::dyn_eval::call_depth_enter().unwrap(); +} diff --git a/crates/perry-runtime/src/gc/roots/runtime_handles.rs b/crates/perry-runtime/src/gc/roots/runtime_handles.rs index 503d47c1fb..1752a32f6e 100644 --- a/crates/perry-runtime/src/gc/roots/runtime_handles.rs +++ b/crates/perry-runtime/src/gc/roots/runtime_handles.rs @@ -167,6 +167,7 @@ impl RuntimeHandleScope { /// Snapshot the transient-handle stack before a callback may throw across /// Rust frames. `longjmp` skips `RuntimeHandleScope::drop`, so exception /// unwinding restores this depth explicitly. +#[inline] pub(crate) fn runtime_handle_stack_savepoint() -> usize { with_runtime_handle_stack(|stack| stack.borrow().len()) } diff --git a/crates/perry-runtime/src/gc/roots/shadow_stack.rs b/crates/perry-runtime/src/gc/roots/shadow_stack.rs index 3f17b47f12..27c562c554 100644 --- a/crates/perry-runtime/src/gc/roots/shadow_stack.rs +++ b/crates/perry-runtime/src/gc/roots/shadow_stack.rs @@ -729,7 +729,7 @@ pub(crate) fn shadow_stack_has_active_frame() -> bool { /// reaches its `js_gc_temp_root_truncate`. That depth is therefore recorded /// here and restored with the frames, so one savepoint covers both precise /// root stacks and `crate::exception` needs no separate hook. -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub(crate) struct ShadowSavepoint { frame_top: usize, len: usize, @@ -749,6 +749,7 @@ impl ShadowSavepoint { /// Capture the current shadow-stack depth so it can be restored after a /// non-local exit. Call at `js_try_push` time, before the protected /// region can push any callee frames. +#[inline] pub(crate) fn shadow_stack_savepoint() -> ShadowSavepoint { SHADOW.with(|cell| unsafe { let s = &*cell.get(); diff --git a/crates/perry-runtime/src/gc/roots/temp_roots.rs b/crates/perry-runtime/src/gc/roots/temp_roots.rs index 8405fdf8d9..0bcbb877bc 100644 --- a/crates/perry-runtime/src/gc/roots/temp_roots.rs +++ b/crates/perry-runtime/src/gc/roots/temp_roots.rs @@ -176,6 +176,7 @@ pub extern "C" fn js_array_push_f64_temp_rooted(idx: u32, value: f64) { } /// Current depth — the value a savepoint records. +#[inline] pub(crate) fn temp_root_depth() -> usize { unsafe { (*hot_temp_roots()).len() } } diff --git a/crates/perry-runtime/src/lib.rs b/crates/perry-runtime/src/lib.rs index 264f395c30..7312879d3b 100644 --- a/crates/perry-runtime/src/lib.rs +++ b/crates/perry-runtime/src/lib.rs @@ -464,10 +464,16 @@ pub(crate) mod stdlib_pump { } } + #[cfg(test)] + pub(crate) fn test_enter_catch_pump() { + std::mem::forget(PumpDepthGuard::enter().0); + } + /// Capture the re-entrant stdlib-pump depth at `try` entry. A caught JS /// throw can longjmp past `PumpDepthGuard::drop`; exception handling uses /// this savepoint to keep the next top-level pump recognizable as a new /// tick (and therefore run its lifecycle hooks). + #[inline] pub(crate) fn pump_depth_savepoint() -> u32 { PUMP_DEPTH.with(|depth| depth.get()) } diff --git a/crates/perry-runtime/src/map.rs b/crates/perry-runtime/src/map.rs index 7fb8e0c0cb..78a6ae60da 100644 --- a/crates/perry-runtime/src/map.rs +++ b/crates/perry-runtime/src/map.rs @@ -43,6 +43,7 @@ fn map_foreach_leave(map: *const MapHeader) -> bool { }) } +#[inline] pub(crate) fn map_foreach_stack_savepoint() -> usize { MAP_FOREACH_STACK.with(|stack| stack.borrow().len()) } @@ -4027,3 +4028,8 @@ mod tests { #[cfg(test)] #[path = "map_tombstone_tests.rs"] mod map_tombstone_tests; + +#[cfg(test)] +pub(crate) fn test_enter_catch_foreach() { + map_foreach_enter(js_map_alloc(4)); +} diff --git a/crates/perry-runtime/src/object/call_method_depth.rs b/crates/perry-runtime/src/object/call_method_depth.rs index c48f1ea2ac..848e48dc59 100644 --- a/crates/perry-runtime/src/object/call_method_depth.rs +++ b/crates/perry-runtime/src/object/call_method_depth.rs @@ -60,6 +60,7 @@ impl Drop for CallMethodDepthGuard { /// dispatching). System unwinding does run those drops; guards remember their /// entry depths so the eager restore makes their later cleanup a no-op instead /// of a second decrement. See `crate::exception::{js_try_push, js_throw}`. +#[inline] pub(crate) fn call_method_depth_savepoint() -> u32 { CALL_METHOD_DEPTH.with(|d| d.get()) } @@ -70,3 +71,8 @@ pub(crate) fn call_method_depth_savepoint() -> u32 { pub(crate) fn call_method_depth_restore(depth: u32) { CALL_METHOD_DEPTH.with(|d| d.set(depth)); } + +#[cfg(test)] +pub(crate) fn test_enter_catch_method() { + std::mem::forget(CallMethodDepthGuard::enter("catch-savepoint").unwrap()); +} diff --git a/crates/perry-runtime/src/object/field_get_set.rs b/crates/perry-runtime/src/object/field_get_set.rs index dc9438fcd3..9ff187b390 100644 --- a/crates/perry-runtime/src/object/field_get_set.rs +++ b/crates/perry-runtime/src/object/field_get_set.rs @@ -291,8 +291,6 @@ pub(crate) use has_property::{ wide_key_index_lookup, wide_key_index_note_hit, WIDE_KEY_INDEX_MIN_KEYS, }; pub use has_property::{js_in_operator, js_object_has_property}; -#[cfg(test)] -pub(crate) use ic_miss::primitive_proto_method_name_static; pub(crate) use ic_miss::{ bind_primitive_proto_method_static, cannot_be_private_member_name, current_private_lexical_brand_value, is_array_method_value_name, @@ -309,6 +307,8 @@ pub use ic_miss::{ js_object_set_field_by_property_id, js_private_brand_add, js_private_brand_check, js_private_field_add, js_private_guard, PicCache, PicCacheSlot, PIC_CACHE_WORDS, }; +#[cfg(test)] +pub(crate) use ic_miss::{primitive_proto_method_name_static, test_push_catch_private_hint}; /// The one slow exit of the emitted generic property-get tower. Declared here /// rather than inside `ic_miss.rs` only because that file sits at the /// 2000-line cap; the source lives next to its sibling entries. diff --git a/crates/perry-runtime/src/object/field_get_set/ic_miss.rs b/crates/perry-runtime/src/object/field_get_set/ic_miss.rs index 9cedc25adb..0babcbf77e 100644 --- a/crates/perry-runtime/src/object/field_get_set/ic_miss.rs +++ b/crates/perry-runtime/src/object/field_get_set/ic_miss.rs @@ -1274,6 +1274,7 @@ pub(crate) fn private_lexical_brand_pop() { }); } +#[inline] pub(crate) fn private_lexical_brand_stack_savepoint() -> usize { PRIVATE_LEXICAL_BRAND_STACK.with(|stack| stack.borrow().len()) } diff --git a/crates/perry-runtime/src/object/field_get_set/ic_miss/private_member_access.rs b/crates/perry-runtime/src/object/field_get_set/ic_miss/private_member_access.rs index 3025840726..4ddb8b7b74 100644 --- a/crates/perry-runtime/src/object/field_get_set/ic_miss/private_member_access.rs +++ b/crates/perry-runtime/src/object/field_get_set/ic_miss/private_member_access.rs @@ -18,6 +18,7 @@ struct PrivateMemberAccessHint { is_write: bool, } +#[inline] pub(crate) fn private_member_access_hints_savepoint() -> usize { PRIVATE_MEMBER_ACCESS_HINTS.with(|hints| hints.borrow().len()) } @@ -373,3 +374,16 @@ fn throw_private_type_error(msg: &str) -> ! { let v = crate::value::JSValue::pointer(err as *const u8).bits(); crate::exception::js_throw(f64::from_bits(v)) } + +#[cfg(test)] +pub(crate) fn test_push_catch_private_hint(marker: u32) { + PRIVATE_MEMBER_ACCESS_HINTS.with(|hints| { + hints.borrow_mut().push(PrivateMemberAccessHint { + class_id: marker, + name: format!("catch-savepoint-{marker}"), + kind: 0, + is_static: false, + is_write: true, + }); + }); +} diff --git a/crates/perry-runtime/src/object/global_this/fetch_globals.rs b/crates/perry-runtime/src/object/global_this/fetch_globals.rs index 2051ae5807..f27c058eec 100644 --- a/crates/perry-runtime/src/object/global_this/fetch_globals.rs +++ b/crates/perry-runtime/src/object/global_this/fetch_globals.rs @@ -166,10 +166,17 @@ pub unsafe extern "C" fn js_global_or_console_property_by_name( /// sentinels directly (e.g. `globalThis.Array(3)`) returns undefined — /// best-effort no-op rather than throwing — and remains a known gap for /// non-String call-form constructors after re-binding the global to a local. +// This function's address identifies builtin constructors across separately +// linked runtime and stdlib archives. Thin LTO must not replace each copy with +// a different unrelated undefined-returning thunk. Keep one external symbol +// and a distinct body, even when changes elsewhere alter LTO partitioning. +#[no_mangle] +#[inline(never)] pub(crate) extern "C" fn global_this_builtin_noop_thunk( _closure: *const crate::closure::ClosureHeader, _arg: f64, ) -> f64 { + std::hint::black_box(global_this_builtin_noop_thunk as *const u8); f64::from_bits(crate::value::TAG_UNDEFINED) } diff --git a/crates/perry-runtime/src/object/mod.rs b/crates/perry-runtime/src/object/mod.rs index e15899d4e5..36ec3ef148 100644 --- a/crates/perry-runtime/src/object/mod.rs +++ b/crates/perry-runtime/src/object/mod.rs @@ -293,6 +293,8 @@ pub(crate) use field_get_set::{ private_member_access_hints_restore, private_member_access_hints_savepoint, scan_private_lexical_brand_roots_mut, }; +#[cfg(test)] +pub(crate) use this_binding::js_derived_super_scope_push; pub(crate) use this_binding::{ derived_super_binding_stack_restore, derived_super_binding_stack_savepoint, scan_implicit_this_roots_mut, static_private_owner_current, static_private_owner_pop, @@ -591,6 +593,8 @@ pub(crate) use keys_lookup::*; pub(crate) mod array_tail_transition; mod call_method_depth; +#[cfg(test)] +pub(crate) use call_method_depth::test_enter_catch_method; mod meta_accessors; use call_method_depth::CallMethodDepthGuard; pub(crate) use call_method_depth::{call_method_depth_restore, call_method_depth_savepoint}; diff --git a/crates/perry-runtime/src/object/prototype_chain.rs b/crates/perry-runtime/src/object/prototype_chain.rs index f9100deb31..9fcf0904f4 100644 --- a/crates/perry-runtime/src/object/prototype_chain.rs +++ b/crates/perry-runtime/src/object/prototype_chain.rs @@ -89,6 +89,7 @@ impl Drop for PrototypeResolutionGuard { } } +#[inline] pub(crate) fn resolution_stack_savepoint() -> usize { PROTOTYPE_RESOLUTION_STACK.with(|stack| stack.borrow().len()) } diff --git a/crates/perry-runtime/src/object/this_binding.rs b/crates/perry-runtime/src/object/this_binding.rs index b8421f2a19..5fe88a66f3 100644 --- a/crates/perry-runtime/src/object/this_binding.rs +++ b/crates/perry-runtime/src/object/this_binding.rs @@ -84,6 +84,7 @@ pub(crate) fn static_private_owner_current() -> Option { STATIC_PRIVATE_OWNER_STACK.with(|stack| stack.borrow().last().copied().map(f64::from_bits)) } +#[inline] pub(crate) fn static_private_owner_stack_savepoint() -> usize { STATIC_PRIVATE_OWNER_STACK.with(|stack| stack.borrow().len()) } @@ -327,6 +328,7 @@ pub extern "C" fn js_derived_super_scope_pop() { }); } +#[inline] pub(crate) fn derived_super_binding_stack_savepoint() -> usize { DERIVED_SUPER_BINDING_STACK.with(|stack| stack.borrow().len()) } diff --git a/crates/perry-runtime/src/regex/site_test.rs b/crates/perry-runtime/src/regex/site_test.rs index a21e388304..2f3f94a268 100644 --- a/crates/perry-runtime/src/regex/site_test.rs +++ b/crates/perry-runtime/src/regex/site_test.rs @@ -295,6 +295,7 @@ impl Drop for ActiveFactoryGuard { } } +#[inline] pub(crate) fn active_factory_stack_savepoint() -> usize { ACTIVE_FACTORY_SITES.with(|stack| stack.borrow().len()) } @@ -538,6 +539,11 @@ pub(super) fn test_header(site_key: usize) -> Option { lookup(site_key).map(|(header, _)| header as usize) } +#[cfg(test)] +pub(crate) fn test_enter_catch_factory(marker: u32) { + std::mem::forget(ActiveFactoryGuard::push(marker as usize, usize::MAX)); +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/perry-runtime/src/set.rs b/crates/perry-runtime/src/set.rs index 2011b8ead8..e8393cca3b 100644 --- a/crates/perry-runtime/src/set.rs +++ b/crates/perry-runtime/src/set.rs @@ -42,6 +42,7 @@ fn set_foreach_leave(set: *const SetHeader) -> bool { }) } +#[inline] pub(crate) fn set_foreach_stack_savepoint() -> usize { SET_FOREACH_STACK.with(|stack| stack.borrow().len()) } @@ -3309,3 +3310,8 @@ mod ordered_delete_repair_tests { #[cfg(test)] #[path = "set_tombstone_tests.rs"] mod set_tombstone_tests; + +#[cfg(test)] +pub(crate) fn test_enter_catch_foreach() { + set_foreach_enter(js_set_alloc(4)); +}