Repository navigation
perf(GC): make per-object layout O(1)-loadable — kill per-operation thread-local layout tracking (umbrella: method_calls/array-downgrade/object-property) #5094
Description
Summary
Several of Perry's worst benchmark gaps share one systemic root cause, not separate ones: Perry tracks per-object / per-slot GC layout (which slots hold raw-f64 numbers vs NaN-boxed pointers) in thread-local hashmaps (TYPED_LAYOUTS, LAYOUT_SLOT_MASKS) and queries or updates them on every field/element access. On macOS each access is a _tlv_get_addr (TLS accessor) + a hashmap hash+lookup. The all-numeric "unboxed" fast paths bypass it, but any heterogeneous shape, downgraded array, or raw-f64 class field falls into the per-op hashmap path, which dominates.
| Benchmark | Gap vs Node | Hot path | Per-op cost |
|---|---|---|---|
method_calls (#5093) |
~290× (3300 ms vs 11 ms) | this.field get/set in class methods |
js_typed_feedback_class_field_{get,set}_guard → class_field_fast_contract → layout_typed_raw_f64_slot_for_user (TLS TYPED_LAYOUTS) |
bench_numeric_array_downgrade |
~781×; ~21× over the same-shape numeric array | arr[i]=… on heterogeneous/any[] arrays |
js_array_set_f64_extend → note_array_slot → layout_note_slot (TLS), per write |
bench_object_property |
~17× after #5084 | dynamic property writes | same field-access guard family |
Evidence (measured)
- Profile (
sample): the downgrade hot loop is a storm of_tlv_get_addr(TLS) calls. - method_calls: removing the per-access
register_sitecall (perf(codegen): make typed-feedback site registration opt-in (3.6x on dynamic property access) #5084) → ~2%; an MRU cache overlayout_typed_raw_f64_slot_for_usermade it 2× SLOWER (3300 → 7050 ms). Inlining the guard is blocked because the raw-f64 check is a TLS hashmap lookup, not an O(1) header field. - numeric_array_downgrade: an
obj_typepre-filter that skips the per-element typedarray/buffer/set/map registry lookups gave only ~5%; a "skip the in-boundsRuntimeHandleScoperooting" fast path made it worse. So the cost isnote_array_slot→layout_note_slot(the TLS layout write), per element.
There is no cheap shortcut. The fix is to make per-object layout O(1)-loadable from the object/GC header so the layout check/update is an inline bit-test/bit-set instead of a thread-local hashmap op.
Proposed fix — "layout is canonical" header bit + static class mask
GcHeader is 8 bytes with a free _reserved: u16 and spare gc_flags bits. Add a bit meaning "this object's slot layout still matches its declared/canonical shape" (no downgrade yet):
- Class instances: when set, the authoritative raw-f64 mask is the compile-time
perry_typed_shape_raw_f64_mask_<class>global codegen already emits — a constant codegen can bit-test inline (no TLS). With the method already inlined (perf(transform): inline small this-using methods on exact receivers (1.6x on method_calls) #5092), LLVM LICM hoists the loop-invariant check out of the hot loop. - Arrays: when set, skip the per-write
layout_note_slotfor scalar-over-scalar in-bounds stores. - On downgrade (a pointer/string written into a canonical slot): clear the bit and fall back to today's per-object
TYPED_LAYOUTS/LAYOUT_SLOT_MASKSpath (unchanged). The GC scanner consults the bit.
The TLS hashmap exists today only to track downgrades; the common (no-downgrade) case does not need it.
Optional complement: store a compact inline mask in _reserved for small objects/arrays (≤16 slots) so even small downgraded shapes avoid the hashmap.
Correctness invariants (the crux — this is GC-internals, memory-corruption risk)
- GC scanner sees the truth. Per-slot pointer/raw-f64 determination after the change must equal today's (
gc/trace.rs:758,gc/copying.rs:309,579consult per-slotlayout_kind). A wrong mask = trace a number as a pointer (crash) or miss a pointer (use-after-free). - Representation. A slot read as raw
doublemust hold raw f64; the canonical bit must be cleared before the first non-number is observable in a canonical slot (publish-order discipline, likedescriptors_in_use). - Downgrade is monotonic + complete; the fallback path stays byte-for-byte current behavior.
- GC moves transfer the bit + mask (
gc/copying.rs:504layout_transfer).
Phasing (each independently shippable + verifiable)
- Spike + microbench harness; prototype the bit read-only and confirm it tracks downgrade under GC stress.
- Arrays first (lowest blast radius): wire the bit for arrays; skip
layout_note_slot+ write barrier for scalar-over-scalar in-bounds writes; scanner honors the bit. - Class fields: emit the inline guard in codegen (
expr/property_get.rs:1551,property_set.rs) using the header bit + static class mask, by-name fallback for the cleared-bit case. - Object property path if the mechanism generalizes.
Verification
- Full local parity (
./run_parity_tests.sh) — zero NEW regressions vs base (compare combined stdout+stderr per file).cargo test --release --workspace. - GC correctness: run under
PERRY_GC_VERIFY_EVACUATION=1,PERRY_GC_FORCE_EVACUATE=1,PERRY_GC_DIAG=1, andPERRY_GEN_GC=0(full mark-sweep) — these panic on the corruption modes a wrong mask causes. - Targeted tests: (a) write a non-number into a
number-typed class field via ananyalias, then read it back; (b) a numeric array that receives an object slot then is GC-evacuated mid-loop; (c) holey/sparse downgraded arrays. - Per-benchmark perf regression gate.
Risk: HIGH (memory-corruption class). Effort: L (GC-internals). Maintainer-driven; not for autonomous execution.
Prior groundwork on this line
- perf(codegen): make typed-feedback site registration opt-in (3.6x on dynamic property access) #5084 — typed-feedback
register_siteopt-in (object_property 3.6×). - perf(transform): inline small this-using methods on exact receivers (1.6x on method_calls) #5092 — inline small
this.fieldmethods (method_calls 1.6×; prerequisite — the guard must be inlined into the loop for the phase-3 hoist to pay off).
Files
gc/types.rs (GcHeader), gc/layout.rs (TYPED_LAYOUTS/LAYOUT_SLOT_MASKS, TypedLayoutDescriptor, layout_note_slot, layout_typed_raw_f64_slot_for_user), gc/trace.rs + gc/copying.rs (scanner + layout_transfer), array/indexing.rs + array/header.rs (note_array_slot), typed_feedback/guards.rs (class_field_fast_contract), codegen expr/property_get.rs:1551, expr/property_set.rs, typed_shape.rs.
Activity
TheHypnoo commentedon Jun 13, 2026
Update — precise localization of
bench_numeric_array_downgrade(M1 Pro)Investigated the array side directly. The cost is not inside the array setter functions, and it is not dominated by a single call — three surgical attempts moved the needle 0–5%:
obj_typepre-filter injs_array_get_f64/js_array_set_f64*→ ~5% (helps the GET path)note_array_slotscalar-aware (skip the layout-mask update + barrier when neither old nor new value is a pointer) → 0%incremental_mark_barrier_value's thread-local touch → 0%Why 0%: for an in-bounds
arr[i] = value, codegen does not call the array setter — it inlines the slot store and emits separate per-write runtime calls:js_array_set_f64_extendonly appears on the realloc fallback path. So the setter-internal layout tracking I optimized is dead code for this loop.Measured ~736 ns per
arr[i] = arr[i] + 1on M1 Pro (vs ~21 ns for a plain read) — split across the inline store + ~4 codegen-emitted non-inlined runtime calls (layout note, write barrier, numeric note, shadow rooting) + thejs_array_get_f64read. No single one dominates; it is death-by-many-per-element-calls.Implication for the fix
This is the codegen emission side of the same systemic issue. The clean wins are at codegen (
crates/perry-codegen/src/expr/write_barrier.rs):js_write_barrier_slot/js_array_note_numeric_write— both are no-ops for numbers. (Limited forany[]where the value is staticallyany, but covers typed numeric arrays.)js_gc_note_slot_layoutper-write call is the layout-mask update; making it skip cheaply for non-pointer stores needs the O(1) header-state work described in this issue (theSIDE_MASKclear is a thread-local hashmap touch).method_calls).A properly symbolicated profile is needed to rank the ~4 calls precisely — the release binary is stripped (
-Wl,--strip-debugvia auto-optimize), sosampleonly shows_tlv_get_addrstorms without function names.TheHypnoo commentedon Jun 13, 2026
Update — first concrete win landed: PR #5098 (
bench_numeric_array_downgrade4482 → 427ms, ~10.5× on M1 Pro).This is the array side of the umbrella: the per-write
js_gc_note_slot_layout(→layout_note_slot) thread-local hashmap touch on everyarr[i] = value. An A/B stub experiment confirmed it was the dominant cost (stubbing it: 11× faster; stubbing the write barrier: ~2%).The fix is the targeted, low-risk version of the design here:
js_gc_note_slot_layout_aware(parent, slot, new, old)skips the hashmap when neither old nor new is a heap pointer (slot pointer-ness unchanged → mask invariant preserved). Codegen loads the previous slot value before an in-place array element overwrite and routes only those through it. GC-stress verified (pointer↔number transitions underVERIFY_EVACUATION/FORCE_EVACUATE/full mark-sweep), full parity zero-new-regression.Still open under this umbrella:
method_calls/ class fields (perf(method dispatch): method_calls ~290× Node — remaining cost is per-field-access shape-guard calls (plan + standby) #5093): theclass_field_*_guardraw-f64 check is aTYPED_LAYOUTSthread-local lookup, not O(1) — needs the codegen inline-guard + the O(1) header raw-f64 mask described above.bench_object_property: the dynamic-property-write field-guard family.The
js_gc_note_slot_layout_awarepredicate + the GcHeader_reservedlayout-state machine (GC_LAYOUT_POINTER_FREE/SIDE_MASK, already present) are the building blocks for those.perf(gc): skip the per-write layout-mask hashmap on scalar-over-scala…
perf(codegen): inline the slot load for guarded numeric array index r…
proggeramlug commentedon Jun 16, 2026
Investigation: why #5093/#5198's inline class-field guard stays ~10%, and what the real blocker is
Followed up on the
method_callsleg after #5198 ("inline class-field shape guard fast path") merged. Findings (all measured locally; benchmarks are noisy on this box so numbers are best-of):1. The ~10% ceiling is the per-access flag load, not the fallback safepoint. #5198's
emit_class_field_inline_precheckreads@PERRY_CLASS_FIELD_INLINE_GUARD_DISABLEDwith aload_volatileon every access. That load is pinned inside the hot loop — the coldjs_typed_feedback_class_field_*_guardfallback is an opaque external store, so LLVM must assume it writes the global and cannot hoist the load. That single load defeats LICM. #5198's NOTE attributes the non-collapse to "the cold fallback call remains a GC safepoint that forces the shadow-rooted receiver to reload"; the safepoint is present in a collapsing variant too, so the dominant blocker is the flag load. (Relaxingvolatile→plain load changes nothing — still pinned by the cold call.)2. Removing the flag from the hot path does unlock the targeted collapse. With the per-access flag load gone (gating the escape-hatch/verify at compile time instead), LLVM hoists the whole loop-invariant shape check:
09_method_calls(foldable): ~3200 ms → ~36 ms (collapses; LLVM even const-folds the counter loop)delete/defineProperty(instance + prototype) / polymorphism; GC-safe underPERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1and full mark-sweep.3. But the flag is load-bearing for correctness, so dropping it isn't safe yet. Removing it regresses
test_gap_dup_class_name_field_layout(readsisOptional: trueinstead offalse). The inline deref logic is byte-identical to main's, so main is correct here only because its flag disables the inline path when descriptors come into use — masking a latent inline-path slot bug in the cross-module duplicate-class-name (#26/#321) scenario. The flag's global "descriptors-in-use ⇒ disable inline" is doing real work the per-objectOBJ_FLAG_HAS_DESCRIPTORScheck doesn't cover.Takeaway for the collapse (#5093): the path isn't "relax the volatile flag" — it's "fix the underlying inline-path slot bug in the dup-class-name case so the global disable-on-descriptors can be dropped." Once the inline path is correct without the global flag, removing the per-access load unlocks the ~2.7× collapse. This is the loop-versioning/deopt-exit work #5198 already flagged as open here.
(No shippable code change fell out of this — the feature is already in via #5198 — so I'm closing the exploratory PR. Recording this so the next attempt starts from the right blocker.)
fix(codegen): #5094/#26/#321 — derive GC raw-f64/pointer masks from t…
proggeramlug commentedon Jul 10, 2026
Scoping note from the #6180 default-on campaign (2026-07-10): this remains the top GC-adjacent PERF item after the incremental graduation, and the campaign added one synergy worth folding in — runtime allocations now stamp birth flags from a thread-local (
GC_BIRTH_EXTRA_FLAGS, allocate-black during budgeted cycles), which is exactly the kind of per-alloc TLS the header-bit strategy here would subsume: a canonical-layout bit AND the birth-mark could ride the same header word write. Sequencing recommendation: land after the flip PR (the collector's write patterns are now stable), as its own measured campaign — the per-op TLS elimination touches the codegen inline paths (lower_call.rs bump allocator, typed-feedback guards) that the GC work deliberately avoided. The issue's plan (canonical bit + compile-time class mask + downgrade fallback to today's TYPED_LAYOUTS) still matches the code as of main @ #6239.perf(runtime,gc): fix quadratic downgraded-array element writes (~4.3…
perf(gc): stop the runtime write barrier re-deriving its parent addre…
proggeramlug commentedon Aug 6, 2026
New data point for this umbrella, from a symbolicated profile of an allocation-heavy workload (
gc-handoff/bench/churn_alloc.ts— 20M{v: number, w: number}literals pushed into an array), filed as #7510.The table in this issue covers the access paths (
this.fieldget/set, array element writes, dynamic property writes). The construction and death path is a fourth member of the family, and on allocation-heavy code it is now the single largest cost:gc::layoutside tableslayout_forget_object(gc/layout.rs:336)layout_note_slot(gc/layout.rs:685)js_gc_init_typed_shape_layout(gc/layout.rs:962)_tlv_get_addrlayout_forget_objectalone is nearly twice the allocator.js_gc_init_typed_shape_layoutruns pernew,layout_forget_objectper object death — so this scales with allocation rate, not live set, which is why it dominates churn-shaped code and not retain/tree.The "layout is canonical" header-bit design proposed here applies directly; #7510 spells out what it means for construction/death specifically. Note #6893 closed the memory half of
TYPED_LAYOUTS(289 → 133 B/record) but left the CPU half untouched.proggeramlug commentedon Aug 6, 2026
Symbolicated profile data putting a number on the CPU half of this issue — it is now the single largest cost in object construction.
churn_alloc(object literal + push), quiet pinned host,PERRY_DEBUG_SYMBOLS=1, 1500 leaf samples:gc::layoutside tables —layout_forget_*14.5%,layout_note_slot7.9%,js_gc_init_typed_shape_layout7.7%,shape_install_*2.2%,LayoutSlot*1.3%_tlv_get_addrjs_object_alloc_class_inline_keys6.3%,arena_alloc*1.4%)Object construction is ~79% of
churn(subtractingpush_num, which runs at 2.7× vs node and shows the array machinery is basically fine). Of that construction cost, ~76% is GC and feedback bookkeeping and 8% is the allocation itself.Two things worth pulling out:
layout_forget_*alone is 14.5% — nearly twice the allocation. It runs on object death as well as construction, so this is not only a construction-path cost._tlv_get_addris this same subsystem. perf(runtime): cache the hot thread-locals so one allocation pays one _tlv_get_addr #7474 cached the hot thread-locals and took TLS from 34% to ~14–17%; the residue is partly here. One fix, two wins.#6893 fixed the memory half of
TYPED_LAYOUTS(289 → 133 B/record). The CPU half was never touched and is now the top lever in the whole mutator profile.Partial progress just landed: #7501 declares an all-pointer element layout once at array allocation instead of noting each pointer push, removing the per-push
layout_note_slot/js_array_note_numeric_writepair on proven arrays (verified by IR call census, 2 calls → 0 in the hot block). That addresses a slice of the 7.9%layout_note_slotrow on the array-store path only — it does not touchlayout_forget_*orjs_gc_init_typed_shape_layout, which are per-object and together are 22.2%.One measurement caveat for anyone benchmarking here:
PERRY_WRITE_BARRIERS=0makeschurn_allocslower (2.44 s → 5.21 s) because it also switches the collector out of evacuating mode. It cannot be used to bound barrier cost, and a run with it set is not a barriers-free baseline.perf(class-fields): typed slot stores for pointer-typed class fields (#…
perf(class-fields): typed slot stores for pointer-typed class fields (#…
perf(class-fields): a conforming pointer store skips the layout note (#…
test(class-fields): follow the CFG for the guarded bookkeeping region
perf(class-fields): a conforming pointer store skips the layout note (#…
proggeramlug commentedon Aug 14, 2026
Resolution: this umbrella's proposal is implemented, and its evidence base has been superseded
Re-audited against
origin/main@514ff7c9a(v0.5.1510). Every claim below isa citation into current main or a closed sibling ticket; nothing here rests on
"the issue is old".
1. The proposed fix shipped, in the functions this issue named
The issue proposed a "layout is canonical" bit in
GcHeaderplus acompile-time class raw-f64 mask, with the thread-local hashmaps kept only as
the downgrade fallback. That is what main does.
GC_OBJ_TYPED_LAYOUT_INTACT: u16 = 0x1000— bit 12 ofGcHeader._reserved—crates/perry-runtime/src/gc/layout.rs:80, whose doc comment states thisissue's invariant verbatim:
62 references tree-wide. The four phases the issue laid out:
gc/layout.rs:80-107GC_ARRAY_RAW_F64_LAYOUT0x80,GC_ARRAY_RAW_F64_HOLES0x1000 (#6011),GC_ARRAY_ELEMENT_SHAPE0x800 (#7480),GC_LAYOUT_ALL_POINTERS0x2000,GC_LAYOUT_POINTER_FREE0x4000 —gc/types.rs:1050-1092,gc/layout.rs:19-82. Codegen elides the per-element note viaarray_store_needs_layout_note(perry-codegen/src/expr/helpers.rs:186)perry-codegen/src/expr/class_field_inline_guard.rs— "The single per-objectGC_OBJ_TYPED_LAYOUT_INTACTbit … stands in for the thread-local raw-f64 layout probe" (lines 32-37)class_field_store_needs_layout_note/class_field_store_layout_note_is_conforming, both labelled "Phase 4b.1"/"Phase 4b.2 (#5094, refs #7510)" —expr/helpers.rs:196-240The named per-operation lookup is gated by the bit inside the very function
this issue pointed at, with the issue number in the comment
(
gc/layout.rs:584-618):Beyond the original proposal, the storage was collapsed twice more:
#6893 (shape-keyed
SHAPE_LAYOUTS, "shared keys_array IS a shape", churnpeak RSS 830 → 262 MB) and #7510/#7525 (
PER_OBJECT_LAYOUTS_NONEMPTYplus a4096-bit inline address filter,
gc/layout_tables.rs).2. The
_tlv_get_addrpremise is deadThis issue and its comments attribute a large share to the Darwin TLS
accessor.
docs/engine-plan.md:137tracks that row to completion:_tlv_get_addrgc::layout_tables::layout_forget_objectand states the conclusion in the plan itself:
(Also worth recording for anyone re-reading the old comments: the "Darwin has
no local-exec TLS so the accessor must stay call-free" framing is true of
std::thread_local!and false ofcrate::perry_thread_local!, which resolvesthrough pthread TSD from
TPIDRRO_EL0— established by #7469, whose findingwas that
_tlv_get_addrcost is call count, not call price.)3. Every benchmark row in the issue's evidence table has a closed successor
method_calls(#5093)changelog.d/7425: 90 ms → 10 ms (9.0×), "puts Perry at Node 26.5.1's 11 ms instead of 8× behind it"; hot loop 71 IR instrs → 10bench_numeric_array_downgradeexpr/index.rs,expr/write_barrier.rscomments)bench_object_propertygc::layout)shape_install_sharedentered once in a 20 M-object runchangelog.d/7525also retires this issue's own headline datum directly:Two of the comments on this issue proposed that removing the per-access
@PERRY_CLASS_FIELD_INLINE_GUARD_DISABLEDvolatile load would unlock a ~2.7×collapse. That has now been measured twice and refuted both times —
class_field_inline_guard.rs:13-27(repsel Phase 3b audit) andchangelog.d/7425("De-volatilizing it in the emitted IR and re-runningopt -O3gives 71 hot-path instructions either way"). The real fix wasPerry's own preheader hoist, which shipped.
4. The remaining "obvious extension" is a documented correctness trap
docs/engine-plan.md:177-208, "⛔ Two remedies that look obvious and are wrong.Do not rebuild them", item 2:
5. Re-measured on current main (v0.5.1510, Node 26.5.1)
Compiler and both static archives rebuilt from a clean worktree at
514ff7c9a(libperry_runtime.a23:23,libperry_stdlib.a23:30,perry23:31),
PERRY_RUNTIME_DIRpinned to thatrelease/, default auto-optimizingpipeline. The host is heavily contended (node's own
bench_numeric_array_downgradevaried 7 → 44 ms across five runs), so theload-independent figures are
/usr/bin/time -l, best of 3, with each engine'sown startup floor subtracted (node 537,045,322 instr / 193,521,152 cycles;
perry 25,092,026 / 10,644,738, from
console.log("x")).09_method_callsbench_numeric_array_downgradebench_object_propertyIn-program
Date.now(), 5 alternated pairs, checksums identical on every run:09_method_callsnode 16 ms / Perry 13 ms — Perry beats Node(
value:10000000both).bench_object_propertynode ~33 / Perry ~243 (~7.4x,was 17x).
bench_numeric_array_downgradeis drowned by host noise; take the2.21x cycles figure.
churn_alloc(the 20M-{v,w}workload #7510 profiled, where the family was17.4x): now 1.96x instructions / 1.53x cycles, work-only.
Two notes on what the two remaining rows are today, because neither is
per-object layout any more:
bench_object_propertywritesobj["field_" + j]into a fresh{}— anallocating dynamic key plus a
class_id == 0receiver. That is perf: generalize guarded object-write fast paths beyond the #6811 two-field loop #6812'sterritory (slice A / the class-id-zero row), not this issue's.
bench_numeric_array_downgradeis the heterogeneous-array element path,already served by the
_reservedelement bits.6. Two defects found while auditing, both filed separately
root_reload.rs'sNON_COLLECTINGcontainedjs_gc_layout_note_slot, asymbol that has never existed (the runtime exports
js_gc_note_slot_layoutand
js_gc_note_slot_layout_aware). Safe-direction, hence silent — but itforced a root reload at every emitted slot-layout note, including the one per
guarded array element store, which is this issue's hot path. Fixed with
regression coverage; see the linked PR.
docs/engine-plan.md's "⛔ do not rebuild" item 2 forbids OR-ingGC_OBJ_TYPED_LAYOUT_INTACTinto the inlinenew's header constant. perf(codegen): stamp a pointer-free shape's typed layout into the allocation header #7834shipped exactly that four days later, under three extra conditions the ⛔ text
does not consider, and the doc was never updated. The ⛔'s mechanical claim is
still literally true of
layout_note_slot—gc/layout.rs:627still asserts"exactly one [descriptor] is present for an INTACT object", the
Downgradearm is reachable only from
Some(verdict), and the fall-through pointer path(
:702-752) setsSIDE_MASKwithout clearing the bit — but there is adefence the ⛔ does not mention: every by-name own-slot overwrite calls
mark_object_dynamic_shape_unknownfirst(
object/field_set_by_name/fast_paths.rs:101+ 10 more sites), which for abaked object does not early-return and does clear the bit. I could not
settle it by experiment — my probe compiled to
declare, not the bake(
--trace llvmshows@PERRY_PER_OBJECT_LAYOUTS_ANYis never loaded), so itproves nothing. Flagging for perf(codegen): stamp a pointer-free shape's typed layout into the allocation header #7834/perf(object/GC): finish the common-object header shrink from 56 B to 40 B after shape-transition migration #8047 rather than claiming a clean bill.
7. Where the live work is
The object-representation ladder that grew out of this line is tracked
granularly and is still moving: #8067 (make ShapeId authoritative for keys,
live slots and class-object kind) → #8047 (finish the 56 B → 40 B common
object header). Keeping this umbrella open on top of them adds no information
and its own evidence table is now three profiles out of date.
Recommendation
Close as delivered + superseded. Six shipped changelog fragments name this
issue (
7193,7250,7525,7535,7686,7698), all four phases are inmain, all three benchmark rows have closed successors, and the plan of record
says the two named cost rows should not be worked. Follow-on work belongs to
#8067/#8047.
proggeramlug commentedon Aug 14, 2026
Closing per the audit above: all four phases of the proposed fix are in main and tagged with this issue number in ~30 source sites; every named child (#5093, #6893, #7510, #7578, #7469, #7511, #7480) is closed; the headline row
09_method_callswent from ~290x to beating Node (13 ms vs 16 ms, 0.45x on cycles);_tlv_get_addris 1.1% (#7565) anddocs/engine-plan.mdrecords that it andlayout_forget_object"are no longer levers and should not be worked".The two remaining benchmark rows are no longer per-object layout problems:
bench_object_propertyis an allocating dynamic key on aclass_id == 0receiver, now tracked as #8098 and #6812;bench_numeric_array_downgradeis the heterogeneous-array element path. Live successor work is #8067 -> #8047.One defect found during the audit is fixed in #8106 (a root-reload allowlist entry that has never matched a symbol, forcing a spurious reload at every slot-layout note). One is flagged unresolved for #7834/#8047:
docs/engine-plan.md's "⛔ do not rebuild" item 2 forbids exactly what #7834 shipped four days later, and the doc was never updated — see §6 above.perf(codegen): fix a root-reload allowlist symbol that has never matc…
perf(runtime): skip dead feedback observation on the property wrappers
perf(runtime): skip dead feedback observation + field-only shape read…
perf(runtime): megamorphic read stub cache (pure property read −12%) (#…
fix(codegen): PropertySet rejections honour sloppy mode (#9459); a wr…
perf(runtime): gate the numeric push guard's observation on feedback …
shapes: link-time static ShapeIds by content — runtime adoption + dri…