Skip to content

perf(class-fields): a conforming pointer store skips the layout note (#5094) - #7698

Merged
proggeramlug merged 4 commits into
mainfrom
perf/7691-p3-conforming-note
Aug 9, 2026
Merged

perf(class-fields): a conforming pointer store skips the layout note (#5094)#7698
proggeramlug merged 4 commits into
mainfrom
perf/7691-p3-conforming-note

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

The P3 investigation resolved the ticket's contradiction as "already fixed"

P3 (gc-handoff/PROMPT-P3-class-canonical-layout.md) opens with a contradiction
to adjudicate before writing any fix: profiles showing 25–29% of class-heavy
benches in per-object layout side tables, against a shape_install_shared path
that is supposed to store one descriptor per shape with no per-object insert.
Two hypotheses were named. Both are false, and the ticket's own acceptance
criteria 1 and 2 are already satisfied on main — its table was measured at
a853135aa, before P1 (#7686).

Counters from an instrumented runtime, tree.ts:

INIT_CALLS        20447192     NOTE_CALLS          20447156
INIT_KEYS_ZERO           0  ←  NOTE_CONFORMS       20447154
INIT_MEMO_HIT     20447192     NOTE_DOWNGRADE             0  ←
INIT_SHARED_OK           1     FORGET_CALLS        64000001
INIT_SHARED_FAIL         0     FORGET_SLOW                1  ←
residency: slot_masks=0  typed_layouts=0  per_object_flag_armed=false
  • H1 — the keys_array is attached after the layout declaration. False.
    INIT_KEYS_ZERO = 0; every instance reaches shape_install_shared keyed.
  • H2 — a.peer = b demotes the descriptor. False. NOTE_DOWNGRADE = 0.
  • Criterion 2 (residency ~0) already holds. Both per-object maps are empty,
    the emptiness flag never arms, and layout_forget_object took its slow path
    once in 64 million calls.

What was actually left, and is fixed here

20,447,154 of those 20,447,156 js_gc_note_slot_layout calls return Conforms
— a cross-crate call, header decode, TLS touch and SHAPE_LAYOUTS hashmap
lookup per pointer store, to re-derive what codegen knew when it emitted the
mask. class_field_store_needs_layout_note's doc comment named this case, why
it was left alone, and the condition that would unblock it:

Deliberately NOT elided: a pointer-valued store into a pointer-masked slot.
[…] Closing that exit (#6921) is the prerequisite for the stronger elision.

#6921 is closed. This takes the elision as a live header test with the real
note kept on the cold arm
— see the changelog fragment for the two facts that
make the taken arm a proof (the #5093 precheck has already pinned the receiver's
keys_array to this class's keys global; the slot is in the pointer mask and
checked absent from the raw-f64 mask, so neither downgrade arm can fire).

Measured — quiet M1 mini, best-of-3, interleaved, stdout byte-identical

bench before after
cycles 0.33 0.29
tree_wide 7.90 7.77
tree 5.17 5.13

Unchanged: churn 1.21, push_cls 0.89, churn_alloc 0.89, retain 2.41,
retain1 0.04, retain_wide 3.38, retain_wide1 0.06, deeplist 0.03,
cls_mistyped 0.02. Peak RSS unchanged.

The profile states it more plainly than the clock: on cycles_big,
layout_note_slot is the second-heaviest leaf frame in the base arm (155
samples)
and is absent from the fix arm entirely. Wall-clock share is
smaller because these workloads are GC-dominated under the current default
pacing.

Safety

The intact bit is not made sticky and no downgrade path is touched.

  • cls_mistyped.ts (a number-declared field constructed with a heap string
    per instance — it must demote or the collector never traces those strings)
    still prints 20000 string payload-19999.
  • PERRY_GC_VERIFY_MARK=1 PERRY_GC_VERIFY_EVACUATION=1 clean over the bench set.
  • PERRY_GC_TRACE cycle counts and copied bytes identical between arms
    (churn 13, tree 20).
  • cargo test -p perry-codegen --lib 781 passed; -p perry-runtime --lib 1935
    passed. cargo fmt --all -- --check clean.

Three new tests, unit-level so cargo-test sees them (#5960), each verified able
to fail: the positive asserts the elision is reached (a predicate silently
answering false everywhere would otherwise be invisible), and the negative
asserts a slot in neither mask (flag: boolean) keeps its unconditional note —
sabotaging the predicate to return true makes that test fail.

Note for reviewers

Measuring P3 at all required #7690. On main before it merged, tree ran
204 s against an expected 4.4 s, churn 9.1 s with 1304 GC cycles against
105, with "copying_nursery":{"eligible":false} on every trace line — #7687 had
landed the immobility guard without the pacing half. cycles was nearly
unaffected (0.36 s), which is why that state was easy to miss. All numbers here
are on main with #7690 merged.

Summary by CodeRabbit

  • Performance

    • Improved garbage-collection bookkeeping for eligible class-field pointer stores by avoiding unnecessary layout notifications.
    • Preserved existing fallback behavior for non-eligible or uncertain cases.
  • Bug Fixes

    • Strengthened handling of class-field layout metadata, including pointer, raw floating-point, unmasked, and out-of-range slots.
    • Expanded validation across guarded and multi-block store paths.
  • Documentation

    • Updated the documented release version and added a changelog entry describing the optimization.
  • Tests

    • Added coverage for layout classification, optimized stores, fallback behavior, and demotion scenarios.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5a64177d-849b-4e16-a1d3-6169ba79e652

📥 Commits

Reviewing files that changed from the base of the PR and between 6106f69 and e6ddfbf.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • CLAUDE.md
  • Cargo.toml
  • changelog.d/7698-conforming-class-field-layout-note.md
  • crates/perry-codegen/src/expr/conforming_layout_note_tests.rs
  • crates/perry-codegen/src/expr/helpers.rs
  • crates/perry-codegen/src/expr/mod.rs
  • crates/perry-codegen/src/expr/property_set.rs
  • crates/perry-codegen/src/expr/write_barrier.rs
  • crates/perry-codegen/src/typed_shape.rs
  • crates/perry-codegen/tests/class_field_store_pointer_test.rs

📝 Walkthrough

Walkthrough

The code generator now identifies pointer-declared class slots and conditionally elides redundant layout-note calls after checking object header state. Tests validate conforming and fallback IR paths. The workspace version and changelog were updated.

Changes

Class-field layout-note elision

Layer / File(s) Summary
Detect conforming pointer slots
crates/perry-codegen/src/typed_shape.rs, crates/perry-codegen/src/expr/helpers.rs, crates/perry-codegen/src/expr/property_set.rs, crates/perry-codegen/src/expr/mod.rs
Typed layout metadata identifies pointer-declared fields. Class-field store paths pass class and field information to the conformance check.
Emit guarded layout-note bookkeeping
crates/perry-codegen/src/expr/write_barrier.rs
Conforming stores test parent-header layout state before calling js_gc_note_slot_layout. Non-conforming stores retain the existing behavior.
Validate IR behavior and record release changes
crates/perry-codegen/src/expr/conforming_layout_note_tests.rs, crates/perry-codegen/tests/class_field_store_pointer_test.rs, changelog.d/7698-conforming-class-field-layout-note.md, CLAUDE.md, Cargo.toml
IR tests cover pointer, undeclared, raw-f64, unmasked, and out-of-range slots. Bookkeeping analysis traverses guarded basic blocks. The changelog and version references were updated.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ClassFieldStore
  participant LayoutHelper
  participant WriteBarrier
  participant ParentHeader
  participant GCLayoutRuntime

  ClassFieldStore->>LayoutHelper: check class and field layout
  LayoutHelper-->>ClassFieldStore: conforming pointer-slot flag
  ClassFieldStore->>WriteBarrier: emit pointer-tested store
  WriteBarrier->>ParentHeader: test layout state
  ParentHeader-->>WriteBarrier: header state
  WriteBarrier->>GCLayoutRuntime: call js_gc_note_slot_layout on fallback
Loading

Possibly related issues

Possibly related PRs

  • PerryTS/perry#7486 — Both PRs conditionally elide class-field layout-note bookkeeping in property_set.rs.
  • PerryTS/perry#7536 — This PR extends the class-field store bookkeeping path used by that PR.
  • PerryTS/perry#7686 — Both PRs modify class-field pointer-slot lowering and layout-note behavior.

Suggested reviewers: thehypnoo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main optimization: skipping layout notes for conforming class-field pointer stores.
Description check ✅ Passed The description clearly covers the purpose, implementation, benchmarks, safety validation, tests, and related issue context.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/7691-p3-conforming-note

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.

Ralph Küpper added 4 commits August 9, 2026 16:24
The #5094 layout-note conditional splits the gc_bookkeeping block into three,
and LLVM emits gc_bookkeeping.done BETWEEN the entry and layout_note, so the
region is not textually contiguous -- neither the old first-block slice nor a
wider text slice describes it. Walk the CFG from the guard entry, stopping at
the join.

Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix
@proggeramlug
proggeramlug force-pushed the perf/7691-p3-conforming-note branch from 023f0b9 to e6ddfbf Compare August 9, 2026 14:30
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Audit — merging as v0.5.1404, after fixing two tests it broke

I could not break the elision. I could break the branch, and the way it broke is the interesting part.

Soundness: I tried the five ways this could be wrong

The note is a genuine no-op on the path being skipped — layout_note_slot returns without touching any state when the classify closure says Conforms (layout.rs:834-839). So the question is only whether the header test can be true while the real verdict would have been Downgrade.

attempt why it fails
subclass instance into a superclass-typed field layout_declares_pointer_slot asks "is this slot pointer-masked", not which class. A subclass instance is still a pointer.
undefined/null then a pointer the mask describes the slot's declared shape, not its current value
union / Any field type_is_pointer_bearing marks Any/Unknown/Union pointer-masked and type_is_raw_f64_candidate is Number-only, matching what the runtime installs
pointer → f64 → pointer a raw-f64-masked field never reaches this arm. The variant that is interesting — INTACT cleared by an earlier store — is handled: the header test is a live re-check per store, so a prior downgrade makes the very next store take the cold arm.
an IC keyed on the note firing the only other side effect is invalidate_representation_change, and it runs on Downgrade only — never on Conforms, so it is never wrongly skipped

The thread I pulled hardest: can the scanner trust INTACT+SIDE_MASK without finding a descriptor? No — heap_payload_slot_selection re-derives the mask at trace time and self-heals to GC_LAYOUT_UNKNOWN (scan everything) if both lookups miss. The trace path never depends on bookkeeping a skipped Conforms call would have done. Not a missing root. The write barrier is untouched.

What it actually broke, and why neither of us saw it

class_field_store_pointer_test.rstwo failures, guarded_store_leaves_every_block_terminated and opaque_param_store_guards_all_three_bookkeeping_calls, both "@js_write_barrier_slot must live inside the guarded block".

The barrier had not moved. Your new layout_note conditional splits gc_bookkeeping into three blocks, and gc_bookkeeping_block() sliced text from the label to the first br — which is the region only while it is one block:

gc_bookkeeping.4:     br i1 %r33, →layout_note.done.7, →layout_note.6
layout_note.6:        call @js_gc_note_slot_layout   → layout_note.done.7
layout_note.done.7:   call @js_write_barrier_slot    → gc_bookkeeping.done.5

layout_note.done.7 is reachable only from gc_bookkeeping.4, so the barrier is still dominated by the pointer-bearing guard. Sound; the test just could no longer see it.

A wider text slice would not have worked either — LLVM emits gc_bookkeeping.done.5 between gc_bookkeeping.4 and layout_note.6, so the region isn't textually contiguous. I replaced the slice with a CFG walk from the guard entry, stopping at the join (commit on the branch). That makes the assertion stronger than before: a call hoisted onto the not-taken edge or past the join now fails, whereas the old slice only ever proved a call wasn't in the first block. The test's existing negative assertion (!guarded.contains("store double")) still passes, which is what proves the region is a proper subset and not the whole IR.

Why CI couldn't tell you: per-PR cargo-test runs --lib --bins — no tests/*.rs suite runs on a PR at all, and e2e-scoped maps all of crates/perry-codegen/src/ to three suites, none of them this one. Full cargo test -p perry-codegen now shows 6 failures, byte-identical to the set main already has (those 6 are pre-existing and not yours — worth its own issue).

Two follow-ups, neither blocking

  1. No IR test covers the ptr_shape_proven call site (property_set.rs:1089) — the one arm with no runtime class_id/keys_array check, resting instead on the compile-time "exactly one Stmt::Let whose init is new C(...)" provenance rule. The two arms that are tested both have runtime guards.
  2. class_field_store_layout_note_is_conforming (helpers.rs:509) and typed_shape_init.rs:110 are independent copies of the same class_init_chains-preferred/name-keyed-fallback lookup. Consistent by construction today, with nothing enforcing it stays that way.

Gates 26/26.

@proggeramlug
proggeramlug merged commit 5c4ac16 into main Aug 9, 2026
1 of 13 checks passed
@proggeramlug
proggeramlug deleted the perf/7691-p3-conforming-note branch August 9, 2026 14:33
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