Skip to content

chore: land #10860 — inherited-read cache never primed (v0.5.1628) - #10865

Merged
proggeramlug merged 4 commits into
mainfrom
train10860-landing
Sep 21, 2026
Merged

proggeramlug merged 4 commits into
mainfrom
train10860-landing

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Lands #10860 — a regression fix for #10834, which is live in main today and makes inherited reads slower than before it.

Carried as a train rather than merging the PR head directly, because the PR as submitted would have reddened main:

  • RUSTFLAGS="-D warnings" cargo check failed. The prime call sits inside get_field_ic_miss_impl's existing unsafe block (ic_miss.rs:874), so its own nested unsafe is unused_unsafe. CI's warnings job runs -D warnings. Fixed here.
  • cargo fmt --all -- --check was red in two files. Fixed.
  • No changelog.d/ fragment. Added.

The fix

Two defects made the cache pure overhead — the probe ran on every read, never served, and the chain walk proceeded unchanged.

A. The prime was gated on R::NotOwn. A receiver with no keys array reports ObjectNoKeys and returns from an earlier arm, hundreds of lines before the prime — and Object.create(p) with nothing of its own is exactly that shape, the most common inherited-read receiver there is. ObjectNoKeys means no own properties at all, so the prime's precondition holds there more strongly than under NotOwn.

B. entry_index hashed only (shape, key). js_object_create mints a fresh synthetic class id per call, so N receivers from Object.create(p) have N class ids and one shape; all N shared a direct-mapped slot and evicted each other, while the entry compare on recv_class_id made every read miss, re-walk and re-prime — hits=0 primes=6295655 over ten million reads.

fixture main today this node
8 Object.create receivers via an array 1600 494 19.1
single keyless receiver, 1-level chain 1481 427 9.0
single keyless receiver, 3-level chain 2442 446 8.7

Audit

entry_index has exactly one definition and three callers, all updated consistently — a missed site would desync prime from lookup and silently reinstate the miss. The ObjectNoKeys arm primes and then falls through via get_field_by_name_past_inherited_cache, so the top-of-function lookup is not repeated, and !inherited_declined still respects hook A's decline.

Both regression tests pass in release (a_receiver_with_no_own_keys_is_cached, several_object_create_receivers_do_not_evict_each_other); the PR reports them proven non-vacuous — reverting the source fixes makes each panic with a specific message.

Gates on the assembled tree: check_file_size, raw_handle_debt, gc_runtime_root_holders, addr_class_inventory, shape_descriptor_census, string_payload_access_inventory, gc_rekeyed_key_tables all rc=0; cargo fmt --all -- --check clean; cargo check --all-targets under -D warnings clean.

Expedited at the owner's request — landed on a targeted audit plus the PR's own regression tests rather than a full train sweep, because the regression is live and costs 2442 instructions per read on a keyless three-level chain.

perry-bot and others added 4 commits September 21, 2026 08:54
… it exists to serve

#10834 is live in main (train 247, v0.5.1626) and makes inherited reads SLOWER
than before it. Same binary, one environment variable apart:

| fixture | `PERRY_INHERITED_IC=0` | cache on | |
|---|---|---|---|
| 8 `Object.create` receivers via an array | 1525.00 | 1600.00 | **+75** |
| one `Object.create` receiver, no own keys | 1375.00 | 1481.00 | **+106** |

The cache was pure overhead: the probe ran on every read, never served, and the
chain walk proceeded unchanged.

The counters say why, and they rule out the obvious guess. All four inherited
counters read ZERO on the single-receiver fixture — including `declines` — so
the prime was never CALLED, not merely refused. Two independent defects:

## A. The prime site is gated on the wrong miss reason

`get_field_ic_miss_impl` primes only under `matches!(miss_reason, R::NotOwn)`.
A receiver with no keys array reports `ObjectNoKeys` and returns from an
earlier arm, several hundred lines before the prime. `Object.create(p)` with
nothing of its own is exactly that shape, and it is the most common
inherited-read receiver there is.

`ObjectNoKeys` means the object has NO own properties at all, so "the key is
not an own property" — the precondition the prime needs — holds there MORE
strongly than it does under `NotOwn`. The fix primes in that arm and then
continues past the cache rather than through it, so the lookup at the top of
the function is not repeated.

## B. The slot index ignored the class id

`js_object_create` mints a FRESH synthetic class id on every call, so N
receivers built by `Object.create(p)` have N different class ids and ONE
identical shape. `entry_index` hashed only (shape, key), so all N landed in the
same direct-mapped slot and evicted one another. An entry compares
`recv_class_id`, so every read missed, re-walked and re-primed:

    inherited: hits=0 primes=6295655      (ten million reads, eight receivers)

A full chain walk PLUS an entry write per read. The fix hashes the class id
into the index, so the eight receivers occupy eight slots.

## Result

| fixture | main-247 | this | node |
|---|---|---|---|
| 8 receivers via array | 1600.00 | **494.00** | 19.1 |
| single keyless receiver | 1481.00 | **427.00** | 9.0 |

`perf stat -x, -e instructions:u`, min of 3, fitted 500 k -> 5 M, two trees
whose binaries `cmp` different, output identical to node on both.

Counters after: `hits=50108984 primes=1` for the keyless receiver and
`hits=57043013 primes=8` for the eight — exactly one prime per receiver, then
hits. So this is not a repair to parity; it is the win #10834 was supposed to
deliver, on the shapes it was missing entirely.

## Why the original measurement missed both

#10834's fixtures give the receiver an own property and mutate it in the loop
(`O.x = k`, added to keep the loop honest against node's optimiser). That one
incidental detail puts the read on the `NotOwn` path, so defect A never fires,
and uses a single receiver, so defect B never fires. On that shape the cache
genuinely is a 43% win — 2246 off, 1264 on — which is why the reported numbers
were real and generalised badly.

## Tests

Two runtime tests, driven through `js_object_get_field_ic` — the real entry the
compiled code calls — because both defects live in the miss handler's routing
and a test that calls the cache's own functions cannot see either.

Against this commit with the two source fixes reverted and the tests kept:

    a_receiver_with_no_own_keys_is_cached
      panicked: a keyless receiver never reached the prime, so the cache can
      never serve this shape and its probe is pure overhead on every read

    several_object_create_receivers_do_not_evict_each_other
      panicked: primed 64 times for 8 receivers: every read is re-priming, so
      the site pays a full chain walk AND an entry write per read

`cargo test -p perry-runtime -- --test-threads=1`: 4162 passed, 0 failed.
#10860's prime call sits inside get_field_ic_miss_impl's existing unsafe block
(ic_miss.rs:874), so its own unsafe is unused_unsafe. CI's warnings job runs
-D warnings and would have rejected the tree.
@proggeramlug
proggeramlug merged commit 47ade47 into main Sep 21, 2026
22 of 23 checks passed
@proggeramlug
proggeramlug deleted the train10860-landing branch September 21, 2026 07:16
@coderabbitai

coderabbitai Bot commented Sep 21, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 610ffde2-66c2-4942-8e8a-6a87a7445e10

📥 Commits

Reviewing files that changed from the base of the PR and between 6768ed6 and 281633f.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • CLAUDE.md
  • Cargo.toml
  • changelog.d/10860-inherited-cache-never-primed.md
  • crates/perry-runtime/src/object/field_get_set/ic_miss.rs
  • crates/perry-runtime/src/object/inherited_read_cache.rs
  • crates/perry-runtime/src/object/inherited_read_cache_tests.rs
 ___________________________________________
< I am a verified code reviewer on Twitter. >
 -------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

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.

2 participants