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
Draft
Conversation
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.
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
5f6a21734— the recordedRegExp.prototype.testproof becomes oneCanonicalTestSiteTLS 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-callmay_have_descriptor_entry(proto, "test", true)(registry probe +key_bytes_hash+ Bloom load) becomes one(*meta).accessor_key_bitsload against a compile-time FNV-1a mask;object_static_prototype_known_non_metareads theOBJECT_PROTOTYPES_NONEMPTYlatch first.b94c586a7—regexp_test_str_boundedno longer re-validates the regex pointer its only caller already validated.5b7c4fa94— the cursor's numeric fields are read through fixed slots (CursorFields, one base per entry) and_nextwrites 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) andutf16_len's second pass.Why
On the current best bundle (I7-view)
js_segments_view_regexp_testis 6.8 % of the thread inclusive, dominated by property loads (js_object_get_field2.3 %,object_static_prototype1.2 %) and TLS reads;js_object_set_fieldfor 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 perrydefault features,nmfor the exported symbols, the runtime suite, the five tests above. Draft until they run.Gate history
5b7c4fa94on perrymaster (archives stamped af9227369):cargo build --release -p perryrc=0; the archive feature set (--features perry-runtime/wasm-host) rc=0 withT js_regexp_testand 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 13view_mode_tests. Next: the I8-view relink on I7-view's cache (identity byobjdumpcall-site counts 20/10), the probe compile, then rows vs I7-view. Stays draft until the rows are on this PR.