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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions benchmarks/runtime_handle_scopes/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Runtime handle scopes — 2026-09-14

Baseline: `9fda98df68d9fac3c08b2385fae007aa9f5278df` (Perry 0.5.1563).
Linux host: perrymaster.skelpo.net, x86_64. The baseline was built from a clean
worktree; the promise profile was recorded before runtime edits began.

## Instruction counts

Whole-program user instructions, `perf stat -e instructions:u -r 3`:

| workload | baseline | changed | reduction |
|---|---:|---:|---:|
| 100,000 promise/await iterations | 1,100,168,994 | 1,050,989,735 | 4.47% |
| 100,000 JSON round-trips | 3,190,619,152 | 3,132,631,719 | 1.82% |
| Original hoisted regex `test` probe | 13,773,038,016 | 13,003,278,589 | 5.59% |
| Original regex `exec1` probe | 31,188,918,430 | 29,854,497,805 | 4.28% |

Every binary's stdout matched Node 26.5.1. These are complete workload counts,
including startup and input construction, without subtracting a loop control.
No wall-clock speedup is claimed on the shared host. The measured build uses
the repository's release profile with 16 codegen units, debug level 1, and no
symbol stripping; both sides use identical settings and package selections.

`perf record -e instructions:u -c 1000003 --call-graph dwarf` produced the
folded stacks in `evidence/`. The pre-edit promise profile had 1,097 samples;
4.10% of leaves named runtime handles or operations on their slots. JSON had
3,189 samples and 3.98% such leaves. This classifier includes inlined primitive
slot loads/stores, not just function-call overhead. The corresponding changed
shares are 4.29% and 0.48%; residual handle work does not vanish in every probe.
Inclusive shares also contain callbacks invoked through handle helpers and
must not be interpreted as the cost of rooting itself. Instruction totals,
rather than these small sampled shares, are the performance verdict.

## Reproduction

Build each source tree independently with the same environment:

```sh
export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16
export CARGO_PROFILE_RELEASE_DEBUG=1
export CARGO_PROFILE_RELEASE_STRIP=none
export CARGO_INCREMENTAL=0
cargo build --release -p perry -p perry-runtime-static -p perry-stdlib-static
PERRY_RUNTIME_DIR="$PWD/target/release" PERRY_NO_AUTO_OPTIMIZE=1 \
python3 benchmarks/runtime_handle_scopes/measure.py \
--perry "$PWD/target/release/perry" --output /tmp/handle-profile
```

For the recorded A/B, `PERRY_BUILD_COMMIT` was pinned to the baseline SHA for
both complete builds and their compiler invocations. This intentionally permits
relinking the changed runtime while keeping coherence stamps equal; it does not
replace rebuilding both static wrappers. Compiler/archive hashes are retained
in `evidence/` and under `/root/runtime-handle-evidence/` on the measurement host.
The later GC stress build also selects `perry-ext-net` and `perry-ext-http`
to unify their Tokio configuration with the stdlib. Its runtime archive hash
is unchanged; its compiler and stdlib hashes differ from the measured build.
Raw perf data
and validation logs are also retained locally in
`/tmp/perry-runtime-handle-evidence/`.

## Contracts and tradeoffs

The live prefix is still `[0, top)`. Handles remain indices so buffer growth
does not invalidate them, and the scope lifetime and bounds/kind checks remain.
Scopes and handles cache a reference to non-dropping, thread-local metadata;
their `Cell`-containing referent prevents sending them between threads. Rust
scopes and handles grow from one machine word to two, while each root slot
shrinks from 24 to 16 bytes by encoding the raw tag in the variant discriminant.
The first push allocates 64 slots (1 KiB), and later growth doubles capacity.

The existing named hot-TLS pointer and its offsets stay in place. The fallback
metadata can be accessed without filling the hot cache, preserving #9183's
initialization contract. A separate TLS guard owns the buffer, unpublishes its
cache pointer on every target, and clears the live prefix before freeing it.
Late scope destructors see empty metadata; a push after buffer destruction
fails through the destroyed owner guard instead of resurrecting storage.

Scanners visit local slot copies and commit relocations by index. This avoids
references into reallocated storage, including a reentrant legacy Copy visitor;
a non-rewriting callback cannot have its own update overwritten. Throw restore
and scope drop both retain `Vec::truncate`'s non-growing semantics. The root
write barrier tests the existing global idle flag before decoding slot kind;
an active cycle still uses the existing per-thread shading machinery.

