Skip to content

perf(runtime): skip dead feedback observation + field-only shape reads (computed-key read now at node parity) - #8983

Merged
proggeramlug merged 2 commits into
PerryTS:mainfrom
proggeramlug:perf-shape-field-read
Aug 28, 2026
Merged

perf(runtime): skip dead feedback observation + field-only shape reads (computed-key read now at node parity)#8983
proggeramlug merged 2 commits into
PerryTS:mainfrom
proggeramlug:perf-shape-field-read

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Two dead-work removals on the property read path, both taken from the top of an isolated read profile.

1. Typed-feedback observation when recording is off. Recording is off by default, and both guard_observe and record_fallback_call early-return in that mode — but the property wrappers built the entire Observation first, hashing the key and resolving the receiver's shape, purely to hand it to functions that discard it. js_typed_feedback_object_get_field_by_name_f64 was 10% of an isolated property-read loop, nearly all of it that.

The array index wrappers have carried #5094's gate for exactly this reason, and #8951 gave it to the fast store path. The property get/set wrappers never got it.

Behaviour is unchanged in both modes, and provably so: with recording off guard_observe returns contract_valid unmodified and the fallback recorder is a no-op, so the wrapper already reduced to precisely the underlying call it now makes directly.

2. Field-only shape reads. shape_descriptor_by_id returns ShapeDescriptor by value, so object_live_slot_count — the slot bound consulted on essentially every property read and write — lifted the whole ~48-byte record and kept four bytes of it. It now reads the field through the table's record using the same way-cache probe and the same epoch validation. shape_descriptor_by_id was 10.1% of the same loop.

Measurement

Interleaved A/B, min-of-21, built from the exact parent commit and this commit in one run. Node on the same host in brackets:

loop base this PR
pure property read 21 ms (node 4) 17 ms −19%
computed-key read 27 ms (node 23) 23 ms −15% — now at parity with node
combined overwrite 46 ms (node 31) 41 ms −11%
write only 21 ms (node 23) 21 ms unchanged; already ahead of node

Means track the mins (22 → 18, 28 → 24, 46 → 42).

Correctness

  • perry-runtime: 2779 passed / 0 failed, including the 55 typed-feedback tests — that gate changes when observations get recorded, so it is the one to watch.
  • Private-member exercise: byte-identical to base.
  • Computed-key differential vs node: byte-identical.

Where the remaining gap is

The pure property read (o[k], key already built) is now 17 ms against node's 4 ms and is the largest single remaining gap in this area — worth noting that perry's key construction is already faster than node's, so this is the lookup itself.

https://claude.ai/code/session_01Ay8VyLkKbm8Hkc1xmvTEsP

Summary by CodeRabbit

  • Performance Improvements

    • Improved property-read performance when tracing is disabled.
    • Reduced overhead when determining available object slots.
    • Preserved existing write performance and behavior.
  • Reliability

    • Maintained existing fallback behavior for unavailable shape information.
    • Verified changes with passing tests and byte-identical outputs.

Typed-feedback recording is off by default, and guard_observe and
record_fallback_call both early-return in that mode — but the property
wrappers had already built the whole Observation to hand them, hashing the key
and resolving the receiver's shape first. On an isolated property-read loop
js_typed_feedback_object_get_field_by_name_f64 was 10% of self time, nearly all
of it that dead work.

Apply PerryTS#5094's gate, which the array index wrappers already carry and PerryTS#8951 gave
the fast store path: when recording is off, take the underlying op directly.
Behaviour is unchanged in both modes — with recording off guard_observe returns
contract_valid and the fallback recorder is a no-op, so the wrapper already
reduced to exactly this call.

Also: object_live_slot_count reads live_inline_slot_count through the shape
table's record instead of lifting the whole ~48-byte descriptor to discard all
but four bytes. That bound is consulted on essentially every property
operation, and shape_descriptor_by_id was 10.1% of the same loop.

Claude-Session: https://claude.ai/code/session_01Ay8VyLkKbm8Hkc1xmvTEsP
@coderabbitai

coderabbitai Bot commented Aug 28, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

Next included review available in 12 minutes.

View limit details

