Skip to content

perf(runtime): classify native-call receivers from the tracked header; no Buffer/typed-array registry probe for known kinds - #9937

Draft
proggeramlug wants to merge 10 commits into
PerryTS:mainfrom
proggeramlug:perf/native-call-receiver-class
Draft

perf(runtime): classify native-call receivers from the tracked header; no Buffer/typed-array registry probe for known kinds#9937
proggeramlug wants to merge 10 commits into
PerryTS:mainfrom
proggeramlug:perf/native-call-receiver-class

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Runtime-only, on main 87dc33492. Written by codex from the campaign's caller tables; gated only partially (archive-feature build green; the runtime test suite and the default compiler build not yet run; the last commit not yet compiled). Perrymaster runs the full gate ladder and the rows; they will be appended here.

Why

On the best cc bundle, every native receiver.method() dispatch classifies its receiver through the Buffer / typed-array registries (gc_pointer_and_type_from_valueis_registered_buffer, lookup_registered_typed_array_kind, …): 11.5 M registry probes per 3300-char reply, 97 % of them rejections, 5.27 % of the main thread (125 of 2,373 samples), two thirds of it under the string-width loop. #9919 showed a cheaper probe does not help (a header read cost what the address filter cost); the cut has to be no probe for receivers whose kind is already known.

What changes

  • NativeReceiverClass: primitives by NaN-box tag return at once; managed addresses are admitted only by try_read_tracked_gc_header, whose GC type is authoritative, with ObjectHeader.class_id read only for GC_TYPE_OBJECT. Plain objects, boxed primitives, segment cursors and RegExp cells are allocator-owned allocations with explicit GC types, so they can never be a Buffer or typed array; the registries are consulted only after a tracked-header miss (external buffers registered without a GC header, the process-global SharedArrayBuffer backing, native-arena views) or for a finer Buffer / Uint8Array / ArrayBuffer / SAB brand that affects the requested operation.
  • js_native_call_method classifies once at the call boundary and carries the answer into class and handle dispatch; typed feedback forwards its site_id. A 64-slot thread-local site cache stores (site_id, gc_type, class_id) only, never a pointer; every hit revalidates type and class id; headerless answers are never cached.
  • The same allocator-proven decision replaces the registry questions in object_static_prototype, js_object_get_field_by_name / get_field_by_name_object_tail, js_object_get_field_ic_miss and dispatch_primitive (its registry question is confined to the residual untagged arm). The redundant Buffer retry at the end of Map/Set dispatch is removed.
  • Segment view: cursor_ptr requires POINTER_TAG, current-arena membership, GC_TYPE_OBJECT and the fixed cursor class id; js_segments_view_regexp_test validates its RegExp argument exactly once per call (regexp_test_str_bounded no longer repeats it).
  • Before-counter commit (8ad6e0777): a [native-call-diag] line splits the probes by caller (native_receiver, view_cursor, object_static_prototype, field_by_name, field_tail, dispatch_primitive, other), so the caller attribution is measured on the pre-fix runtime.

Tests (named, authored; not yet executed)

cached_plain_object_receiver_probes_zero_buffer_registries, cached_site_revalidates_when_plain_receiver_becomes_buffer, buffer_uint8array_and_arraybuffer_keep_their_method_paths, external_buffer_and_sab_backed_view_keep_native_dispatch, cached_receiver_kind_does_not_hide_reassigned_prototype, primitive_receiver_tag_skips_byte_storage_registries, view_cursor_brand_is_a_class_load_with_zero_registry_probes, view_regexp_pointer_is_validated_exactly_once_per_call.

Falsifiers on the cc reply

[buffer-diag] probes= from ≈ 11.5 M to ≤ 1,000,000 (prediction ≈ 0.8 M); rejected < 100,000; the registry group's self share from 5.27 % to ≤ 1 % with js_native_call_method inclusive down; turn CPU on 5 paired 3300-char turns vs the best bundle; RSS unchanged (the cache is 64 fixed entries per thread — any repeatable growth falsifies the mechanism).

Gates