## Failure evidence

`fault-results.json` records all four injected faults:

| fault | observed failure |
|---|---|
| Scan `[0, top - 1)` | Moving-root test: final root 256 was not relocated. |
| Skip throw-savepoint truncation | Real caught-throw test: live depth does not return to its saved value. |
| Skip root shading during marking | Marking test: newly published handle target is still white. |
| Skip cache unpublication | Late TLS destructor asserts that the cache pointer is null; process aborts. |

All mutations were removed. The restored full runtime suite passed 3,802 tests,
with four ignored and zero failures. Regex-off checking passed. Runtime clippy
diagnostics match the clean baseline exactly, including its 12 existing
`approx_constant` errors in tests. The root-holder custody and raw-handle debt
ratchets pass without modifying their inventories.

The GC ratchet was measured with seven runs per probe on macOS arm64. Both
builds match Node on all 14 probes, and all 126 heap/cycle/copy/promotion/free
metric medians are identical. Both fail the same 30 cells against the older
pinned artifact; this is a reproduced baseline failure, not a green pinned
ratchet. The two compressed measurements, exact comparison, and shared list
of pinned failures are in `evidence/`. RSS and wall time are excluded from
this comparison, as required by the shared-host profile.

GC instrument smoke passes, including all 14 real probes and the forced
verification/evacuation checks. The full GC matrix matches Node in 595/602
cells (428 PASS, 167 UNVER); every required collection mode is live. The seven
failures are the HTTP/2 pending-event fixture in all arms: its server tries
port 443, already occupied on the host, and exits without callback output.
An independently rebuilt clean Linux baseline produces the identical empty
output and bind error; `http2-baseline-comparison.json` records both results.
In a private network namespace, both binaries time out after 20 seconds without
callbacks (`http2-netns-results.json`); this fixture remains unverified.
The matrix JSON is retained without suppressing those failures.

The full lint replay executes 83 gates: 80 pass, three fail, and two additional
CI-only checks are skipped. All three failures reproduce on the clean baseline:
public benchmark freshness, two dead-code errors in unchanged WebAssembly
helpers under the workspace warning check, and missing generated `bun-pty`
API documentation. The clean baseline compiler generates byte-identical API
documents to the changed compiler. The generated drift is retained as evidence
and is not included as an unrelated source change.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
640ea229e6db05f6fd866ab51fd9e639d7de0d8abd3386bf413b2f3e55ea578b /root/runtime-handle-scopes-20260914/target/release/perry
6ac20705316e387871b5b7e4188adeb9d76e3dcb30fc265e081b235b4c5766fe /root/runtime-handle-scopes-20260914/target/release/libperry_runtime.a
697f36b9c7821649e089877a43561ba22ce147aef7020d4e870ee7a090d06bd9 /root/runtime-handle-scopes-20260914/target/release/libperry_stdlib.a
Binary file not shown.
3 changes: 3 additions & 0 deletions benchmarks/runtime_handle_scopes/evidence/baseline-exec1.stat
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# started on Mon Sep 14 03:46:30 2026

31188918430;;instructions:u;0.00%;2404090881;100.00;;
Binary file not shown.
Binary file not shown.
3 changes: 3 additions & 0 deletions benchmarks/runtime_handle_scopes/evidence/baseline-hoist.stat
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# started on Mon Sep 14 03:45:11 2026

13773038016;;instructions:u;0.00%;845736469;100.00;;
Binary file not shown.
3 changes: 3 additions & 0 deletions benchmarks/runtime_handle_scopes/evidence/baseline-json.stat
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# started on Mon Sep 14 03:44:03 2026

3190619152;;instructions:u;0.00%;182814018;100.00;;
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# started on Mon Sep 14 03:43:37 2026