Limit details: You’ve used all 8 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b0f9c4a1-de83-425c-a3b2-efb26c40172e

📥 Commits

Reviewing files that changed from the base of the PR and between 0fa1435 and 8749022.

📒 Files selected for processing (1)
  • changelog.d/8983-feedback-gate-and-shape-field.md

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: 88b3a84a-0910-49e8-a580-5cd384b43b5b

📥 Commits

Reviewing files that changed from the base of the PR and between f4e8206 and 0fa1435.

📒 Files selected for processing (4)
  • changelog.d/8981-feedback-gate-and-shape-field.md
  • crates/perry-runtime/src/object/live_slots.rs
  • crates/perry-runtime/src/object/shapes.rs
  • crates/perry-runtime/src/typed_feedback.rs

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


📝 Walkthrough

Walkthrough

The runtime now skips typed-feedback observation setup when tracing is disabled. Shape access reads live_inline_slot_count directly from cached shape records. The changelog records benchmark, test, and output results.

Changes

Property access optimizations

Layer / File(s) Summary
Gate typed-feedback observation
crates/perry-runtime/src/typed_feedback.rs
Property reads, property writes, and polymorphic object-index writes call the underlying operations directly when tracing is disabled.
Read live slot count directly
crates/perry-runtime/src/object/shapes.rs, crates/perry-runtime/src/object/live_slots.rs, changelog.d/8981-feedback-gate-and-shape-field.md
Shape lookup reads the requested descriptor field directly from cached records or uses the existing fallback. object_live_slot_count uses the new live-slot helper, and the changelog records validation results.

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

Merge Risk: ⚪ Minimal · up to 0fa14

This change removes unnecessary property-read work while preserving existing behavior and validation, with the supplied test and differential results passing. No actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant TypedFeedback
  participant TracingState
  participant ObjectOperation
  TypedFeedback->>TracingState: check tracing enabled
  TracingState-->>TypedFeedback: disabled
  TypedFeedback->>ObjectOperation: perform property read or write
  ObjectOperation-->>TypedFeedback: return operation result
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description gives detailed optimization context, measurements, and test results, but it does not follow the required template structure. It omits the Summary, Changes, Related issue, Test plan, an… Rewrite the description using the repository template. Add the required headings, list the concrete changes, specify a related issue or use "n/a", include the test commands and results, and complete the checklist items. Keep the existing me…
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies both runtime performance changes and states the main computed-key read result. It is somewhat long but remains specific and relevant.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 3 files. (1 skipped: 1 u…
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.
Full details: Description check

Explanation

The description gives detailed optimization context, measurements, and test results, but it does not follow the required template structure. It omits the Summary, Changes, Related issue, Test plan, and Checklist headings and does not state a related issue or checklist status.

Resolution

Rewrite the description using the repository template. Add the required headings, list the concrete changes, specify a related issue or use "n/a", include the test commands and results, and complete the checklist items. Keep the existing measurement and correctness details under the appropriate sections.

Full details: Docstring Coverage

Explanation

Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 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

Merged (batched with #8981 and #8982). Both removals are dead work rather than new behaviour — building an Observation (hashing the key, resolving the receiver's shape) only to hand it to functions that early-return when recording is off is pure waste, and the gate is checked before the construction rather than inside it.

One fix pushed: the fragment was named 8981-feedback-gate-and-shape-field.md — that is #8981's number, a different PR in this same batch. Renamed to 8983-. Left as-is it would have collided with #8981's own fragment at release time.

That makes three fragment-naming problems in this batch of three: one named for an issue, one carrying another PR's number, one missing entirely. I filed #8978 about this earlier — the gate validates the filename shape (^changelog\.d/[0-9]+-[^/]+\.md$), so any number passes and only a missing fragment is caught.

Validation — runtime 2780/0, hir 355/0, codegen 1341/0, stdlib 124/0 (RUST_TEST_THREADS=1); scripts/run_lint_gates.sh 57 of 58 with the compile tier green — the exception is the pre-existing Actions-expression artifact (#8929). The node-parity claim is not re-measured here.

@proggeramlug
proggeramlug merged commit 626f6ad into PerryTS:main Aug 28, 2026
19 checks passed
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