Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions crates/perry-codegen/src/expr/typed_feedback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,28 @@ fn emit_typed_feedback_bytes_global(
format!("@{}", global)
}

/// Whether to emit the per-site `js_typed_feedback_register_site` call at all.
///
/// Typed feedback (#854) is an opt-in profiling feature, disabled at runtime
/// unless `PERRY_TYPED_FEEDBACK` / `PERRY_TYPED_FEEDBACK_TRACE` is set — in
/// which case `js_typed_feedback_register_site` early-returns and does nothing.
/// But the *call itself* (14 pointer/length arguments) was still emitted on
/// every property get/set, which on hot OOP code (e.g. a method doing
/// `this.x = this.x + 1` in a tight loop) costs two no-op cross-crate calls per
/// field access — the dominant cost of the `method_calls` benchmark (491× Node).
///
/// Gate emission on the same env that enables feedback at runtime: a normal
/// build (env unset) 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 is unchanged — guards stay correct either way.
fn typed_feedback_emission_enabled() -> bool {
// Read fresh (not cached) so tests that toggle the env per-case observe the
// change. At compile time this is a cheap getenv per property-access site.
std::env::var_os("PERRY_TYPED_FEEDBACK").is_some()
|| std::env::var_os("PERRY_TYPED_FEEDBACK_TRACE").is_some()
}

pub(crate) fn emit_typed_feedback_register_site(
ctx: &mut FnCtx<'_>,
kind: TypedFeedbackKind,
Expand All @@ -210,6 +232,11 @@ pub(crate) fn emit_typed_feedback_register_site(
let local_site_id = ctx.ic_site_counter;
ctx.ic_site_counter += 1;
let site_id = ctx.typed_feedback_site_id(local_site_id);
// Default build: skip the no-op registration call (and its byte globals)
// but keep the site-id stable for the guard call.
if !typed_feedback_emission_enabled() {
return site_id.to_string();
}
let module = if ctx.strings.module_prefix().is_empty() {
"main".to_string()
} else {
Expand Down
37 changes: 37 additions & 0 deletions crates/perry-codegen/tests/typed_feedback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,37 @@ use perry_codegen::{compile_module, AppMetadata, CompileOptions};
use perry_hir::{BinaryOp, Class, ClassField, Expr, Function, Module, ModuleInitKind, Param, Stmt};
use perry_types::{FunctionType, Type};

/// Serializes env-mutating tests so a concurrent test never observes a
/// half-applied variable. Mirrors the guard in `typed_shape_descriptors.rs`.
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());

/// Sets an env var for the duration of a test and restores the previous value
/// (or unsets it) on drop, so the mutation never leaks to other tests.
struct EnvVarGuard {
key: &'static str,
prev: Option<std::ffi::OsString>,
}

impl EnvVarGuard {
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),
}
}
}

fn empty_opts() -> CompileOptions {
CompileOptions {
target: None,
Expand Down Expand Up @@ -184,6 +215,12 @@ fn typed_feedback_trace_dump_runs_before_entry_return() {

#[test]
fn typed_feedback_instruments_property_and_method_boundaries() {
// Typed-feedback site *registration* is opt-in (emitted only when
// PERRY_TYPED_FEEDBACK / _TRACE is set); this test exercises the enabled
// path. Serialize on ENV_LOCK and restore the var on drop so concurrent or
// later tests in this binary never observe the changed environment.
let _lock = ENV_LOCK.lock().unwrap();
let _env = EnvVarGuard::set("PERRY_TYPED_FEEDBACK", Some("1"));
let ir = ir_for(module(
"typed_feedback_property.ts",
vec![param(1, "obj", Type::Any)],
Expand Down
Loading