perf(runtime): classify native-call receivers from the tracked header; no Buffer/typed-array registry probe for known kinds - #9937
Conversation
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.
|
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 |
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
left a comment
There was a problem hiding this comment.
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.
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.
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_value→is_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 bytry_read_tracked_gc_header, whose GC type is authoritative, withObjectHeader.class_idread only forGC_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_methodclassifies once at the call boundary and carries the answer into class and handle dispatch; typed feedback forwards itssite_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.object_static_prototype,js_object_get_field_by_name/get_field_by_name_object_tail,js_object_get_field_ic_missanddispatch_primitive(its registry question is confined to the residual untagged arm). The redundant Buffer retry at the end of Map/Set dispatch is removed.cursor_ptrrequires POINTER_TAG, current-arena membership,GC_TYPE_OBJECTand the fixed cursor class id;js_segments_view_regexp_testvalidates its RegExp argument exactly once per call (regexp_test_str_boundedno longer repeats it).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 % withjs_native_call_methodinclusive 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-hostgreen (after four mechanical pointer/unsafe fixes);llvm-nm -gon 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 perrydefault features; commit72b713b99(the field-IC residual) not compiled at all. Draft until perrymaster's ladder and rows are on this PR.Gate history
72b713b99on perrymaster (picked onto the best tree, and separately on this branch's own base): default build green, archive feature set green,nmall 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 coldbuild_and_install_programspath repeated the RegExp brand validation (regex/lazy.rs);typedarray::is_offheap_sidetable_allocentered 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.toStringprobed 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.e02fb408bon perrymaster (picked onto the best tree): default build, archive feature set andnmgreen; suite 1 failed —cached_plain_object_receiver_probes_zero_buffer_registries(the second, site-cachedtoStringon 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 wasarray/indexing.rs— a plain object's internal keys array enteringjs_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,nmall 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, andis_node_bufferfollows 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 withadmits = 1050 = true positivesand 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_slow1.61 → 0.04 %,is_registered_buffer1.15 → 0.09,lookup_registered_typed_array_kind0.55 → 0,object_static_prototype0.69 → 0.09,js_object_get_field_ic_miss0.72 → 0.23,js_segments_view_regexp_testinclusive 6.8 → 0.5 %; appeared —try_read_tracked_gc_header0.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_site2.90 vs 2.31,dispatch_primitive1.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.