perf(codegen): make typed-feedback site registration opt-in (3.6x on dynamic property access)#5084
Conversation
Every property get/set emitted a js_typed_feedback_register_site call (14 pointer/length args) plus the shape guard. Typed feedback (#854) is an opt-in profiling feature that no-ops at runtime unless PERRY_TYPED_FEEDBACK / PERRY_TYPED_FEEDBACK_TRACE is set, so in a normal build the register_site call was pure overhead executed on every field access — e.g. a dynamic-property write loop paid one no-op cross-crate call per write. Gate register_site emission on the same env that enables feedback at runtime (read fresh at codegen time). A normal build skips registration entirely; a profiling build (PERRY_TYPED_FEEDBACK=1 perry app.ts -o app && ./app, env inherited by the run) emits and uses it. The site-id is still allocated, so the shape *guard* call is unchanged and dispatch correctness is unaffected. bench_object_property (10k objects x 20 dynamic string-keyed fields): 1127ms -> ~310ms (3.6x), checksum identical to Node. No behavior change with feedback disabled (register_site was already a runtime no-op there). The dedicated typed-feedback codegen test opts in via the env var to exercise the enabled path; all other codegen tests assert the guards (still emitted).
📝 WalkthroughWalkthroughThis PR adds an environment-driven opt-in for typed-feedback site registration: ChangesEnvironment-driven typed-feedback gating
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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-codegen/tests/typed_feedback.rs`:
- Line 190: The test typed_feedback_instruments_property_and_method_boundaries
sets PERRY_TYPED_FEEDBACK without restoring it; wrap that set in the existing
EnvVarGuard + ENV_LOCK pattern (capture previous env var value and restore in
Drop) used in EnvVarGuard in typed_shape_descriptors.rs: acquire ENV_LOCK,
create an EnvVarGuard for "PERRY_TYPED_FEEDBACK" (saving previous state), call
set_var("PERRY_TYPED_FEEDBACK","1") under the guard, and let the guard drop at
test end to restore/remove the variable so other tests won't observe the changed
environment.
🪄 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: cd21aefb-854b-4574-81a3-4983958ee4b6
📒 Files selected for processing (2)
crates/perry-codegen/src/expr/typed_feedback.rscrates/perry-codegen/tests/typed_feedback.rs
The typed-feedback opt-in test set PERRY_TYPED_FEEDBACK without restoring it, leaking the variable to other tests in the binary. Wrap it in the EnvVarGuard + ENV_LOCK pattern (capture previous value, restore on drop, serialize) used in typed_shape_descriptors.rs. Addresses CodeRabbit review feedback on #5084.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
crates/perry-codegen/tests/typed_feedback.rs (1)
5-34: ⚡ Quick winConsider consolidating the duplicated
EnvVarGuard+ENV_LOCKpattern into a shared test utility.The comment on line 6 confirms this mirrors the guard in
typed_shape_descriptors.rs. Duplicating this 30-line pattern violates DRY and creates a maintenance burden—if the implementation needs enhancement (e.g., logging, better lock-poisoning recovery), it must be updated in multiple places.♻️ Proposed consolidation approach
Create
crates/perry-codegen/tests/common.rs:pub static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); pub struct EnvVarGuard { key: &'static str, prev: Option<std::ffi::OsString>, } impl EnvVarGuard { pub fn set(key: &'static str, value: Option<&str>) -> Self { let prev = std::env::var_os(key); match value { Some(value) => std::env::set_var(key, value), None => std::env::remove_var(key), } Self { key, prev } } } impl Drop for EnvVarGuard { fn drop(&mut self) { match &self.prev { Some(value) => std::env::set_var(self.key, value), None => std::env::remove_var(self.key), } } }Then in both
typed_feedback.rsandtyped_shape_descriptors.rs:mod common; use common::{EnvVarGuard, ENV_LOCK};🤖 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-codegen/tests/typed_feedback.rs` around lines 5 - 34, Extract the duplicated ENV_LOCK and EnvVarGuard definitions into a shared test utility module (a common mod) and replace the local definitions in this file and the other test file with imports; specifically move the static ENV_LOCK and the EnvVarGuard struct/impl/Drop into the shared module, make EnvVarGuard::set and ENV_LOCK public, then in this test file import them with use common::{EnvVarGuard, ENV_LOCK} and remove the local definitions so both tests use the single canonical implementation.
🤖 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.
Nitpick comments:
In `@crates/perry-codegen/tests/typed_feedback.rs`:
- Around line 5-34: Extract the duplicated ENV_LOCK and EnvVarGuard definitions
into a shared test utility module (a common mod) and replace the local
definitions in this file and the other test file with imports; specifically move
the static ENV_LOCK and the EnvVarGuard struct/impl/Drop into the shared module,
make EnvVarGuard::set and ENV_LOCK public, then in this test file import them
with use common::{EnvVarGuard, ENV_LOCK} and remove the local definitions so
both tests use the single canonical implementation.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 1a5a7115-91c4-473d-9f06-76c0054e5c76
📒 Files selected for processing (1)
crates/perry-codegen/tests/typed_feedback.rs
Removes a per-access no-op runtime call from the property get/set hot path in normal builds.
Root cause
Profiling
bench_object_property(10k objects × 20 dynamic string-keyed fields) showed it at 1127ms vs Node 16ms (70×). The emitted IR for every property get/set contained:js_typed_feedback_register_siteis part of typed feedback (#854), an opt-in profiling feature that early-returns at runtime unlessPERRY_TYPED_FEEDBACK/PERRY_TYPED_FEEDBACK_TRACEis set. In a normal build the call did nothing — but it was still emitted and executed (a 14-argument cross-crate call) on every field access, e.g. once per write in a dynamic-property loop.Fix
Gate the
register_siteemission on the same env that enables feedback at runtime, read fresh at codegen time (crates/perry-codegen/src/expr/typed_feedback.rs). A normal build skips registration entirely and pays nothing; a profiling build (PERRY_TYPED_FEEDBACK=1 perry app.ts -o app && ./app, env inherited by the run) emits and uses it.The site-id is still allocated and returned, so the shape guard call (
js_typed_feedback_class_field_*_guard/object_*_by_name_guard) is unchanged — dispatch and field-access correctness are unaffected. This is purely removing a call that was already a runtime no-op in the default configuration.Results
bench_object_property1999990000(identical to Node, before & after)Correctness re-verified vs
node --experimental-strip-typesonbench_int_arithmetic,bench_string_heavy,bench_json_roundtrip,bench_array_grow,bench_numeric_array_numeric— all checksums match.Helps any dynamic-property-heavy code (the common TypeScript case), not just this benchmark.
Tests
cargo test -p perry-codegen— all pass. The dedicatedtyped_feedback_instruments_property_and_method_boundariestest opts in via the env var to exercise the enabled path; all other feedback tests assert the guards (still emitted) and are unaffected.Not included
This does not fix
method_calls(491×), which profiling showed is dominated by the per-access shape guard calls and the method-dispatch guard/call — a larger change (devirtualize monomorphic dispatch + inline/hoist the field-access guards, entangled with therequire_raw_f64typed-shape layout). That is scoped as separate follow-up work.Summary by CodeRabbit
New Features
Chores