Skip to content

perf(runtime): reuse stable one-key for-in snapshots - #8710

Merged
proggeramlug merged 1 commit into
mainfrom
merge/b18
Aug 24, 2026
Merged

perf(runtime): reuse stable one-key for-in snapshots#8710
proggeramlug merged 1 commit into
mainfrom
merge/b18

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Lands #8709. (#8707 is held — see below.)

Routes compiled ForInKeys through js_for_in_keys_stable_value, reusing the immutable shape-owned key snapshot when the receiver, key, descriptors and %Object.prototype% generation prove the result exact, with the complete generic enumerator kept for every proof miss.

The reuse guard

PrototypeSignature carries a raw prototype_addr, which is exactly the shape that went stale in #8393 — so that's what I checked. It is not that shape. The signature is recomputed live on every call from object_prototype_addr(), with try_read_gc_header validation and an explicit GC_FLAG_FORWARDED rejection:

let header = try_read_gc_header(prototype_addr)?;
if header.obj_type != GC_TYPE_OBJECT || header.gc_flags & GC_FLAG_FORWARDED != 0 {
    return None;
}

The cached verdict is consulted only when that freshly-read signature compares equal in all three fields, and the cached address is never dereferenced — so a moved prototype yields a mismatch and a cold recompute, not a false hit. Descriptor, key and prototype mutations mint a new ShapeId; class-level changes move vtable_generation.

PROTOTYPE_VERDICT is a new rule-T holder and is pinned on the inventory frontier alongside the other identity-ratcheted thread-locals.

Validation

  • All 30 lint-job checkers pass
  • perry-runtime --lib (RUST_TEST_THREADS=1): 2655 passed, 0 failed
  • perry-codegen --lib: 1214 passed, 0 failed
  • perry-transform --lib: 87 passed, 0 failed
  • perf(runtime): reuse stable one-key for-in snapshots #8709's own issue_8694_stable_for_in acceptance test: 1 passed

That last one initially failed, and the cause is worth recording: cargo test -p perry --test <suite> links a stale libperry_{runtime,stdlib}.a unless the -static wrapper crates are rebuilt first. After cargo build -p perry -p perry-runtime-static -p perry-stdlib-static with PERRY_RUNTIME_DIR pinned, it passes. The failure was the harness, not the change.

Why #8707 is not in this batch

#8707 fixes a real and serious bug — await inside a catch compiling to a blocking busy-wait that deadlocks the single runtime thread. But its own new test async_generator_linearizes_every_await_position fails once rebased onto current main:

raw Expr::Await survived async-generator linearization (would block-wait at runtime):
  ["await-in-finally: 2 raw await(s) survived"]

It passes on #8707's own head, so this is an interaction with main, not a defect in the PR as written. Both the test and its await-in-finally case are new in #8707 — neither exists on main or at #8707's merge base — so the PR is catching a genuine remaining gap rather than regressing anything. Resolving it means knowing whether finally should route through catch_entry_state the same way catch now does, which is the author's call, not mine to guess at.

