Skip to content

perf(intl): Segments view — one realm record for the test proof, fixed-slot cursor fields, no duplicate pointer validation - #9917

Draft
proggeramlug wants to merge 6 commits into
PerryTS:mainfrom
proggeramlug:perf/segview-percall
Draft

perf(intl): Segments view — one realm record for the test proof, fixed-slot cursor fields, no duplicate pointer validation#9917
proggeramlug wants to merge 6 commits into
PerryTS:mainfrom
proggeramlug:perf/segview-percall

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Stacked on #9893 (perf/segview-levers @ f076656). Three runtime-only commits, written by codex from the segmenter lane's per-call read list, not yet compiled (the dev box was out of disk); gates and measurement run on perrymaster and will be appended here.

What changes, per commit

  1. 5f6a21734 — the recorded RegExp.prototype.test proof becomes one CanonicalTestSite TLS record (prototype pointer, canonical NaN-boxed closure, field index) instead of three TLS slots; both roots stay GC-visited (visit_atomic_i64_slot / visit_atomic_nanbox_u64_slot). The per-call may_have_descriptor_entry(proto, "test", true) (registry probe + key_bytes_hash + Bloom load) becomes one (*meta).accessor_key_bits load against a compile-time FNV-1a mask; object_static_prototype_known_non_meta reads the OBJECT_PROTOTYPES_NONEMPTY latch first.
  2. b94c586a7regexp_test_str_bounded no longer re-validates the regex pointer its only caller already validated.
  3. 5b7c4fa94 — the cursor's numeric fields are read through fixed slots (CursorFields, one base per entry) and _next writes slots 1–4 through a number-only store with no barrier.

Unchanged by design: with_input's per-entry re-derivation of the input (the §9a rooting contract) and utf16_len's second pass.

Why

On the current best bundle (I7-view) js_segments_view_regexp_test is 6.8 % of the thread inclusive, dominated by property loads (js_object_get_field 2.3 %, object_static_prototype 1.2 %) and TLS reads; js_object_set_field for the cursor's four integers was 1.6 %. The match itself is 0.8 %.

Tests (named; sabotage stated in each)

an_accessor_installed_after_recording_makes_the_next_call_decline, a_patched_prototype_test_declines_on_the_next_call, canonicality_proof_walks_once_per_realm_not_once_per_call, bounded_regex_test_does_not_repeat_entry_pointer_validation, cursor_position_fields_are_never_pointer_typed.

Gates

Run: rustfmt, git diff --check, scripts/gc_runtime_root_holders.py (pass), scripts/check_file_size.sh (pass). Not run (disk): cargo build --release -p perry default features, nm for the exported symbols, the runtime suite, the five tests above. Draft until they run.

Gate history

  • 5b7c4fa94 on perrymaster (archives stamped af9227369): cargo build --release -p perry rc=0; the archive feature set (--features perry-runtime/wasm-host) rc=0 with T js_regexp_test and the five view symbols present; RUST_TEST_THREADS=1 cargo test --release -p perry-runtime --lib -- --test-threads=1: 3230 passed, 0 failed, 4 ignored. Named tests green: an_accessor_installed_after_recording_makes_the_next_call_decline, a_patched_prototype_test_declines_on_the_next_call, canonicality_proof_walks_once_per_realm_not_once_per_call, bounded_regex_test_does_not_repeat_entry_pointer_validation, cursor_position_fields_are_never_pointer_typed, and the 13 view_mode_tests. Next: the I8-view relink on I7-view's cache (identity by objdump call-site counts 20/10), the probe compile, then rows vs I7-view. Stays draft until the rows are on this PR.

Ralph Küpper added 6 commits September 6, 2026 16:58
… walk

`js_segments_view_regexp_test` asks "is `RegExp.prototype.test` still the
builtin?" twice per grapheme, and the proof cost more than the match it guards.
Symbolised on the view arm of the string-width probe, the proof was ~13 % of the
thread — `get_field_by_name_object_tail` 3.6, `js_object_get_field_by_name` 3.5,
`get_accessor_descriptor` 2.1, `closure_get_dynamic_prop` 1.75,
`RandomState::hash_one<&str>` 1.4 (it hashed the key string on every call),
`js_object_get_prototype_of` 1.3 — against 0.8 % for `regexp_test_str_bounded`,
the actual matching.

