Skip to content

perf(runtime): #6759 Phase C2 — per-key descriptor summaries in the ObjectMeta record#6800

Merged
proggeramlug merged 5 commits into
mainfrom
perf/6798-c2-shape-attrs
Jul 22, 2026
Merged

perf(runtime): #6759 Phase C2 — per-key descriptor summaries in the ObjectMeta record#6800
proggeramlug merged 5 commits into
mainfrom
perf/6798-c2-shape-attrs

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Phase C2 of #6759 / #6798: per-key descriptor-presence facts move into the per-object ObjectMeta record, so the hot read/write/reflection paths can prove "no descriptor can cover this key" in two loads instead of a per-call String build + side-table probe.

What changed

  • ObjectMeta grows two POD Bloom wordsattr_key_bits / accessor_key_bits, bit fnv(key) & 63 per installed key, monotonic per object. The GC trace arm is untouched (still visits only prototype); the record size flows from size_of at the one alloc site.
  • Every insert path notes the bit first (set_property_attrs, set_accessor_descriptor, and both gate-neutral set_builtin_* variants — the only four insert sites, verified by audit; everything else is removes/scans). For meta-capable owners this maintains the invariant the probes rely on: a clear bit — or a still-null meta — proves the tables hold no entry for that key.
  • get_property_attrs / get_accessor_descriptor gate on the summary: authoritative misses return None with no allocation. All ~30 call sites (read/write accessor gates, prototype-chain walks, for-in/Object.keys enumerability filtering, JSON getter checks, Reflect) get the per-key precision automatically.
  • Object.keys fast path replaces its per-call O(table-size) owner scan with the owner-level summary check.
  • plain_data_write_may_intercept refines OBJ_FLAG_HAS_DESCRIPTORS per-key: an object with a descriptor on one key (webpack's defineProperty(exports, "__esModule", …)) keeps the dynamic-write fast path for its other keys.
  • New GcSuppressScope (flag-only RAII no-move window): object_meta_ensure allocates, and a triggered collection could move the owner while installers (freeze/seal loops, defineProperty) hold raw pointers across repeated installs. All GC triggers check GC_FLAG_SUPPRESSED (gc_check_trigger, budgeted stepper, safepoint moving minor), so the tiny window is genuinely move-free. Nesting-safe (restores prior state).
  • Non-meta-capable owners (handle-band ids, typed arrays, RegExp, closures) stay on the conservative probe-always arm — behavior unchanged.

Why not the design doc's per-shape attrs sidecar

Shape records are keyed on the keys_array address with validation-on-hit — a trust model that only works for positive facts. "No descriptors on this shape" is a negative fact with nothing to validate against, and keys identity churns on every grow-realloc, so a shape-resident claim goes stale undetectably. The meta record is the sound carrier: it travels with the object (GC-traced, moved + rewritten with its owner) and is null on every fresh allocation — which also means no shape-split is needed (no O(N) keys clone on freeze). True shape-resident attributes return in C3 when shape identity becomes header-resident and descriptor install becomes an explicit transition. docs/shape-tree-plan.md updated accordingly.

Bonus correctness: a fresh object at a recycled address can no longer be misread as owning a dead tenant's not-yet-pruned descriptor entries (null meta ⇒ authoritative miss).

Validation

  • cargo test -p perry-runtime --lib -- --test-threads=1 green, including two new tests: summary gating semantics (fresh-owner miss, installed-key hit, non-colliding miss, attr/accessor word separation, handle-band conservative round-trip) and a copied-minor move test (bits travel with the moved meta record while the table entry rekeys — gated getter resolves at the new address).
  • Gap suite run against node --experimental-strip-types — results in PR comment.

Per the #6759 method: lands independently behind green suites; runtime-only (no codegen change, no header change).

Summary by CodeRabbit

  • Performance
    • Improved object property and accessor descriptor lookups by quickly ruling out keys that cannot have descriptors.
    • Reduced unnecessary scans during object-key enumeration and dynamic property writes.
  • Reliability
    • Improved descriptor metadata handling when objects are moved by garbage collection.
    • Added safeguards for recycled objects and nested garbage-collection suppression.
  • Tests
    • Added coverage for descriptor lookups, metadata preservation, installation, clearing, and object movement.
  • Documentation
    • Updated the shape-tree design documentation to reflect the revised descriptor metadata approach.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Phase C2 stores per-key descriptor summaries in ObjectMeta, gates descriptor and enumeration probes, refines write interception, adds GC suppression scope support, and adds tests for fresh owners, non-meta owners, and copied-minor moves.

Changes

Descriptor summary gating

Layer / File(s) Summary
Object metadata summary fields
crates/perry-runtime/src/object/mod.rs, crates/perry-runtime/src/object/prototype_chain.rs
ObjectMeta now stores initialized attribute and accessor Bloom-word summaries, with meta-capable lookup access exposed within the crate.
Descriptor summary recording and verdicts
crates/perry-runtime/src/object/descriptor_state.rs, docs/shape-tree-plan.md
Descriptor keys are recorded in per-object summaries, with conservative per-key and per-owner verdict helpers documented for the C2 design.
Gated consumers and validation
crates/perry-runtime/src/object/descriptor_state.rs, crates/perry-runtime/src/object/field_get_set/enumeration.rs, crates/perry-runtime/src/gc/tests/dead_owner_side_tables.rs, changelog.d/6800-object-meta-descriptor-summaries-phase-c2.md
Property, accessor, write-interception, and Object.keys paths skip impossible descriptor probes; tests cover installation, clearing, non-meta owners, and copied-minor evacuation.

GC suppression scope

Layer / File(s) Summary
Nesting-safe suppression guard
crates/perry-runtime/src/gc/policy.rs
Adds suppression-state inspection and GcSuppressScope, which restores the prior suppression state when the scope exits.

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

Possibly related issues

Possibly related PRs

  • PerryTS/perry#6795 — Shares the descriptor lookup helpers that this PR extends with metadata gating.
  • PerryTS/perry#6796 — Provides the ObjectMeta and GC meta-slot infrastructure extended by this PR.

Suggested reviewers: thehypnoo

Sequence Diagram(s)

sequenceDiagram
  participant DescriptorInstaller
  participant ObjectMeta
  participant DescriptorLookup
  participant DescriptorTable
  DescriptorInstaller->>ObjectMeta: record descriptor key bits
  DescriptorLookup->>ObjectMeta: check owner and key summary
  ObjectMeta-->>DescriptorLookup: possible entry or authoritative miss
  DescriptorLookup->>DescriptorTable: probe only when possible
  DescriptorTable-->>DescriptorLookup: descriptor attributes or accessor
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately reflects the main runtime performance change.
Description check ✅ Passed The description covers summary, changes, motivation, and validation, but it does not follow the template headings exactly.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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/6798-c2-shape-attrs

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (4)
crates/perry-runtime/src/gc/policy.rs (2)

646-684: 🩺 Stability & Availability | 🔵 Trivial

Rebuild the linked runtime archives during validation.

This runtime change requires rebuilding perry-runtime-static and perry-stdlib-static, along with the relevant runtime crates, so tests do not run against stale archives.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/gc/policy.rs` around lines 646 - 684, Update the
validation/build workflow for the GcSuppressScope change to rebuild
perry-runtime-static and perry-stdlib-static, including the relevant runtime
crates, before running tests. Ensure validation uses these freshly generated
archives rather than stale linked artifacts.

Source: Coding guidelines


663-668: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Mark the RAII guard as must-use.

Calling GcSuppressScope::new() without retaining the result immediately drops it, so suppression lasts for no time. Add #[must_use] to prevent accidental misuse.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/gc/policy.rs` around lines 663 - 668, Add a
#[must_use] attribute to the GcSuppressScope RAII guard, targeting the
GcSuppressScope type or its new constructor, so callers are warned when the
returned guard is immediately discarded. Preserve the existing suppression
behavior and constructor logic.
crates/perry-runtime/src/gc/tests/dead_owner_side_tables.rs (1)

667-746: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

if x_bit != 0 guard is always true.

descriptor_key_bit_bytes returns 1u64 << (hash & 63), which can never be 0, so this condition never gates anything (likely copy/paste from the x_bit != other_bit check above). Harmless — the assertion still runs — but the guard is dead weight.

🧹 Optional cleanup
-        // The attr install must not set the ACCESSOR word.
-        if x_bit != 0 {
-            assert!(
-                !crate::object::descriptor_state::may_have_descriptor_entry(addr, "x", true),
-                "attr install must not claim a possible accessor entry"
-            );
-        }
+        // The attr install must not set the ACCESSOR word.
+        assert!(
+            !crate::object::descriptor_state::may_have_descriptor_entry(addr, "x", true),
+            "attr install must not claim a possible accessor entry"
+        );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/gc/tests/dead_owner_side_tables.rs` around lines 667
- 746, Remove the redundant if x_bit != 0 guard around the accessor-word
assertion in test_descriptor_meta_summary_gates_probes, since
test_descriptor_key_bit("x") always returns a nonzero bit. Keep the assertion
itself and its existing message unchanged.
crates/perry-runtime/src/object/descriptor_state.rs (1)

542-550: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider adding direct test coverage for the refined write-intercept gate.

The per-key refinement to plain_data_write_may_intercept sits on the dynamic-write hot path (webpack's defineProperty(exports, "__esModule", …) case called out in the comment) but isn't directly exercised by the new tests in dead_owner_side_tables.rs (those cover get_property_attrs/get_accessor_descriptor, not this function). A small test installing one descriptor key and asserting the write-intercept verdict for both that key and an unrelated key would lock in this behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/object/descriptor_state.rs` around lines 542 - 550,
Add focused test coverage for the refined write-intercept gate used by
plain_data_write_may_intercept: install an own descriptor for one key, then
assert the gate reports interception for that key but not for an unrelated
string key. Place the test with the existing descriptor-side-table tests and
preserve coverage of the symbol-key/prototype behavior already handled by the
implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@crates/perry-runtime/src/gc/policy.rs`:
- Around line 646-684: Update the validation/build workflow for the
GcSuppressScope change to rebuild perry-runtime-static and perry-stdlib-static,
including the relevant runtime crates, before running tests. Ensure validation
uses these freshly generated archives rather than stale linked artifacts.
- Around line 663-668: Add a #[must_use] attribute to the GcSuppressScope RAII
guard, targeting the GcSuppressScope type or its new constructor, so callers are
warned when the returned guard is immediately discarded. Preserve the existing
suppression behavior and constructor logic.

In `@crates/perry-runtime/src/gc/tests/dead_owner_side_tables.rs`:
- Around line 667-746: Remove the redundant if x_bit != 0 guard around the
accessor-word assertion in test_descriptor_meta_summary_gates_probes, since
test_descriptor_key_bit("x") always returns a nonzero bit. Keep the assertion
itself and its existing message unchanged.

In `@crates/perry-runtime/src/object/descriptor_state.rs`:
- Around line 542-550: Add focused test coverage for the refined write-intercept
gate used by plain_data_write_may_intercept: install an own descriptor for one
key, then assert the gate reports interception for that key but not for an
unrelated string key. Place the test with the existing descriptor-side-table
tests and preserve coverage of the symbol-key/prototype behavior already handled
by the implementation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8c1cdf9b-4a66-46a2-adee-e3c186ee6dfd

📥 Commits

Reviewing files that changed from the base of the PR and between 66961a8 and f1b2f28.

📒 Files selected for processing (8)
  • changelog.d/6800-object-meta-descriptor-summaries-phase-c2.md
  • crates/perry-runtime/src/gc/policy.rs
  • crates/perry-runtime/src/gc/tests/dead_owner_side_tables.rs
  • crates/perry-runtime/src/object/descriptor_state.rs
  • crates/perry-runtime/src/object/field_get_set/enumeration.rs
  • crates/perry-runtime/src/object/mod.rs
  • crates/perry-runtime/src/object/prototype_chain.rs
  • docs/shape-tree-plan.md

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Gap suite vs node --experimental-strip-types on this branch's tip (C2+C3a stacked): 98.2% parity — 386 pass / 7 mismatch / 0 compile-fail / 0 crashed / 0 skipped; gap gate OK (all 7 mismatches are the pre-existing triaged set on main). Report: test-parity/reports/parity_report_20260722_193115.json.

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