Run locally: cargo build --release -p perry-runtime --features wasm-host green (after four mechanical pointer/unsafe fixes); llvm-nm -g on the archive shows every touched export. Not run (disk floor): the runtime lib test target (it compiled after a fixture re-export fix; 0 tests executed), cargo build --release -p perry default features; commit 72b713b99 (the field-IC residual) not compiled at all. Draft until perrymaster's ladder and rows are on this PR.

Gate history

  • 72b713b99 on perrymaster (picked onto the best tree, and separately on this branch's own base): default build green, archive feature set green, nm all seven exports present; runtime lib suite 3 failed on both trees, identically: view_regexp_pointer_is_validated_exactly_once_per_call (2 validations, not 1), cached_plain_object_receiver_probes_zero_buffer_registries (30–37 registry probes), cached_site_revalidates_when_plain_receiver_becomes_buffer (3–7 probes). So broken as filed, not a base interaction.
  • e02fb408b (this head's code): three bypasses fixed with the assertions untouched — the cold build_and_install_programs path repeated the RegExp brand validation (regex/lazy.rs); typedarray::is_offheap_sidetable_alloc entered both byte-storage registries unconditionally from the pre-dispatch header-safety checks, now tracked-header-first with the registry fallback only for headerless storage (typedarray/mod.rs); Object.prototype.toString probed both registries for the brand, now derived from the GC type and payload (object/to_string_tag.rs). The suite has not yet run on this head (disk); perrymaster's ladder runs it before any measurement.
  • e02fb408b on perrymaster (picked onto the best tree): default build, archive feature set and nm green; suite 1 failedcached_plain_object_receiver_probes_zero_buffer_registries (the second, site-cached toString on a plain object still entered the Buffer registry 5–8 times); the other seven tests, including the two named by the previous diagnosis, now pass.
  • bf1d91892 (this head's code): the residual path was array/indexing.rs — a plain object's internal keys array entering js_array_get_f64, which queried both registries unconditionally; it now reads the tracked GC type first and reserves the registries for headerless storage. Not yet run (disk); perrymaster's ladder runs it before any measurement.
  • 6870b1db3 (this head's code) on perrymaster, picked onto the best tree: default build green, archive feature set green, nm all exports; runtime suite 3235 passed / 0 failed / 4 ignored with all eight named tests green — the third path was the Map/Set subclass dispatcher unwrapping a plain receiver through the Buffer registry; it is now entered only when the tracked receiver class says Map/Set, the unwrap is header-first, and is_node_buffer follows the same discipline. The order-sensitive async-hooks failure seen once on the authoring host did not reproduce. Relinked with identity confirmed; the diag replies and rows follow.

Measured — NR6 (perrymaster: this head's six commits on the best tree, runtime relinked on the best bundle's cache, identity equal; quiet box, paired 5-round rotation)

Counters, one 3300-char reply per arm: best bundle [buffer-diag] probes=24 M, admits 27 %, true positives 241 k; the counter-only arm 28.3 M with the split by caller (Buffer: view cursor 17.5 M, native receiver 6.8 M, static prototype 5.5 M, field tail 1.7 M; typed-array 8.7 / 3.4 / 1.7 M); this head crossed 1,048,576 probes once with admits = 1050 = true positives and every classified bucket at 0 — ≈ 28 M → 1–2 M by the counter.
Rows vs the best bundle: 3300 +1.7 / +6.7 / +2.5 / −0.4 / +1.3 % (median +1.7 %, min-vs-min +4.4 %), 400 +0.0 / +5.4 %; peak and settled RSS flat; after-idle 3300 483 → 463 MB.
Draw (999 Hz, 2,297 samples vs 2,373): gone — is_registered_buffer_slow 1.61 → 0.04 %, is_registered_buffer 1.15 → 0.09, lookup_registered_typed_array_kind 0.55 → 0, object_static_prototype 0.69 → 0.09, js_object_get_field_ic_miss 0.72 → 0.23, js_segments_view_regexp_test inclusive 6.8 → 0.5 %; appeared — try_read_tracked_gc_header 0.37 → 2.46 % (callers: the layout-table nursery check, dispatch_primitive, the IC-miss path, js_native_call_method_at_site, js_array_get_f64, the field tail), js_native_call_method_at_site 2.90 vs 2.31, dispatch_primitive 1.33 → 1.79. The receiver-dispatch family sums to ≈ 12.5 % before and ≈ 9.9 % after, inside one draw's variance of the rows. Collector/mutator split unchanged (34.3 / 65.7 → 34.7 / 65.3).
Reading: the mechanism is proven and correct, and the turn did not move — the registries are no longer probed, but classifying the receiver from a validated header read (try_read_tracked_gc_header, which classifies the address before it trusts the header) costs about what the registry filter cost, per call. The same shape as #9919. A uprobe count of the out-of-line registry entries during a reply came out orders of magnitude above the diag counter, so whether the counter undercounts through an LLVM clone that carries no counter is being checked before the next step. Either way the next cut is not a cheaper check but no per-call check: trusting the tagged pointer's header word without address classification on the native-call path, which needs the invariant proven and tested.

Add a single diagnostic line splitting buffer and typed-array probes by the
hot callers identified in the cc profile. This establishes the before-fix
counter SHA independently of the receiver-classification change.
Use NaN-box tags and allocator-proven GC types to classify native-call
receivers, and carry typed-feedback site ids into a revalidated kind cache.
Only headerless external buffers, SAB backings, and native typed views now
consult the address registries.

Apply the same header-directed decision to prototype and named-field paths.
Validate segment-view cursors from their arena-backed fixed class id and avoid
the duplicate RegExp pointer validation below the exported view boundary.

Add counter-based sabotage tests for the skipped probes, receiver-kind changes,
prototype mutation, byte-storage brands, and the cursor/RegExp validation gates.
Use the allocator-proven GC type in the named-field IC miss before consulting
Buffer or typed-array registries. Preserve registry fallback for headerless
external, shared, and native-view storage.
Record the source map, counter and implementation SHAs, local gate outcomes,
and the exact runtime-only perrymaster relink and falsifier request.
@coderabbitai

coderabbitai Bot commented Sep 7, 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.

Trust tracked GC storage before consulting headerless Buffer and typed-array
registries, including Object.prototype.toString brand detection. Keep the
SegmentsView RegExp validation at its exported boundary by removing the cold
lazy-compiler recheck.
Record the pushed implementation, failure root causes, diagnostic evidence,
and the Cargo gates blocked by the mandatory disk threshold.

@jdalton jdalton left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of 4f257d3b40bd2378fa32aaebfd127b71e43ae1c6 (2026-09-07).

The tracked-header classification is a useful boundary, but the new RECEIVER_KIND_CACHE appears to add work without avoiding classification: classify_native_receiver loads the tracked header, gc_type, and class_id before probing the cache, and a hit returns exactly NativeReceiverClass::Gc(gc_type), which the miss already constructs without further inspection. Please measure a variant that removes this cache while retaining header-first classification; it removes a TLS/RefCell probe and possible store from each typed-feedback call. The cache-hit counter proves hits occur, not that they save work. Please also refresh the exact-head runtime results for the three previously failing counter tests before treating the performance evidence as validated.

Validation scope: source/diff inspection; I have not run this PR's build or test suite locally.

Ralph Küpper added 4 commits September 7, 2026 07:24
Reuse allocator-owned GC type metadata in the generic array element accessor.
Internal object keys arrays now bypass Buffer and typed-array side tables, while
headerless legacy receivers retain their registry-backed dispatch.
Record the exact cached-hit registry path, the tracked-header fix, remote code
SHA, and the Cargo gates prohibited by the zero-free-space disk check.
Preserve the class id in native receiver classification so ordinary GC
objects can skip the Map/Set subclass dispatcher unless their ancestry can
actually be a collection subclass. Make the subclass unwrap and neighboring
node-buffer prototype probe consult allocator-owned headers before residual
side registries.
Record the exact Map/Set-subclass probe path, cached-hit registry audit,
implementation SHA, and final verification results.
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.

2 participants