Skip to content

perf(runtime): try the transition fast path before rooting in the dyn-IC miss (48 → 44 ms) - #9329

Merged
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:perf/9287-transition-early
Aug 31, 2026
Merged

perf(runtime): try the transition fast path before rooting in the dyn-IC miss (48 → 44 ms)#9329
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:perf/9287-transition-early

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

One increment on #9287's computed-key residual. Runtime-only, 3 files.

The change

js_put_value_set_dyn_ic_miss opened a RuntimeHandleScope and rooted target/key/value before trying the learned shape-transition shortcut — which then opened its own scope and rooted the same operands again. On the fresh-object-construction lane (the dominant taker: every new-key write changes the receiver's shape, so no per-site IC can ever hit) that was two scopes and six roots per property write, one set protecting nothing.

The shortcut now runs before the scope, via a value-returning twin of the transition fast path:

  • Success returns the stored value re-read through the callee's own root, taken after every internal allocation point — the caller needs no roots.
  • Miss-after-allocation (fresh key interned, then no learned edge) hands back re-rooted operands via an out-param. Without that, this reorder would be the runtime: audit dynamic_arith operand rooting — raw NaN-boxed operands held across GC-capable to_numeric coercions (pre-existing, file-wide) #6655 bug class — the caller rooting stale bits after a GC the callee triggered. Caught at design time, guarded structurally, and stressed directly: the intern-then-miss fixture (40 generations of never-seen keys, string values, spill growth) is node-identical under default GC and under PERRY_GC_HEAP_LIMIT=8 PERRY_GC_FORCE_EVACUATE=1.

Numbers

Quiet host, 20k fresh objects × 20 computed-key writes, three runs each, spread ≤ 1 ms:

shape before after node
concat keys (o["field_" + j]) 48 44 24
pre-built keys 43 39 12
bench_object_property ~35 ~33 12

Stated plainly

~10%, not parity. This removes one of the two scopes; the remaining ~3× lives in the transition install machinery itself — the fast path's own scope (skippable when the key is already interned and no growth occurs), shape-version install, keys handling, spill store. That's the next increment, tracked on #9287, and it's design-shaped rather than shave-shaped.

Also for the record, three levers eliminated by measurement before this one was built: key concat+intern (~5 ms of 48 — the profile's top frame overstated it), the prototype intercept probe (one load when no descriptors exist), and an early-transition shortcut in js_put_value_set (already present in-tree at the exact call site — found before duplicating it).

Refs #9287.

Summary by CodeRabbit

  • Performance Improvements
    • Improved performance when creating objects or setting properties with computed keys.
    • Reduced overhead in common dynamic property writes while preserving correct behavior when allocations occur.

…-IC miss (48 -> 44 ms)

js_put_value_set_dyn_ic_miss opened a RuntimeHandleScope and rooted
target/key/value before trying the learned shape-transition shortcut —
which then opened its own scope and rooted the same three operands again.
On the fresh-object-construction lane (the dominant taker of this entry:
every write to a new key changes the receiver's shape, so no per-site IC
can ever hit) that was two scopes and six roots per property write, one
set of which protected nothing.

The shortcut now runs BEFORE the scope, through a value-returning form of
the transition fast path:

  * On success it returns the stored value re-read through its OWN root,
    taken after every internal allocation point (key interning, spill
    growth) — so the caller needs no roots of its own.
  * On a miss it reports, via an out-param, whether it allocated before
    missing (interning a fresh key and then finding no learned edge). In
    that case the caller's raw f64 operands may point at moved objects,
    and the callee hands back re-rooted copies for the fallback ladder.
    Without that, this reorder would have been the PerryTS#6655 bug class —
    rooting stale bits after a GC the callee triggered.

The i32 entries keep their signatures; the value form is a pub(crate)
twin, and the existing rooted call site is replaced rather than
duplicated.

Measured on the quiet host, 20k fresh objects x 20 computed-key writes,
three runs each, spread <= 1 ms:

    concat keys      48 -> 44 ms    (node 24)
    pre-built keys   43 -> 39 ms    (node 12)
    bench_object_property  ~35 -> ~33 ms  (node 12)

Correctness: the intern-then-miss stress (40 generations of objects with
never-seen keys, string values, spill growth) is node-identical under the
default GC and under PERRY_GC_HEAP_LIMIT=8 PERRY_GC_FORCE_EVACUATE=1.

This is one increment, not parity: the remaining ~3x against node on this
shape is the transition install machinery itself (second handle scope in
the fast path, shape-version install, keys handling, spill store), tracked
on PerryTS#9287.

Refs PerryTS#9287.
@coderabbitai

coderabbitai Bot commented Aug 31, 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: Team

Run ID: ace9b0df-539a-43a4-a9ed-68713b0736fd

📥 Commits

Reviewing files that changed from the base of the PR and between 9943dd9 and cbbf779.

📒 Files selected for processing (4)
  • changelog.d/9287-transition-pre-scope.md
  • crates/perry-runtime/src/object/field_set_by_name.rs
  • crates/perry-runtime/src/object/field_set_by_name/fast_paths.rs
  • crates/perry-runtime/src/proxy/put_value.rs

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


📝 Walkthrough

Walkthrough

The dynamic-write miss handler now attempts the learned shape transition before opening its outer handle scope. The new value-returning fast path roots operands internally, refreshes them after key interning, and returns the stored value on success.

Changes

Dynamic property write transition

Layer / File(s) Summary
Value-returning transition fast path
crates/perry-runtime/src/object/field_set_by_name/fast_paths.rs
The transition implementation now returns Option<f64>. It returns the stored value on success, returns None on failure, and refreshes operand copies after key interning.
Pre-scope dynamic-write integration
crates/perry-runtime/src/object/field_set_by_name.rs, crates/perry-runtime/src/proxy/put_value.rs, changelog.d/9287-transition-pre-scope.md
The new helper is re-exported and used before the outer handle scope opens. The caller adopts refreshed operands after a miss. The changelog records the performance change.

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

Merge Risk: ⚪ Minimal · up to cbbf7

This localized runtime optimization reduces redundant rooting work while preserving existing write checks and fallback behavior; no actionable merge-blocking risk remains beyond normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant js_put_value_set_dyn_ic_miss
  participant object_set_field_by_name_transition_only_fast_value
  participant RuntimeHandleScope
  js_put_value_set_dyn_ic_miss->>object_set_field_by_name_transition_only_fast_value: pass unrooted operands
  object_set_field_by_name_transition_only_fast_value->>object_set_field_by_name_transition_only_fast_value: root operands and attempt transition
  object_set_field_by_name_transition_only_fast_value-->>js_put_value_set_dyn_ic_miss: return stored value or refreshed operands
  js_put_value_set_dyn_ic_miss->>RuntimeHandleScope: open scope after a miss
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the runtime performance change: running the transition fast path before rooting in the dynamic-IC miss path. The benchmark improvement supports the summary.
Description check ✅ Passed The description is substantive and covers the change, rationale, performance results, safety validation, and reference to issue #9287. It does not reproduce the template headings or checklist confirma…
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 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 is substantive and covers the change, rationale, performance results, safety validation, and reference to issue #9287. It does not reproduce the template headings or checklist confirmations, but the required information is mostly present and the omitted screenshots section is not applicable.

Full details: Docstring Coverage

Explanation

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
proggeramlug merged commit 21f1c4c into PerryTS:main Aug 31, 2026
17 of 20 checks passed
proggeramlug added a commit that referenced this pull request Aug 31, 2026
* fix(runtime): scope the refresh key read and classify the test hit counter

with_const_ptr keeps the key pointer's validity window explicit for the
non-allocating tuple build, restoring fast_paths.rs to its ceiling of 8.
TEST_TRANSITION_FAST_HITS is a cfg(test) Cell<u64> counter, not a GC root.

* fix(runtime): gate the now-test-only transition wrapper

#9287 moved the production caller to the value-returning form, leaving the
i32 wrapper used only by the transition tests; -D warnings rejects it.

* chore: changelog fragment for the #9329 follow-up

---------

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
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