It also needs a changelog.d/ fragment and a trivial source_order: 0 field on the ClassComputedMember test fixture at async_to_generator.rs:2196 (the field landed on main with #8645).

Summary by CodeRabbit

  • Performance

    • Improved compiled for...in loops for ordinary objects with stable keys, reducing repeated enumeration overhead.
    • Preserved correct behavior for mutations, prototypes, proxies, exceptions, and unsupported cases through automatic fallback.
  • Testing

    • Added coverage for key ordering, snapshot behavior, garbage collection, cache invalidation, and fallback handling.
  • Documentation

    • Added a changelog entry describing the for...in performance improvement.

Lands #8709.

Routes compiled `ForInKeys` through `js_for_in_keys_stable_value` and
reuses the immutable shape-owned key snapshot when the receiver, key,
descriptors and %Object.prototype% generation prove the result exact,
keeping the complete generic enumerator for every proof miss.

The reuse guard was checked rather than assumed. `PrototypeSignature`
carries a raw `prototype_addr`, which is the shape that went stale in
#8393 -- but this is not that shape. The signature is recomputed live on
every call from `object_prototype_addr()`, with `try_read_gc_header`
validation and an explicit `GC_FLAG_FORWARDED` rejection, and the cached
verdict is consulted only when the freshly-read signature compares equal
in all three fields. The cached address is never dereferenced, so a moved
prototype produces a mismatch and a cold recompute rather than a false
hit. Descriptor, key and prototype mutations mint a new ShapeId, and
class-level changes move `vtable_generation`.

`PROTOTYPE_VERDICT` is a new rule-T holder, pinned on the inventory
frontier alongside the other identity-ratcheted thread-locals.

No version bump.
@proggeramlug
proggeramlug merged commit 2ecdd1a into main Aug 24, 2026
@proggeramlug
proggeramlug deleted the merge/b18 branch August 24, 2026 07:59
@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c87ea40e-1458-4ef1-9181-6041206a59e8

📥 Commits

Reviewing files that changed from the base of the PR and between c76b439 and ab1c8b8.

📒 Files selected for processing (9)
  • benchmarks/compiler_output/fixtures/for_in_stable_keys.ts
  • benchmarks/compiler_output/workloads.toml
  • changelog.d/8709-for-in-stable-keys.md
  • crates/perry-codegen/src/expr/logical_collections.rs
  • crates/perry-codegen/src/runtime_decls/strings.rs
  • crates/perry-runtime/src/object/field_get_set.rs
  • crates/perry-runtime/src/object/field_get_set/for_in_stable.rs
  • crates/perry/tests/issue_8694_stable_for_in.rs
  • scripts/gc_runtime_root_holders.json

📝 Walkthrough

Walkthrough

Compiled for...in loops now use a stable one-key snapshot path for eligible objects. The runtime validates object and prototype metadata, caches prototype verdicts, and falls back to generic enumeration when validation fails. Benchmarks, changelog records, GC roots, and integration tests cover the change.

Changes

Stable for-in enumeration

Layer / File(s) Summary
Runtime stability proof and fallback
crates/perry-runtime/src/object/field_get_set/..., scripts/gc_runtime_root_holders.json
The runtime adds stable one-key validation, prototype-verdict caching, shared key snapshots, diagnostics, and generic fallback handling.
Compiler runtime integration
crates/perry-codegen/src/expr/logical_collections.rs, crates/perry-codegen/src/runtime_decls/strings.rs
Compiled Expr::ForInKeys calls the new js_for_in_keys_stable_value runtime entry.
End-to-end behavior validation
crates/perry/tests/issue_8694_stable_for_in.rs
Integration coverage checks stable enumeration, mutations, prototypes, proxies, exceptions, output, diagnostics, and forced GC evacuation.
Benchmark and release records
benchmarks/compiler_output/fixtures/for_in_stable_keys.ts, benchmarks/compiler_output/workloads.toml, changelog.d/8709-for-in-stable-keys.md
The benchmark measures stable-key enumeration and the changelog records its eligibility and fallback behavior.

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

Suggested reviewers: thehypnoo

Sequence Diagram(s)

sequenceDiagram
  participant CompiledForIn
  participant js_for_in_keys_stable_value
  participant PrototypeVerdictCache
  participant js_for_in_keys_value
  CompiledForIn->>js_for_in_keys_stable_value: submit object value
  js_for_in_keys_stable_value->>PrototypeVerdictCache: check prototype signature
  PrototypeVerdictCache-->>js_for_in_keys_stable_value: return cached or validated verdict
  js_for_in_keys_stable_value-->>CompiledForIn: return stable key snapshot
  js_for_in_keys_stable_value->>js_for_in_keys_value: handle validation miss
  js_for_in_keys_value-->>CompiledForIn: return generic enumeration keys
Loading
✨ 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 merge/b18

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.

1 participant