1100168994;;instructions:u;0.00%;67322145;100.00;;
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
Compiling perry-runtime v0.5.1563 (/Users/amlug/projects/perry/wt-runtime-handle-baseline/crates/perry-runtime)
Compiling aws-lc-rs v1.17.0
Checking aws-lc-sys v0.41.0
Checking rand_xorshift v0.4.0
Compiling phf_shared v0.13.1
Compiling rustls v0.23.43
Checking nom v8.0.0
Checking unarray v0.1.4
Compiling fastrand v2.4.1
Compiling phf_generator v0.13.1
Checking proptest v1.11.0
Checking rustls-webpki v0.103.13
Compiling ident_case v1.0.1
Compiling strsim v0.11.1
Checking crypto-bigint v0.7.5
Checking pkcs8 v0.11.0
Checking hashbrown v0.16.1
Checking concurrent-queue v2.5.0
Checking parking v2.2.1
Checking unicode-bidi v0.3.18
Checking unicode-properties v0.1.4
Checking tokio-rustls v0.26.4
Checking tungstenite v0.29.0
Checking hyper-rustls v0.27.9
Checking reqwest v0.12.28
Checking tokio-tungstenite v0.29.0
Checking stringprep v0.1.5
Checking event-listener v5.4.1
Compiling equator-macro v0.4.2
Compiling crunchy v0.2.4
Compiling tiny-keccak v2.0.2
Checking equator v0.4.2
Checking weezl v0.1.12
Checking hashlink v0.11.1
Checking aligned-vec v0.6.4
Compiling phf_codegen v0.13.1
Checking matchers v0.2.0
Checking sharded-slab v0.1.7
Checking console v0.15.11
Checking tracing-log v0.2.0
Checking thread_local v1.1.9
Compiling glob v0.3.3
Checking byteorder v1.5.0
Checking nu-ansi-term v0.50.3
Checking dialoguer v0.11.0
Checking tracing-subscriber v0.3.23
Compiling clang-sys v1.8.1
Checking v_frame v0.3.9
Checking crypto-primes v0.7.2
Checking pkcs1 v0.8.0-rc.4
Compiling getrandom v0.2.17
Checking num-rational v0.4.2
Checking as-slice v0.2.1
Checking atty v0.2.14
Checking which v8.0.6
Compiling paste v1.0.15
Compiling unicode-segmentation v1.13.3
Compiling once_cell v1.21.4
Compiling av-scenechange v0.14.1
Checking base64 v0.23.1
Compiling built v0.8.1
Checking rsa v0.10.0-rc.18
Compiling convert_case v0.10.0
Compiling rav1e v0.8.1
Compiling const-random-macro v0.1.16
Checking aligned v0.4.3
Compiling phf_macros v0.13.1
Checking no_std_io2 v0.9.4
Compiling profiling-procmacros v1.0.18
Compiling num-derive v0.4.2
Compiling arg_enum_proc_macro v0.3.4
Compiling regex-syntax v0.8.11
Checking quick-error v2.0.1
Compiling pastey v0.1.1
Checking imgref v1.12.3
Checking crc-catalog v2.5.0
Checking y4m v0.8.0
Checking alloc-no-stdlib v2.0.4
Compiling unicode-xid v0.2.6
Compiling minimal-lexical v0.2.1
Compiling derive_more-impl v2.1.1
Compiling nom v7.1.3
Checking alloc-stdlib v0.2.2
Compiling regex-automata v0.4.18
Checking crc v3.4.0
Checking phf v0.13.1
Checking profiling v1.0.18
Checking bitstream-io v4.10.0
Compiling const-random v0.1.18
Checking perry-container-compose v0.5.1563 (/Users/amlug/projects/perry/wt-runtime-handle-baseline/crates/perry-container-compose)
Checking av1-grain v0.2.5
Compiling string_cache_codegen v0.6.1
Checking rgb v0.8.53
Checking maybe-rayon v0.1.1
Checking miniz_oxide v0.8.9
Compiling libloading v0.8.9
Checking futures-intrusive v0.5.0
Checking tokio-stream v0.1.18
Checking crossbeam-queue v0.3.12
Checking itertools v0.14.0
Checking half v2.7.1
Compiling macro_magic_core_macros v0.5.1
Compiling derive-syn-parse v0.2.0
Compiling simd_helpers v0.1.0
Compiling fnv v1.0.7
Compiling either v1.16.0
Compiling noop_proc_macro v0.3.0
Checking zune-core v0.5.1
Compiling bindgen v0.72.1
Checking precomputed-hash v0.1.1
Checking zune-jpeg v0.5.15
Compiling itertools v0.12.1
Compiling darling_core v0.20.11
Compiling macro_magic_core v0.5.1
Checking sqlx-core v0.9.0
Compiling web_atoms v0.2.4
Compiling regex v1.13.1
Checking derive_more v2.1.1
Compiling cexpr v0.6.0
Checking brotli-decompressor v5.0.1
Checking loop9 v0.1.5
Compiling darling_core v0.23.0
Checking zune-inflate v0.2.54
Checking fdeflate v0.3.7
Checking core-foundation v0.10.1
Checking security-framework-sys v2.17.0
Checking avif-serialize v0.8.9
Compiling system-configuration-sys v0.6.0
Checking lebe v0.5.3
Compiling rustc-hash v2.1.2
Checking fax v0.2.7
Checking byteorder-lite v0.1.0
Compiling shlex v1.3.0
Checking color_quant v1.1.0
Checking pxfm v0.1.29
Compiling bitflags v2.12.1
Checking bit_field v0.10.3
Checking exr v1.74.0
Compiling darling_macro v0.23.0
Checking moxcms v0.8.1
Checking gif v0.14.2
Checking image-webp v0.2.4
Checking tiff v0.11.3
error: function `registered_extern_handle` is never used
--> crates/perry-runtime/src/object/global_this_webassembly.rs:201:15
|
201 | pub(crate) fn registered_extern_handle(wrapper: usize, expected_kind: &[u8]) -> Option<usize> {
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `-D dead-code` implied by `-D warnings`
= help: to override `-D warnings` add `#[expect(dead_code)]` or `#[allow(dead_code)]`

error: function `wasm_memory_descriptor_maximum` is never used
--> crates/perry-runtime/src/object/global_this_webassembly.rs:662:4
|
662 | fn wasm_memory_descriptor_maximum(descriptor: f64) -> u32 {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Checking ravif v0.13.0
Checking security-framework v3.7.0
Checking png v0.18.1
error: could not compile `perry-runtime` (lib) due to 2 previous errors
warning: build failed, waiting for other jobs to finish...
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

thread 'gc::tests::runtime_roots::handle_stack::handle_stack_growth_preserves_every_moving_root_including_last' (140172551) panicked at crates/perry-runtime/src/gc/tests/runtime_roots/handle_stack.rs:36:9:
assertion `left != right` failed: root 256 was not relocated
left: 5193047556104
right: 5193047556104
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
test gc::roots::runtime_handles::tests::handle_storage_teardown_clears_cache_before_late_scope_drop ...
thread '<unnamed>' (140212420) panicked at crates/perry-runtime/src/gc/roots/runtime_handles/tests.rs:69:13:
assertion failed: crate::tls_hot::hot().runtime_handle_stack.get().is_null()
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
fatal runtime error: thread local panicked on drop, aborting
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

thread 'gc::tests::runtime_roots::handle_stack::handle_stack_pushes_and_updates_shade_all_kinds_during_marking' (140202315) panicked at crates/perry-runtime/src/gc/tests/runtime_roots.rs:109:9:
assertion `left != right` failed: new handle during marking should be marked
left: 0
right: 0
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

thread 'gc::tests::runtime_roots::handle_stack::handle_stack_real_throw_restores_live_outer_scope_and_ffi_roots' (140196641) panicked at crates/perry-runtime/src/gc/tests/runtime_roots/handle_stack.rs:88:5:
assertion `left == right` failed
left: 259
right: 1
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
0fe4c749815c59704150d6d3cc27307e99c1cbf76b38d546ac7224dbc7b3d9ee target/release/perry
4c4ca74d1a1c2c44a4031484becbdaed81c9ad74f40714b0fb117ce90c3c894f target/release/libperry_runtime.a
59d55f6218322b4af78ac9fd91768990490c70c408cd7a63f80b1fae783c4317 target/release/libperry_stdlib.a
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
0a51732419d93c3d0840a3b579029ea5f4757053ec43d234234cb00d00f6baaf target/release/perry
4c4ca74d1a1c2c44a4031484becbdaed81c9ad74f40714b0fb117ce90c3c894f target/release/libperry_runtime.a
1cd675153e968f039b680651813e5606f55bab986757e40c7a98ad288941da29 target/release/libperry_stdlib.a
Binary file not shown.
3 changes: 3 additions & 0 deletions benchmarks/runtime_handle_scopes/evidence/fix-exec1.stat
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# started on Mon Sep 14 04:01:58 2026

29854497805;;instructions:u;0.00%;2506884207;100.00;;
Loading
Loading