The property belongs to `RegExp.prototype`, not to the call, so it is recorded
once when the prototype's methods are installed: the prototype pointer, the
FIELD INDEX of its own `test`, and the canonical closure value. A call reads
that slot by index and compares — three loads — plus the per-key accessor Bloom
bit off the meta record. Everything it can get wrong, it gets wrong in the
declining direction: a replaced or deleted `test` no longer matches the recorded
closure; a reshaped prototype makes the index hold something else, which also
does not match; `defineProperty(proto,"test",{get})` leaves the data slot alone
and is caught by the accessor bit; a reparented receiver is caught by
`object_static_prototype`, which answers from the object's own meta record or an
atomic "nothing was ever recorded" latch — no mutex, no chain walk.

Deliberately NOT a flag invalidated from the property-set path: that design
makes every property store in the program pay for this one question and adds an
invalidation surface that fails silently. This one hooks no shared write path.

`REGEXP_PROTOTYPE_TEST_WALKS` counts by-name walks. The fast path does none, so
it counts realms rather than calls, and the tests pin the property that matters:
50 accepted calls, plus a second cursor and a second regex, add ZERO walks; and
patching `RegExp.prototype.test` after the site is recorded makes the very next
call decline, so the caller materialises and runs the user's function.

Also removes `report_segview_counters`, which had no caller. The counters now
have one — the test suite — and a comment says to wire a runtime-side printer
when a rig run needs the numbers, not before.

`cargo test -p perry-runtime --release --lib -- --test-threads=1`: 3,239 passed,
0 failed.
Every view entry point re-derives the input `&str` from the cursor's traced slot
per call — that is the §9a rooting contract and it stays — but each derivation
also re-ran `std::str::from_utf8` over the WHOLE input. On the symbolised view
arm of the string-width probe that was the top self symbol at 6.2 %.

`open` already validates: it refuses an input that is not already a string
primitive and runs `from_utf8` on its bytes before allocating the cursor. So the
per-call validation re-establishes something the slot's only writer guaranteed.

The borrow now uses `from_utf8_unchecked`, with the invariant written out where
the `unsafe` is, in four checkable parts: `F_INPUT` is written exactly once, by
`open`, and never reassigned; `open` validated that value; a collection MOVES
the string but never rewrites its bytes, and the traced slot is updated to the
new address; the SSO path decodes the same value into the stack buffer. A
`debug_assert` re-checks it in debug builds, which is where a future second
writer to the slot would be caught.

`cargo test -p perry-runtime --release --lib -- --test-threads=1`: 3,239 passed,
0 failed.
The canonicality fast path records the prototype address and the canonical
`test` closure and reads them on every call — and neither was scanned. An
address held across a collection without being visited is stale the first time
the collector moves the object, which is the PerryTS#9539 / PerryTS#9445 shape and exactly
what this campaign keeps finding. Nothing had failed yet because a realm
prototype is long-lived and rarely moves; that is luck, not a design.

The packed `(index << 48) | ptr` word is split so each part can be handled
correctly:

* `REGEXP_PROTOTYPE_PTR` — a raw address, visited by
  `scan_object_cache_roots_mut` with `visit_atomic_i64_slot` beside the
  iterator-prototype towers, so a move rewrites it;
* `REGEXP_PROTOTYPE_TEST_CLOSURE` — a NaN-boxed word, visited with
  `visit_atomic_nanbox_u64_slot` and stored through
  `runtime_store_root_atomic_nanbox_u64` with the GC_STORE_AUDIT(ROOT) note the
  other mutable roots carry, so the pointer inside it is rewritten too;
* the field index is not an address and stays an ordinary atomic.

The per-call cost is unchanged — three loads and the accessor Bloom bit — and
the identity compare stays an identity compare across a move, because both
sides are now maintained by the collector.

`cargo test -p perry-runtime --release --lib -- --test-threads=1`: 3,239 passed,
0 failed.
@coderabbitai

coderabbitai Bot commented Sep 6, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant