Skip to content

perf(codegen): skip canonical-shape own-method scans - #8505

Merged
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:fix/8406-shapes-hot-path
Aug 21, 2026
Merged

perf(codegen): skip canonical-shape own-method scans#8505
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:fix/8406-shapes-hot-path

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Speed up polymorphic instance-method dispatch by using an exact compiler-published (class_id, ShapeId) pair to prove that canonical instances have no own-method override. This removes the hot js_object_get_own_field_or_undef keys scan from eligible calls and reuses the class ID for the bounded dispatch tower.

Declared or computed fields, dynamic inheritance, post-construction shape changes, descriptor/prototype invalidation, non-instance receivers, and wide towers keep the existing guarded fallback.

Changes

  • add a guarded canonical-shape fast path to dynamic instance dispatch
  • reuse the validated class ID in the dispatch tower
  • preserve fallback behavior for own method fields and mutated instances
  • add end-to-end regression coverage for declared and post-construction overrides

Related issue

Fixes #8406

Performance

Quiet A/B measurement for this change:

  • shapes retired instructions: -9.14%
  • shapes CPU cycles: -9.93%
  • peak RSS: unchanged
  • all 19 corpus rows: byte-exact

A fresh alternating 21-run sample on the currently contended host still clears the issue's wall-time criterion:

  • Perry median wall: 103.528 ms
  • Node median wall: 145.085 ms
  • ratio: 0.714x Node

The same local /usr/bin/time -l sample reported median retired instructions / peak RSS of 1,149,921,569 / 33,914,880 bytes for Perry and 1,090,746,015 / 94,044,160 bytes for Node. The machine was at load average ~70, so the alternating high-resolution wall sample is reported separately from the quiet A/B compute deltas above.

Test plan

  • cargo build --release -p perry -p perry-runtime-static -p perry-stdlib-static
  • cargo test --release -p perry-runtime --lib — 2,606 passed, 4 ignored
  • cargo test --release -p perry --bin perry — 1,008 passed
  • cargo test --release -p perry --test issue_8406_dynamic_dispatch_shape_probe
  • cargo test --release -p perry-codegen --lib tower_route_is_guarded_by_the_class_shape_id
  • all 19 benchmark corpus programs compile and match their Node-oracle stdout byte-for-byte
  • BASE_SHA=upstream/main SKIP_COMPILE_GATES=1 bash scripts/run_lint_gates.sh — all 50 lint gates passed
  • cargo fmt --all -- --check

Checklist

  • No workspace version bump; no CLAUDE.md or CHANGELOG.md changes
  • Added focused user-visible regression coverage
  • Commit follows the repository convention

Summary by CodeRabbit

  • Performance

    • Improved polymorphic method dispatch for compatible object shapes.
    • Added guarded optimizations that preserve fallback behavior when shape information is unavailable.
  • Bug Fixes

    • Corrected dispatch handling for inherited methods, declared function fields, and later property overrides.
  • Tests

    • Added regression coverage confirming methods resolve correctly across inherited, declared, and mutated properties.
    • Expanded validation of shape-based dispatch safeguards.

@coderabbitai

coderabbitai Bot commented Aug 21, 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: 55cc24a8-3dd2-4cce-8aab-706363274bbc

📥 Commits

Reviewing files that changed from the base of the PR and between 9aee500 and 5dbe8c1.

📒 Files selected for processing (4)
  • changelog.d/8406-shape-header-reads.md
  • crates/perry-codegen/src/collectors/proven_this_routing_tests.rs
  • crates/perry-codegen/src/lower_call/property_get/dynamic_dispatch.rs
  • crates/perry/tests/issue_8406_dynamic_dispatch_shape_probe.rs

Included review availability: Your plan provides up to 8 included reviews per hour; 2 remain after this review.


📝 Walkthrough

Walkthrough

The change adds canonical class/ShapeId validation to dynamic method dispatch. Eligible receivers skip the own-property probe, while unsupported or mismatched cases retain existing fallback behavior. Tests cover inherited methods, declared function fields, property mutation, and ShapeId guard dataflow.

Changes

Dynamic dispatch shape validation

Layer / File(s) Summary
Canonical shape eligibility and implementor tracking
crates/perry-codegen/src/lower_call/property_get/dynamic_dispatch.rs
The compiler checks class inheritance and declared fields before marking an implementor eligible for canonical-shape validation. It records each implementor’s concrete class.
Runtime shape probe and dispatch reuse
crates/perry-codegen/src/lower_call/property_get/dynamic_dispatch.rs
The dispatch path compares runtime class and ShapeId values against published pairs. Matching receivers bypass the own-property probe. Other receivers use the existing probe and fallback paths.
Regression coverage and changelog
crates/perry-codegen/src/collectors/proven_this_routing_tests.rs, crates/perry/tests/issue_8406_dynamic_dispatch_shape_probe.rs, changelog.d/8406-shape-header-reads.md
The tests inspect ShapeId guard dataflow and validate inherited, declared-field, and mutated-property dispatch. The changelog records the change and benchmark results.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: ⚪ Minimal · up to 5dbe8

This change adds a guarded dispatch fast path while preserving fallback behavior for mutable or non-canonical instances; the supplied checks and regression coverage support merge readiness with no actionable merge-blocking risk remaining.

Sequence Diagram(s)

sequenceDiagram
  participant Receiver
  participant DynamicDispatch
  participant RuntimeMetadata
  Receiver->>DynamicDispatch: provide receiver
  DynamicDispatch->>RuntimeMetadata: read class ID and ShapeId
  RuntimeMetadata-->>DynamicDispatch: return metadata
  DynamicDispatch->>DynamicDispatch: compare canonical class/ShapeId pair
  DynamicDispatch->>DynamicDispatch: skip or run own-property probe
Loading

Suggested reviewers: jdalton

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the performance change to skip canonical-shape own-method scans.
Description check ✅ Passed The description includes the required summary, changes, issue reference, test plan, performance results, and checklist.
Linked Issues check ✅ Passed The implementation addresses issue #8406 with a guarded dispatch fast path, preserved fallbacks, regression tests, benchmarks, and required validation.
Out of Scope Changes check ✅ Passed All changes support issue #8406, including codegen, regression coverage, benchmarks, and the related changelog entry.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 3 files. (1 skipped: 1 unsupported.)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Audited and merging. The performance claim is fine; the review here is the soundness of the proof, since a wrong "no own override" verdict silently resolves a method to the prototype when an own property should have shadowed it — that is a wrong-answer bug, not a slow one.

The compile-time half is conservative in the right ways. canonical_shape_excludes_own_property bails on a declared field matching the property (which may intentionally shadow, #620), on any computed key_expr, on extends_expr/native_extends, on an unknown class, and on a cycle in the chain. Good.

The runtime half rests on js_method_direct_shape_class, which is pre-existing (#8412's exact-pair guard, already sabotage-tested), not new surface introduced here. Its gates — obj_type == GC_TYPE_OBJECT, not forwarded, !descriptors_in_use(), !class_prototype_fast_guards_invalidated(), class_id != 0 — mean the whole path disarms as soon as anything descriptor-shaped is in play.

I verified the new path is actually exercised before trusting any probe. My first two adversarial fixtures passed against Node while emitting zero new arms — the counts were identical to main, so passing proved nothing about this change. The trigger turned out to be the this.kids[i].area() shape: an untyped element read as receiver across a multi-level hierarchy, which is exactly what shapes.ts does and what a statically-typed xs: A[] parameter does not reproduce.

On shapes.ts: 6 js_method_direct_shape_class call sites versus 4 on main — the subject is live on the benchmark this claims to speed up.

With a fixture built to that shape (Group holding kids: any[], four-level chain plus a fieldless subclass), emitting 2 call sites versus main's 1, every shadowing path agrees with Node:

path expected result
post-construction inst.area = f 100 match
computed key inst["ar"+"ea"] = f 200 match
Object.assign(inst, {area: f}) 300 match
Object.defineProperty(inst, "area", …) 400 match
assign then delete → prototype again 2 match
megamorphic site seeing all ten shapes match

Output 12|1002|1014, byte-identical to Node and to main.

Also ran the two gates this PR skipped. The test plan reports "all 50 lint gates" because it used SKIP_COMPILE_GATES=1; the two compile-tier gates were therefore not covered. Ran them on the branch: cargo check --workspace --all-targets and cargo clippy --workspace both clean. Worth noting for future PRs — 50 of 52 is a green that omits the two gates most likely to catch a codegen change.

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.

perf: shapes spends 100% of self time in try_read_tracked_gc_header + shape_descriptor_by_id (1.06x Node)

1 participant