Skip to content

perf(runtime): #6759 C5a — per-key vetting for the class-field inline-guard disable - #6802

Merged
proggeramlug merged 2 commits into
mainfrom
perf/6798-c5a-inline-guard-per-key
Jul 22, 2026
Merged

perf(runtime): #6759 C5a — per-key vetting for the class-field inline-guard disable#6802
proggeramlug merged 2 commits into
mainfrom
perf/6798-c5a-inline-guard-per-key

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

C5a of #6759 / #6798 (the "typed_feedback exactness" family, first rung). Stacked on #6801 (Phase C3a).

Problem

disable_class_field_inline_guard_for_target flips the process-wide PERRY_CLASS_FIELD_INLINE_GUARD_DISABLED kill switch on ANY descriptor install targeting a class prototype or Object.prototype. Babel-compiled classes install every method via defineProperty(C.prototype, name, …) — so the first babel class definition permanently pushes every this.field access in the process from the inlined shape-guard fast path onto the full js_typed_feedback_class_field_{get,set}_guard call. The code itself marked per-key granularity as follow-up work; this is that follow-up. Directly targets the #6759 babel-class-init acceptance micro.

What changed

  • The inline fast path only ever compiles accesses to declared instance fields, so an install whose key names no declared field of any registered class cannot affect anything the inline path handles. disable_class_field_inline_guard_for_target now takes the installed key and vets it against DECLARED_FIELD_NAME_HASHES — FNV hashes of every declared instance-field name, harvested by remember_class_keys_array at class registration (the runtime already walks each class's keys array there).
  • Ordering is covered in both directions: installs that precede a class's registration are recorded in PROTO_DESCRIPTOR_KEY_HASHES, and note_declared_instance_field_name retro-checks new field names against it — a class registering late still triggers the disable for previously-installed matching keys.
  • Conservative by construction: hash collisions and lock-poisoning paths all fall toward disable, never toward an unsound skip. The typed-feedback-tracing disable path (case b in the perf(method dispatch): method_calls ~290× Node — remaining cost is per-field-access shape-guard calls (plan + standby) #5093 docs) is untouched.

Validation

Full cargo test -p perry-runtime --lib -- --test-threads=1 green (1447 passed), including two new tests: method-key installs keep the guard enabled while declared-field-key installs disable it, and the late-registration retro-check.

Summary by CodeRabbit

  • Performance

    • Improved class field access performance by making the inline-guard disable behavior key-specific, so unrelated prototype descriptor updates no longer disable the optimized path.
    • Optimization is now gated by whether the updated descriptor matches a declared instance field key.
    • Class registration includes a retro-check to apply the optimization change for relevant previously-installed descriptors.
  • Tests

    • Added tests covering non-matching vs matching keys and behavior when descriptors are installed before class registration.

@coderabbitai

coderabbitai Bot commented Jul 22, 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: f1116bc5-aaa0-46c9-b996-13b5fc1b1c26

📥 Commits

Reviewing files that changed from the base of the PR and between 3340446 and 0043552.

📒 Files selected for processing (3)
  • changelog.d/6802-inline-guard-per-key-c5a.md
  • crates/perry-runtime/src/object/alloc.rs
  • crates/perry-runtime/src/object/descriptor_state.rs

📝 Walkthrough

Walkthrough

The runtime now tracks declared instance-field names and prototype descriptor keys by hash. The inline class-field guard is disabled only for matching keys, including when class registration occurs after a descriptor install. Tests cover immediate and retroactive disabling.

Changes

Per-key inline guard behavior

Layer / File(s) Summary
Per-key guard state
crates/perry-runtime/src/object/descriptor_state.rs
Descriptor-state tracking records declared and installed key hashes and disables the process-wide guard only for matching keys.
Field registration and descriptor wiring
crates/perry-runtime/src/object/alloc.rs, crates/perry-runtime/src/object/descriptor_state.rs
Class registration records declared instance fields, while property and accessor descriptor paths pass their keys to the refined guard logic.
Behavior validation and documentation
crates/perry-runtime/src/object/descriptor_state.rs, changelog.d/6802-inline-guard-per-key-c5a.md
Tests cover matching, non-matching, and retroactive key detection; the changelog documents the per-key behavior.

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

Sequence Diagram(s)

sequenceDiagram
  participant ClassRegistration
  participant RuntimeAllocator
  participant DescriptorState
  participant DescriptorMutation
  ClassRegistration->>RuntimeAllocator: register keys_array and field_count
  RuntimeAllocator->>DescriptorState: record declared field names
  DescriptorMutation->>DescriptorState: install descriptor with key
  DescriptorState->>DescriptorState: compare declared and installed key hashes
  DescriptorState-->>DescriptorMutation: update inline guard state
Loading

Possibly related PRs

  • PerryTS/perry#6795: Both changes modify descriptor-state machinery and shared descriptor-install call paths.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Title check ✅ Passed The title clearly identifies the runtime perf change and the per-key inline-guard vetting behavior.
Description check ✅ Passed The description covers the problem, changes, related issues, and validation, matching the template's intent despite missing some optional sections.
✨ 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 perf/6798-c5a-inline-guard-per-key

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

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@proggeramlug
proggeramlug force-pushed the perf/6798-c3a-stable-shape-identity branch from f2c5148 to 182695f Compare July 22, 2026 19:44
Base automatically changed from perf/6798-c3a-stable-shape-identity to main July 22, 2026 19:44
Ralph Küpper added 2 commits July 22, 2026 21:44
…-guard disable

Prototype-level descriptor installs disable the codegen-inlined class-field
fast path process-wide. The inline path only compiles DECLARED instance
fields, so an install whose key names no declared field of any registered
class (babel's defineProperty(C.prototype, method) storm) can never affect
it. Vet the key against FNV hashes of all declared field names (harvested
in remember_class_keys_array); ordering is covered both ways via a
retro-check when a class registers after the install.
@proggeramlug
proggeramlug force-pushed the perf/6798-c5a-inline-guard-per-key branch from 3340446 to 0043552 Compare July 22, 2026 19:44
@proggeramlug
proggeramlug merged commit 99adc73 into main Jul 22, 2026
27 of 29 checks passed
@proggeramlug
proggeramlug deleted the perf/6798-c5a-inline-guard-per-key branch July 22, 2026 19:45

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-runtime/src/object/descriptor_state.rs`:
- Around line 147-151: Update test_reset_class_field_inline_guard to also clear
DECLARED_FIELD_NAME_HASHES and PROTO_DESCRIPTOR_KEY_HASHES, preserving the
helper’s deterministic reset behavior for all class-field inline guard state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4eb723ff-4101-463a-a8fe-a34636870c9d

📥 Commits

Reviewing files that changed from the base of the PR and between f2c5148 and 3340446.

📒 Files selected for processing (3)
  • changelog.d/6802-inline-guard-per-key-c5a.md
  • crates/perry-runtime/src/object/alloc.rs
  • crates/perry-runtime/src/object/descriptor_state.rs

Comment on lines +147 to +151
#[cfg(test)]
pub(crate) fn test_reset_class_field_inline_guard() {
PERRY_CLASS_FIELD_INLINE_GUARD_DISABLED.store(0, Ordering::Relaxed);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Test reset helper doesn't clear the new per-key hash sets.

test_reset_class_field_inline_guard() only resets PERRY_CLASS_FIELD_INLINE_GUARD_DISABLED, leaving DECLARED_FIELD_NAME_HASHES (Line 205) and PROTO_DESCRIPTOR_KEY_HASHES (Line 212) accumulated forever across test invocations in the same process. Both are documented as "never pruned." Since the c5a_tests (Lines 1096-1174) rely on this helper for "deterministic unit tests," any future test that reuses a key/field name already inserted by an earlier test (e.g. "c5a_field_x", "c5a_render_method") will get a stale match and silently disable the guard regardless of that test's own setup — a source of order-dependent flakiness that the two current tests happen to avoid only because they use disjoint key names.

🔧 Proposed fix
 #[cfg(test)]
 pub(crate) fn test_reset_class_field_inline_guard() {
     PERRY_CLASS_FIELD_INLINE_GUARD_DISABLED.store(0, Ordering::Relaxed);
+    if let Ok(mut guard) = DECLARED_FIELD_NAME_HASHES.write() {
+        guard.take();
+    }
+    if let Ok(mut guard) = PROTO_DESCRIPTOR_KEY_HASHES.write() {
+        guard.take();
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/object/descriptor_state.rs` around lines 147 - 151,
Update test_reset_class_field_inline_guard to also clear
DECLARED_FIELD_NAME_HASHES and PROTO_DESCRIPTOR_KEY_HASHES, preserving the
helper’s deterministic reset behavior for all class-field inline guard state.

@proggeramlug

Copy link
Copy Markdown
Contributor Author

Valid — fixed in #6805 (the cfg(test) reset now clears both vetting sets; production monotonicity unchanged).

proggeramlug added a commit that referenced this pull request Jul 23, 2026
…t reset (#6805)

* test(runtime): clear the C5a per-key vetting sets in the inline-guard test reset (CodeRabbit on #6802)

The sets are production-monotonic by design; without clearing them the
cfg(test) reset left earlier tests' declared-field / installed-key state
visible to later tests reusing a key name, making the disable decision
order-dependent.

* docs(changelog): changeset for test-reset follow-up

---------

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
proggeramlug pushed a commit that referenced this pull request Jul 30, 2026
… gate it (#6837)

* chore(warnings): sweep machine-fixable warnings via cargo fix --all-targets

Run `cargo fix --workspace --all-targets` now that the workspace test
targets compile again, so test-only imports are seen instead of pruned.

Clears 87 warnings: 74 unused_imports, 11 function_casts_as_integer (new
in rustc 1.95 — casting a function item straight to an integer), plus one
each of unused_mut and unused_variables.

Two things the tool could not do on its own:
- optimized_libs/tests.rs reached build_missing_prebuilt_ext_lib through
  `super::*`, so cargo fix pruned the re-export it needed. The test now
  imports the function from no_auto directly.
- 49 of the pruned imports were duplicate preambles introduced when
  commands/compile.rs was split into submodules after #6639.

* chore(warnings): drop 150 redundant `unsafe` blocks

rustc reports these as `unused_unsafe`: the block wraps code that needs no
unsafe context, so the marker hides the sites that do. rustc gives no
machine-applicable fix, so this was scripted off the diagnostic byte spans.

Where the block only held expressions or statements, the block goes with the
keyword. Where it held a top-level `let`, `use`, or item, only the keyword
goes and the braces stay — splicing those into the enclosing scope would
widen the binding and could silently re-resolve a later name. 106 blocks
removed, 44 kept as plain scoping blocks.

* chore(warnings): delete two self-declared extern "C" HTTP symbols

handle_dispatch.rs declares `js_node_http_res_write` and
`js_node_http_res_end` in an `extern "C"` block, but perry-ext-http-server
defines both itself (response.rs:1136 and response.rs:1393). Nothing calls
the declarations, so rustc reports them as dead functions.

Both signatures matched their definitions, so the deletion is a no-op at
link time. The other ~62 declarations in that block are used and stay for
now; they carry the same hazard — a local declaration of a symbol you also
define is never checked against the definition, which is how #6646 shipped
an ABI mismatch on `len: i64` vs `u32`. Tracked separately.

* chore(warnings): clear 101 dead_code warnings

Deletes refactor leftovers — helpers, thunks, fields and constants nothing
calls any more. Keeps, with a justified `#[allow(dead_code)]`, the GC
verifiers, the `#[cfg(test)]`-only helpers and the FFI keepalives that exist
to be reachable from generated code rather than from Rust.

No `#[no_mangle]` function is deleted, so no symbol the compiler emits calls
to can go missing at link time. The 11 deleted `extern "C"` items are
Rust-internal closure thunks addressed by function pointer, which rustc
tracks precisely.

Rebased from the triage done on 2026-07-18, so three sites needed fixing
against current main:
- `path::resolve_win32_str` is no longer dead — url/node_compat.rs calls it.
  Restored.
- `class_field_inline_guard_enabled` and `test_reset_class_field_inline_guard`
  arrived after that triage (#6802). Kept.
- The CGContext* declarations in widgets/chart.rs were already removed by
  #6646. Kept removed.

* chore(warnings): clear naming, visibility, unreachable and must-use warnings

Four triaged families, rebased onto current main:

- non_snake_case: the generated `thunk_<module>_<jsName>` wrappers keep the
  JS spelling on purpose, so the allow sits on the three thunk macros and the
  node_submodule thunks rather than on each site. Real Rust names renamed.
- private_interfaces / private_bounds: widen the leaked types to `pub(crate)`
  so the signature matches what callers can already reach.
- unreachable_patterns: 32 duplicate match arms. Each was checked against the
  arm that shadows it — all are literal repeats, none had a different body
  that the first arm was swallowing.
- unreachable_code: two `return js_throw(..)` where `js_throw` returns `!`.
- sqlite `Connection::set_limit` returned a discarded `Result`: propagated
  with `?` in connection.rs, and `let _` in dispatch.rs where the limit id is
  already validated.
- unused_doc_comments: 8 `///` comments on statements, which rustdoc drops.
- Four dead bindings whose right-hand side is pure, so the binding goes
  instead of getting an underscore.

Conflicts against main resolved in favour of main: perry-hir switched to
`crate::types::Type`, and object/mod.rs moved the transition cache into
`RuntimeState`.

* chore(warnings): clear the last 65 warnings — host scope is now clean

- objc2 deprecations: 17 `msg_send!` invocations were missing the comma
  between arguments, plus one `Retained::cast` swapped for the
  `cast_unchecked` the sibling widgets already use.
- Vacuous glob re-exports: `pub use child::*` where every item in the child
  is `pub(super)` re-exports nothing, which is what rustc was reporting.
  Narrowed each to the visibility it actually has — two to `pub(crate)`, six
  to a plain `use` — instead of silencing the lint.
- `native_proof_*` integration tests share one HIR builder toolkit and each
  file drives a subset, so the allow sits on the file with that reason.
- issue_4914_cluster_port_sharing.rs: its only test is gated to non-macOS
  unix; the helpers and imports now carry the same gate.
- `duplex_allow_half_open_defaults_true_and_honors_false_option` had no
  `#[test]`, so it had never run. Added.
- `test_seed_class_parent_closure_root` existed twice, both writing the same
  `CLASS_PARENT_CLOSURES` static. Deleted the unreachable copy.
- `WIDE_KEY_INDEX_CAPACITY` is a leftover of the 4-entry LRU that #6759 C1
  replaced with shape records.
- crash_log.rs took a shared reference to a `static mut` inside a signal
  handler; now `&raw const`.
- Two dead stores removed (`idx`, `adjusted_args_storage`). The third,
  `done` in publish, is kept with a comment: rustc is right that the store
  is never read, and that means the reconnect guard it feeds can never fire
  — a protocol question, not a lint one.
- Four test names de-camel-cased, one duplicated `#[test]` removed.

`cargo check --workspace --all-targets` now reports zero warnings for the
host-compatible scope. The cross-host UI crates (ios/tvos/watchos/visionos/
android/windows/gtk4) cannot be checked from macOS and are untouched.

* chore(warnings): clean the reduced-feature scope too

`perry` depends on perry-runtime with `default-features = false`, so
`cargo clippy -p perry --bins` — the product leg in CI — compiles a runtime
where regex-engine, diagnostics and temporal are off. Eleven items are live
under the workspace feature set and dead under that one, and the workspace
check never saw them.

Each is now gated at the item rather than silenced:
- `array_named_props_install_fresh` and `regex::utf16::utf16_index_to_byte`
  are called only from regex-engine modules; both, and the array re-export,
  carry that feature (the cross-gate shape regex/utf16.rs already documents).
- Four `fs::dir_glob_watch::glob` imports serve regex-engine-gated code and
  now match the `PathBuf` import next to them.
- The two `AllocatorMaintenance*` enums and `TemporalLocaleCtx` are built
  from `diagnostics` and `temporal` code respectively, so the allow applies
  only when that feature is off.
- publish's `done` allow moved to the function: a statement-level attribute
  does not affect `unused_assignments`.

Four scopes now report zero warnings: `--workspace --all-targets`,
`-p perry --bins`, `-p perry-runtime --no-default-features`, and
`-p perry-runtime -p perry-stdlib -p perry-runtime-static -p perry-stdlib-static`.

* ci: gate rustc warnings with -D warnings

Nothing stopped a PR from adding a warning: `lint` runs only
`cargo fmt --check`, and `clippy` exits non-zero only on the deny-level lints
in `[workspace.lints]`. The 96 unused imports this branch removed include 49
that reappeared within four days of #6639, from a file split that copied whole
import preambles into each new submodule — the gate is what stops that.

Two legs, because they compile different code. `perry` depends on
perry-runtime with `default-features = false`, so the product leg sees a
runtime with regex-engine, diagnostics and temporal off. The workspace leg
passes `--all-targets` so test and bench targets count.

Separate from the clippy job on purpose: clippy's warn-level lints stay
informational, rustc's do not. perry-ui-macos sits in the excluded scope
(this runs on ubuntu), so its warnings are not gated here.

* docs(changelog): key the warnings-sweep fragment to #6837

* chore(warnings): fix float_literal_f32_fallback under rustc 1.97.1

CI runs `dtolnay/rust-toolchain@stable`, which is 1.97.1 on the runners; the
local default stable here is 1.97.1's predecessor, 1.95.0, which does not have
this lint yet. `length(1.0)` in the TUI layout pass inferred `f32` by fallback
rather than by the `From<f64>` bound, which rustc is phasing out
(rust-lang/rust#154024). Spelling the literal `1.0_f32` says what was already
meant.

It was the only occurrence. All four scopes re-checked under 1.97.1 report
zero warnings.

* chore(warnings): silence the four warnings that only appear on Linux

The macOS host cannot see these; the first CI run on this branch found them.

`commands::sandbox_profile` is the #506 MVP, macOS-only by design — its only
caller is inside `#[cfg(target_os = "macos")]`, so off macOS all three of its
functions are unreachable. The module declaration now carries the same gate as
its caller instead of compiling into a build that can never call it. Verified
by flipping both cfgs to a target this host is not, and checking: no errors, no
warnings.

The Linux linker branch in `platform_cmd.rs` binds `let mut c`, but only the
`#[cfg(not(target_os = "linux"))]` cross-compile block mutates it, so building
on Linux the `mut` is dead. Scoped allow on the binding, which does apply to
`unused_mut` — verified by flipping the target and watching the warning
disappear.

* fix(runtime): restore `use std::path::Path` on non-unix targets

The import block read:

    #[cfg(unix)]
    use std::os::unix::fs::{MetadataExt, PermissionsExt};
    #[cfg(unix)]
    use std::os::unix::io::AsRawFd;
    use std::path::Path;

`cargo fix` deleted the unused `AsRawFd` line and left its `#[cfg(unix)]`
behind, which then applied to the `Path` import below it. Every unix host still
compiled; Windows failed with four `cannot find type Path` errors.

Swept the whole diff for the same shape — an attribute that outlived the item
it was written for — and this was the only one. The other new cfg/import
adjacencies are deliberate.

* fix: address warnings sweep review

* fix(warnings): clean regressions after main sync

* fix(workspace): restore container compose default build

* fix(gc): poll after allocating loop controls

* fix(warnings): remove redundant iterator unsafe block

* fix(warnings): remove redundant iterator unsafe block

---------

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