diff --git a/CLAUDE.md b/CLAUDE.md index 976f2e31ad..287cf93ddf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1406 +**Current Version:** 0.5.1407 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 9f5f04aa42..f36c8ca37b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5547,7 +5547,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perry" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "anyhow", "base64", @@ -5607,14 +5607,14 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "serde", ] [[package]] name = "perry-audio-miniaudio" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "cc", "libc", @@ -5622,7 +5622,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "anyhow", "inkwell", @@ -5639,7 +5639,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "anyhow", "perry-hir", @@ -5647,7 +5647,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "anyhow", "perry-hir", @@ -5655,7 +5655,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "anyhow", "perry-dispatch", @@ -5664,7 +5664,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "anyhow", "perry-hir", @@ -5672,7 +5672,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "anyhow", "base64", @@ -5684,7 +5684,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "anyhow", "perry-hir", @@ -5692,7 +5692,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "anyhow", "async-trait", @@ -5721,14 +5721,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "serde", "serde_json", @@ -5736,7 +5736,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1406" +version = "0.5.1407" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5747,7 +5747,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "anyhow", "clap", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "block2", "objc2", @@ -5772,7 +5772,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "argon2", "perry-ffi", @@ -5780,7 +5780,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "perry-ffi", "reqwest", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "bcrypt", "perry-ffi", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "perry-ffi", "rusqlite", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "perry-ffi", "scraper", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "perry-ffi", "perry-runtime", @@ -5821,7 +5821,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "chrono", "cron", @@ -5831,7 +5831,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "chrono", "perry-ffi", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "perry-ffi", "rust_decimal", @@ -5847,7 +5847,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "perry-ffi", "serde_json", @@ -5855,7 +5855,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "perry-ffi", "rand 0.10.1", @@ -5863,7 +5863,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "perry-ffi", "perry-runtime", @@ -5871,14 +5871,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "bytes", "http-body-util", @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "bytes", "lazy_static", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "bytes", "h2", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "lazy_static", "perry-ffi", @@ -5943,7 +5943,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "base64", "jsonwebtoken", @@ -5954,7 +5954,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "lru", "perry-ffi", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "chrono", "perry-ffi", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "bson", "futures-util", @@ -5983,7 +5983,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "chrono", "perry-ffi", @@ -5993,7 +5993,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "nanoid", "perry-ffi", @@ -6002,7 +6002,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "bytes", "perry-ffi", @@ -6015,7 +6015,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "const-oid 0.9.6", "der 0.7.10", @@ -6034,7 +6034,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "lettre", "perry-ffi", @@ -6044,7 +6044,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "perry-ffi", "printpdf", @@ -6052,7 +6052,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "perry-ffi", "sqlx", @@ -6061,7 +6061,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "governor", "perry-ffi", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "fast_image_resize", "image", @@ -6079,14 +6079,14 @@ dependencies = [ [[package]] name = "perry-ext-slugify" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-streams" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "lazy_static", "perry-ffi", @@ -6095,7 +6095,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "perry-ffi", "perry-runtime", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "perry-ffi", "uuid", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "perry-ffi", "regex", @@ -6122,7 +6122,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "futures-util", "lazy_static", @@ -6135,7 +6135,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "brotli", "flate2", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "dashmap", "once_cell", @@ -6154,7 +6154,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "anyhow", "perry-api-manifest", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "perry-parser" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "anyhow", "perry-diagnostics", @@ -6184,7 +6184,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "anyhow", "base64", @@ -6226,14 +6226,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6328,14 +6328,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "anyhow", "perry-hir", @@ -6344,14 +6344,14 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "perry-ui-model", ] [[package]] name = "perry-ui-android" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "base64", "itoa", @@ -6368,7 +6368,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "rand 0.10.1", "serde", @@ -6378,7 +6378,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "base64", "cairo-rs 0.22.0", @@ -6401,7 +6401,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "base64", "block2", @@ -6417,7 +6417,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "base64", "block2", @@ -6432,7 +6432,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1406" +version = "0.5.1407" [[package]] name = "perry-ui-test" @@ -6443,11 +6443,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1406" +version = "0.5.1407" [[package]] name = "perry-ui-tvos" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "base64", "block2", @@ -6463,7 +6463,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "base64", "block2", @@ -6479,7 +6479,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "block2", "libc", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "base64", "libc", @@ -6509,14 +6509,14 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "perry-ui-windows", ] [[package]] name = "perry-updater" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "anyhow", "base64", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1406" +version = "0.5.1407" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 00cfcadce7..fdb3d64177 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,7 +315,7 @@ codegen-units = 16 codegen-units = 16 [workspace.package] -version = "0.5.1406" +version = "0.5.1407" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" diff --git a/changelog.d/7702-typed-feedback-recording-emission-gate.md b/changelog.d/7702-typed-feedback-recording-emission-gate.md new file mode 100644 index 0000000000..17ab48e07b --- /dev/null +++ b/changelog.d/7702-typed-feedback-recording-emission-gate.md @@ -0,0 +1,67 @@ +### Performance + +- **Typed-feedback recording calls are no longer emitted into default builds** (#7480 step 4). + + Typed feedback is opt-in. Every one of its recording helpers begins: + + ```rust + if site_id == 0 || !typed_feedback_enabled() { return; } + ``` + + and `typed_feedback_enabled()` is false unless `PERRY_TYPED_FEEDBACK` / + `PERRY_TYPED_FEEDBACK_TRACE` is set. Codegen emitted them anyway, on every + execution of every dynamic property boundary. On `churn_read_big.ts` — + 200k × 1000 reads of `keep[j].v + keep[j].w` — the two that sit on the + monomorphic-IC hot path are **22.3% of the whole program**: + + | symbol | share of self time | + |---|--:| + | `main` (generated code) | 66.0% | + | `js_dynamic_string_or_number_add` | 11.7% | + | `typed_feedback::record_guard_pass` | **11.2%** | + | `typed_feedback::observe_property` | **11.1%** | + + (`sample`, 2465 leaf samples, `PERRY_DEBUG_SYMBOLS=1`.) None of that is + recording. It is the cross-crate call, plus the `LazyLock` acquire load + each helper performs in order to decide it has nothing to do. + + This was never a deliberate trade. The per-site `js_typed_feedback_register_site` + call has been gated on exactly this env since #5093's follow-up, for exactly + this reason, and the gate simply stopped short of the recording it registers + for. `emit_typed_feedback_record_call` now routes all five pure-bookkeeping + helpers — `observe_property_{get,set}`, `record_guard_{pass,fail}`, + `record_fallback_call` — through the same switch, at all nine emit sites (the + generic-get diamond, the array-push fallback, the index realloc arm, the two + by-name lookup arms, the method-override fallback, the closure-call fallback). + + **The line between gated and not is asserted, not described.** Helpers that + also perform the operation or pick the dispatch — `js_typed_feedback_*_guard`, + `…_object_set_field_by_name_fast`, `…_object_get_field_by_name_f64`, + `…_native_call_method` — are real calls on real paths and are emitted + unconditionally; a `debug_assert!` on the callee name rejects any attempt to + route one of them through the eliding helper, and the new test asserts a + default build still emits `js_typed_feedback_object_set_field_by_name_fast`. + + No behaviour changes in a profiling build (`PERRY_TYPED_FEEDBACK=1` set for the + `perry` invocation and the run): the existing + `typed_feedback_instruments_property_and_method_boundaries` test asserts every + helper is still emitted there, unchanged. + +### Fixed + +- **A trace requested from a binary that was not built for one now says so.** + + Because registration was already compile-gated, a default-built binary run with + `PERRY_TYPED_FEEDBACK_TRACE=` could only ever produce *unattributed* + sites — counters with no module, function or source name. With recording gated + too it produces nothing at all, which is the same amount of information and + looks far more like success. `js_typed_feedback_maybe_dump_trace` now prints a + one-line note when the registry is empty, instead of writing `"total_sites": 0` + and letting the reader conclude their program has no dynamic boundaries. + + The note names the compile-time env as the *most common* cause rather than + asserting it, because it is not the only one: an empty registry from a + correctly-built binary reproduces identically on the pre-change compiler, so + there is at least one other path that instruments a site and records nothing. + That is pre-existing and out of scope here; the note is worded so it does not + send anyone chasing the wrong cause. diff --git a/crates/perry-codegen/src/expr/array_push.rs b/crates/perry-codegen/src/expr/array_push.rs index 46594d62c9..d7eecab64d 100644 --- a/crates/perry-codegen/src/expr/array_push.rs +++ b/crates/perry-codegen/src/expr/array_push.rs @@ -538,7 +538,8 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr, value_discarded: bool) -> ctx.current_block = fallback_idx; { let blk = ctx.block(); - blk.call_void( + crate::expr::emit_typed_feedback_record_call( + blk, "js_typed_feedback_record_fallback_call", &[(I64, &feedback_site_id)], ); diff --git a/crates/perry-codegen/src/expr/index.rs b/crates/perry-codegen/src/expr/index.rs index d133970069..de8f022a09 100644 --- a/crates/perry-codegen/src/expr/index.rs +++ b/crates/perry-codegen/src/expr/index.rs @@ -616,7 +616,8 @@ pub(crate) fn lower_index_set_fast( ctx.current_block = realloc_idx; { let blk = ctx.block(); - blk.call_void( + crate::expr::emit_typed_feedback_record_call( + blk, "js_typed_feedback_record_fallback_call", &[(I64, feedback_site_id)], ); diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs index 6939892efd..2d0fa93eda 100644 --- a/crates/perry-codegen/src/expr/mod.rs +++ b/crates/perry-codegen/src/expr/mod.rs @@ -111,8 +111,8 @@ pub(crate) use range_facts::{ }; pub(crate) use strings::emit_string_literal_global; pub(crate) use typed_feedback::{ - emit_typed_feedback_register_site, native_region_slug, typed_feedback_emission_enabled, - TypedFeedbackContract, TypedFeedbackKind, + emit_typed_feedback_record_call, emit_typed_feedback_register_site, native_region_slug, + typed_feedback_emission_enabled, TypedFeedbackContract, TypedFeedbackKind, }; pub(crate) use url_helpers::lower_url_string_getter; pub(crate) use v8_interop::{ diff --git a/crates/perry-codegen/src/expr/property_get.rs b/crates/perry-codegen/src/expr/property_get.rs index 5bc3e375ad..90fb2a2947 100644 --- a/crates/perry-codegen/src/expr/property_get.rs +++ b/crates/perry-codegen/src/expr/property_get.rs @@ -1660,7 +1660,11 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { ctx.current_block = lookup_idx; let blk = ctx.block(); - blk.call_void("js_typed_feedback_record_fallback_call", &[(I64, &site_id)]); + crate::expr::emit_typed_feedback_record_call( + blk, + "js_typed_feedback_record_fallback_call", + &[(I64, &site_id)], + ); let val_fallback_js = blk.call( DOUBLE, "js_object_get_field_by_name_f64", diff --git a/crates/perry-codegen/src/expr/property_get/generic_dispatch.rs b/crates/perry-codegen/src/expr/property_get/generic_dispatch.rs index b68a226bfd..3a8f2c2878 100644 --- a/crates/perry-codegen/src/expr/property_get/generic_dispatch.rs +++ b/crates/perry-codegen/src/expr/property_get/generic_dispatch.rs @@ -168,7 +168,8 @@ pub(crate) fn lower_generic_property_get( ctx.block().br(&final_merge_label); ctx.current_block = pic_idx; - ctx.block().call_void( + crate::expr::emit_typed_feedback_record_call( + ctx.block(), "js_typed_feedback_observe_property_get", &[ (I64, &feedback_site_id), @@ -425,7 +426,8 @@ pub(crate) fn lower_generic_property_get( ctx.block() .cond_br(&slot_in_bounds, &bounds_hit_label, &miss_label); ctx.current_block = bounds_hit; - ctx.block().call_void( + crate::expr::emit_typed_feedback_record_call( + ctx.block(), "js_typed_feedback_record_guard_pass", &[(I64, &feedback_site_id)], ); @@ -446,11 +448,13 @@ pub(crate) fn lower_generic_property_get( // PIC miss: slow path with cache population. ctx.current_block = miss_idx; - ctx.block().call_void( + crate::expr::emit_typed_feedback_record_call( + ctx.block(), "js_typed_feedback_record_guard_fail", &[(I64, &feedback_site_id)], ); - ctx.block().call_void( + crate::expr::emit_typed_feedback_record_call( + ctx.block(), "js_typed_feedback_record_fallback_call", &[(I64, &feedback_site_id)], ); diff --git a/crates/perry-codegen/src/expr/property_get/helpers.rs b/crates/perry-codegen/src/expr/property_get/helpers.rs index 36ea0727c8..158a458b30 100644 --- a/crates/perry-codegen/src/expr/property_get/helpers.rs +++ b/crates/perry-codegen/src/expr/property_get/helpers.rs @@ -817,7 +817,11 @@ pub(crate) fn lower_raw_f64_class_field_get_for_number_context( ctx.current_block = lookup_idx; let blk = ctx.block(); - blk.call_void("js_typed_feedback_record_fallback_call", &[(I64, &site_id)]); + crate::expr::emit_typed_feedback_record_call( + blk, + "js_typed_feedback_record_fallback_call", + &[(I64, &site_id)], + ); let val_fallback_js = blk.call( DOUBLE, "js_object_get_field_by_name_f64", diff --git a/crates/perry-codegen/src/expr/typed_feedback.rs b/crates/perry-codegen/src/expr/typed_feedback.rs index bb7a78d5e3..01e72cc09e 100644 --- a/crates/perry-codegen/src/expr/typed_feedback.rs +++ b/crates/perry-codegen/src/expr/typed_feedback.rs @@ -253,6 +253,58 @@ pub(crate) fn typed_feedback_emission_enabled() -> bool { || std::env::var_os("PERRY_TYPED_FEEDBACK_TRACE").is_some() } +/// Emit a pure-BOOKKEEPING typed-feedback call — or nothing at all in a default +/// build. +/// +/// #7480 step 4, and the other half of the argument +/// [`typed_feedback_emission_enabled`] already makes. That gate removed the +/// per-site `js_typed_feedback_register_site` call from default builds and +/// stopped there, leaving the *recording* helpers +/// (`observe_property_{get,set}`, `record_guard_{pass,fail}`, +/// `record_fallback_call`) emitted on every execution of every dynamic +/// property boundary. Each begins: +/// +/// ```text +/// if site_id == 0 || !typed_feedback_enabled() { return; } +/// ``` +/// +/// so in a default build every one of them is a cross-crate call that answers +/// "no" and returns. On `churn_read_big.ts` — 200k × 1000 reads of +/// `keep[j].v + keep[j].w` — those two answers are **22.3% of the whole +/// program** (`observe_property` 11.1%, `record_guard_pass` 11.2%, `sample`, +/// 2465 leaf samples). Not the recording: the call, plus the `LazyLock` +/// acquire load each one performs to decide it has nothing to do. +/// +/// Leaving them in was never a deliberate trade. Registration and recording are +/// the same feature behind the same env var, and a binary compiled without the +/// var could already only produce *unattributed* feedback — sites with no +/// module, function or source name, because nothing registered them. This makes +/// the contract whole: the env is a compile-time switch, exactly as +/// [`typed_feedback_emission_enabled`]'s doc says, and a run that asks for a +/// trace from a binary that was not built for one now says so +/// (`js_typed_feedback_maybe_dump_trace`) instead of writing an empty file. +/// +/// **Only helpers that exclusively record go through here.** Anything that also +/// performs the operation or picks the dispatch — `js_typed_feedback_*_guard`, +/// `…_object_set_field_by_name_fast`, `…_object_get_field_by_name_f64`, +/// `…_native_call_method` — is a real call on a real path and is emitted +/// unconditionally, whatever the env says. +pub(crate) fn emit_typed_feedback_record_call( + blk: &mut crate::block::LlBlock, + func_name: &str, + args: &[(crate::types::LlvmType, &str)], +) { + debug_assert!( + func_name.starts_with("js_typed_feedback_record_") + || func_name.starts_with("js_typed_feedback_observe_"), + "only pure-recording feedback helpers may be elided: {func_name}" + ); + if !typed_feedback_emission_enabled() { + return; + } + blk.call_void(func_name, args); +} + pub(crate) fn emit_typed_feedback_register_site( ctx: &mut FnCtx<'_>, kind: TypedFeedbackKind, diff --git a/crates/perry-codegen/src/lower_call/early_branches.rs b/crates/perry-codegen/src/lower_call/early_branches.rs index 435fb1ac33..15f50fcc55 100644 --- a/crates/perry-codegen/src/lower_call/early_branches.rs +++ b/crates/perry-codegen/src/lower_call/early_branches.rs @@ -923,8 +923,11 @@ pub fn try_lower_closure_typed_local_call( } ctx.current_block = fallback_idx; - ctx.block() - .call_void("js_typed_feedback_record_fallback_call", &[(I64, &site_id)]); + crate::expr::emit_typed_feedback_record_call( + ctx.block(), + "js_typed_feedback_record_fallback_call", + &[(I64, &site_id)], + ); // Guard failed: the receiver is some OTHER closure whose // body codegen never saw — reset `this` here (and only // here) when the static gating skipped the outer reset. diff --git a/crates/perry-codegen/src/lower_call/method_override.rs b/crates/perry-codegen/src/lower_call/method_override.rs index 22f62e5a7b..e14b3a95e6 100644 --- a/crates/perry-codegen/src/lower_call/method_override.rs +++ b/crates/perry-codegen/src/lower_call/method_override.rs @@ -815,8 +815,11 @@ pub(super) fn emit_guarded_direct_method_call( (ptr_reg, n.to_string()) }; if let Some(site_id) = site_id { - ctx.block() - .call_void("js_typed_feedback_record_fallback_call", &[(I64, &site_id)]); + crate::expr::emit_typed_feedback_record_call( + ctx.block(), + "js_typed_feedback_record_fallback_call", + &[(I64, &site_id)], + ); } let method_id = crate::strings::emit_static_dispatch_id(ctx.block(), &dispatch_global); let fallback_value = ctx.block().call( diff --git a/crates/perry-codegen/tests/typed_feedback.rs b/crates/perry-codegen/tests/typed_feedback.rs index db20795250..82bd5e0adc 100644 --- a/crates/perry-codegen/tests/typed_feedback.rs +++ b/crates/perry-codegen/tests/typed_feedback.rs @@ -373,6 +373,88 @@ fn typed_feedback_instruments_property_and_method_boundaries() { assert!(ir.contains("call void @js_typed_feedback_record_guard_pass")); assert!(ir.contains("call void @js_typed_feedback_record_guard_fail")); assert!(ir.contains("call void @js_typed_feedback_record_fallback_call")); + assert!(ir.contains("call void @js_typed_feedback_observe_property_get")); +} + +/// The negative twin of the test above, and the whole of #7480 step 4's second +/// half: a DEFAULT build emits none of the pure-recording helpers. +/// +/// Every one of them early-returns unless the runtime env is set, so in a +/// default build each was a cross-crate call that answered "no" — 22.3% of +/// `churn_read_big.ts` between `observe_property` and `record_guard_pass`. The +/// registration call has been compile-gated on the same env since #5093's +/// follow-up; this extends that gate to the recording it registers for. +/// +/// Asserted as a census over the whole emitted module rather than one site, +/// because the point is that NO path emits them — the generic-get diamond, the +/// array-push fallback, the index realloc arm, the method-override fallback and +/// the closure-call fallback all route through the same helper. +#[test] +fn a_default_build_emits_no_typed_feedback_recording_calls() { + let _lock = env_lock(); + let _trace = EnvVarGuard::set("PERRY_TYPED_FEEDBACK_TRACE", None); + let _env = EnvVarGuard::set("PERRY_TYPED_FEEDBACK", None); + let ir = ir_for(module( + "typed_feedback_default_build.ts", + vec![param(1, "obj", Type::Any)], + Type::Any, + vec![ + Stmt::Expr(Expr::PropertySet { + object: Box::new(Expr::LocalGet(1)), + property: "x".to_string(), + value: Box::new(Expr::Number(1.0)), + }), + Stmt::Expr(Expr::Call { + callee: Box::new(Expr::PropertyGet { + byte_offset: 0, + object: Box::new(Expr::LocalGet(1)), + property: "run".to_string(), + }), + args: vec![Expr::Number(2.0)], + type_args: Vec::new(), + byte_offset: 0, + }), + Stmt::Return(Some(Expr::PropertyGet { + byte_offset: 0, + object: Box::new(Expr::LocalGet(1)), + property: "x".to_string(), + })), + ], + )); + + // The `declare` lines stay (they are emitted for the whole runtime surface + // and LLVM drops the unused ones); what must be gone is every CALL. + for helper in [ + "js_typed_feedback_observe_property_get", + "js_typed_feedback_observe_property_set", + "js_typed_feedback_record_guard_pass", + "js_typed_feedback_record_guard_fail", + "js_typed_feedback_record_fallback_call", + "js_typed_feedback_register_site", + ] { + let call = format!("call void @{helper}"); + assert!( + !ir.contains(&call), + "a default build must not call `{helper}`; it early-returns at \ + runtime, so the call is pure overhead" + ); + } + + // ANTI-VACUITY. Every assertion above is a negative, so they all pass on an + // empty string. The property boundaries themselves must still be here — + // this test proves the RECORDING is gone, not the program. + assert!( + ir.contains("js_object_get_field_by_name_f64") + || ir.contains("js_object_get_field_ic_miss"), + "the property reads themselves must still be lowered; emitted:\n{ir}" + ); + // And the helpers that DECIDE something, rather than merely counting, are + // not gated: this is the line between the two, asserted rather than + // described. + assert!( + ir.contains("js_typed_feedback_object_set_field_by_name_fast"), + "dispatching feedback wrappers must still be emitted in a default build" + ); } #[test] @@ -421,10 +503,12 @@ fn typed_feedback_guards_direct_class_field_specialization() { // by-name SET it replaced is no longer emitted at the set site. assert!(ir.contains("call void @js_class_field_set_fallback")); assert!(!ir.contains("call void @js_object_set_field_by_name")); - // `record_fallback_call` is still present — but from the class-field-GET - // fallback block below, not the SET site (the SET copy is now folded into - // js_class_field_set_fallback). - assert!(ir.contains("call void @js_typed_feedback_record_fallback_call")); + // #7480 step 4: `record_fallback_call` used to be asserted present here + // (from the class-field-GET fallback block, not the SET site — the SET copy + // was folded into js_class_field_set_fallback by #5334). It is now emitted + // only in a typed-feedback build, like the registration call beside it. + // The fallback ARM itself is unchanged and is asserted above/below. + assert!(!ir.contains("call void @js_typed_feedback_record_fallback_call")); assert!(ir.contains("call double @js_object_get_field_by_name_f64")); // #7430 split the GET fallback arm: `class_field_get_number.fallback` now // holds only the nullish-receiver check and branches to `.throw_nullish` / @@ -437,7 +521,9 @@ fn typed_feedback_guards_direct_class_field_specialization() { // the coerced register. let lookup = block_body(&ir, "class_field_get_number.fallback_lookup") .expect("raw numeric class-field consumer should keep the fallback lookup block"); - assert!(lookup.contains("call void @js_typed_feedback_record_fallback_call")); + // (#7480 step 4 removed the `record_fallback_call` that used to anchor this + // block; the by-name load below identifies it just as well, and is what the + // data-flow assertion actually needs.) assert!(lookup.contains("call double @js_object_get_field_by_name_f64")); let coerce_line = lookup .lines() @@ -833,7 +919,10 @@ fn typed_feedback_guards_direct_closure_call_specialization() { assert!(ir.contains("closure_direct.fallback")); assert!(ir.contains("call double @perry_closure_")); assert!(ir.contains("call double @js_closure_call1")); - assert!(ir.contains("call void @js_typed_feedback_record_fallback_call")); + // #7480 step 4: pure-recording feedback helpers are emitted only in a + // typed-feedback build. The fallback arm is asserted by the two lines + // above; this asserts the counter that rode along with it is gone. + assert!(!ir.contains("call void @js_typed_feedback_record_fallback_call")); } #[test] diff --git a/crates/perry-codegen/tests/typed_shape_descriptors.rs b/crates/perry-codegen/tests/typed_shape_descriptors.rs index 598cd51afe..34fa6325f0 100644 --- a/crates/perry-codegen/tests/typed_shape_descriptors.rs +++ b/crates/perry-codegen/tests/typed_shape_descriptors.rs @@ -406,10 +406,22 @@ fn number_typed_local_array_push_keeps_layout_note_and_barrier() { "number-typed array pushes should validate the runtime value before the numeric path" ); assert!( - ir.contains("call void @js_typed_feedback_record_fallback_call") - && ir.contains("call i64 @js_array_push_f64"), + ir.contains("call i64 @js_array_push_f64"), "wrong runtime values must keep a boxed runtime push fallback" ); + // #7480: the fallback PUSH is the subject; the fallback RECORDING is not. + // This test used to require `record_fallback_call` alongside the push, which + // conflated the two -- and the recording call is exactly what a default + // build no longer emits. Asserting its absence keeps the test honest about + // which of the two survives, and turns it into a second witness for the + // emission gate on the ordinary `arr.push(x)` shape (`LocalGet` is excluded + // from `expr_produces_canonical_raw_f64`, so this path is the common one, + // not an edge case). + assert!( + !ir.contains("call void @js_typed_feedback_record_fallback_call"), + "a default build must not emit the fallback RECORDING call; only the \ + fallback push itself is load-bearing" + ); } #[test] diff --git a/crates/perry-runtime/src/typed_feedback.rs b/crates/perry-runtime/src/typed_feedback.rs index 0027df2588..5d7bfc768a 100644 --- a/crates/perry-runtime/src/typed_feedback.rs +++ b/crates/perry-runtime/src/typed_feedback.rs @@ -400,6 +400,20 @@ fn registry() -> crate::gc::GcRootRegistryGuard<'static, TypedFeedbackRegistry> crate::gc::lock_gc_root_registry(®ISTRY) } +/// Has this process observed anything at all? +/// +/// #7480 step 4: instrumentation is emitted at COMPILE time (see +/// `perry-codegen`'s `typed_feedback_emission_enabled`), so a binary built +/// without `PERRY_TYPED_FEEDBACK[_TRACE]` and then *run* with it set has +/// nothing to report. Before, that produced a trace of unattributed sites — +/// counters with no module, function or source name, because registration was +/// already compile-gated. Now it produces nothing, which is the same amount of +/// information and looks far more like success. The trace dump uses this to say +/// so out loud rather than writing an empty file. +pub(crate) fn no_sites_were_instrumented() -> bool { + registry().sites.is_empty() +} + #[no_mangle] pub extern "C" fn js_typed_feedback_register_site( site_id: u64, diff --git a/crates/perry-runtime/src/typed_feedback/trace.rs b/crates/perry-runtime/src/typed_feedback/trace.rs index 3e4a07661b..36a0d15510 100644 --- a/crates/perry-runtime/src/typed_feedback/trace.rs +++ b/crates/perry-runtime/src/typed_feedback/trace.rs @@ -326,6 +326,22 @@ pub extern "C" fn js_typed_feedback_maybe_dump_trace() { return; } + // #7480 step 4: a trace was asked for and there is nothing to put in + // it. Overwhelmingly that means the binary was compiled without + // `PERRY_TYPED_FEEDBACK[_TRACE]` in the environment — instrumentation + // is emitted at compile time, so setting the var only at run time + // instruments nothing. Say it, rather than writing `{"sites": []}` and + // letting the reader conclude their program has no dynamic boundaries. + if super::no_sites_were_instrumented() { + eprintln!( + "perry typed-feedback trace: the registry is empty, so this \ + trace has no sites. The most common cause is a binary compiled \ + without PERRY_TYPED_FEEDBACK[_TRACE] in the environment — \ + instrumentation is emitted at COMPILE time, so setting the \ + variable only for the run instruments nothing." + ); + } + let json = typed_feedback_trace_json(); let bytes = match serde_json::to_vec_pretty(&json) { Ok(bytes) => bytes,