perf(typed-feedback): stop emitting recording calls into default builds (#7480) - #7702
Conversation
|
Warning Review limit reached
Next review available in: 4 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (16)
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. Comment |
Audit — BLOCKED on one broken test, and on the reason CI can't tell youThe gating mechanism is right and the anti-vacuity test you wrote for it is the best part of the PR. But this breaks a currently-green test, and I want to be precise about why neither of us saw it. The regression
ir.contains("call void @js_typed_feedback_record_fallback_call")
&& ir.contains("call i64 @js_array_push_f64")Bisected against your own parent commit:
Not an edge case: the value is The fix is the same one you already applied to the other three assertions: drop the The part worth more than the fixCI structurally could not have caught this. Per-PR So Everything else checks out
Fix that one assertion and rebase; I'll merge it. |
…ds (#7480) Typed feedback is opt-in, and every recording helper begins if site_id == 0 || !typed_feedback_enabled() { return; } which is false unless PERRY_TYPED_FEEDBACK[_TRACE] is set. Codegen emitted them regardless, on every execution of every dynamic property boundary. On churn_read_big.ts that is 22.3% of the program (record_guard_pass 11.2% + observe_property 11.1%, sample, 2465 leaf samples) -- not recording, just the call and the LazyLock acquire load each one performs to answer "no". js_typed_feedback_register_site has been gated on this same env since #5093's follow-up for this same reason; the gate stopped short of the recording it registers for. Route all five pure-bookkeeping helpers through emit_typed_feedback_record_call at all nine emit sites. Helpers that also perform the operation or pick the dispatch stay unconditional, enforced by a debug_assert on the callee name and asserted in the new test. Since registration was already gated, a default-built binary could only ever produce unattributed feedback anyway; the trace dump now says so out loud rather than writing an empty file.
…CORDING The test required record_fallback_call alongside js_array_push_f64, which conflated the two. The recording call is what a default build no longer emits; the push is the load-bearing half. Asserting the recording's absence makes this a second witness for the emission gate on the ordinary arr.push(x) shape. Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix
22d34ba to
f24da0d
Compare
Resolved and merging as v0.5.1407I fixed the assertion on the branch rather than bouncing it back.
Full Everything in the original audit stands: the gate is compile-time and already hashed into the object-cache key, the one CI consumer sets the var for both Gates 26/26. |
Part 2 of P2 (#7480 step 4). Independent of the element-shape PR; either can
land first.
The finding
The P2 brief asked for "feedback quiescence": a site that has seen the same shape
N times should stop paying
observe_property/record_guard. Reading theruntime first turned up something simpler. Typed feedback is opt-in, and
every recording helper begins:
typed_feedback_enabled()is false unlessPERRY_TYPED_FEEDBACK/PERRY_TYPED_FEEDBACK_TRACEis set. Codegen emitted the calls anyway, on everyexecution of every dynamic property boundary. On
churn_read_big.ts—200k × 1000 reads of
keep[j].v + keep[j].w— the two on the monomorphic-IC hotpath are 22.3% of the whole program:
main(generated code)js_dynamic_string_or_number_addtyped_feedback::record_guard_passtyped_feedback::observe_property(
sample, 2465 leaf samples,PERRY_DEBUG_SYMBOLS=1.) None of that isrecording. It is the cross-crate call, plus the
LazyLock<bool>acquire loadeach helper performs in order to decide it has nothing to do.
So there is no quiescence mechanism to build. There is a gate that already exists
and stops one line short.
The change
js_typed_feedback_register_sitehas been compile-gated on exactly this envsince #5093's follow-up, for exactly this reason — its doc comment says so, and
cites the same benchmark class. It simply never covered the recording it
registers for.
emit_typed_feedback_record_callnow routes all fivepure-bookkeeping helpers —
observe_property_{get,set},record_guard_{pass,fail},record_fallback_call— through the same switch, atall nine emit sites (generic-get diamond, array-push fallback, index realloc arm,
the two by-name lookup arms, method-override fallback, 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 stay unconditional. Adebug_assert!on the callee name rejects any attempt to route one of themthrough the eliding helper, and the new test asserts a default build still emits
js_typed_feedback_object_set_field_by_name_fast.This is strictly simpler than per-site quiescence and cannot freeze a site into a
wrong shape: recording is either fully on (profiling build) or fully absent
(default build), and the PIC's own deopt path is untouched.
Results
Quiet M1 mini. Measured together with the element-shape PR (both arms rebased
onto the #7690 stack —
origin/mainis GC-livelocked, see that PR):churn_read_hugeprofile after both: 100% of self time inmain.observe_property,record_guard_pass,js_dynamic_string_or_number_addand_tlv_get_addr(16.8% at baseline — TLS lookups these calls were driving) do notappear at all. The brief asked for ≤2% each.
No regression anywhere:
churn_alloc/push_cls/push_num/cycles/deeplist/tree/tree_wideall 1.00–1.03×, and a tape-defeatingjson_roundtripprobe (parse → touch every record → stringify, so the lazy tapeis materialized inside the timed region) is 0.761 s on both arms.
Behaviour
Profiling build unchanged.
typed_feedback_instruments_property_and_method_boundariessets the env and asserts every helper is still emitted; it passes untouched.
Polymorphic-after-monomorphic deopt (the brief's explicit ask) verified
end to end: a site converged on shape A for 1000 iterations, then shape B for
1000, then alternating, plus an
Object.createprototype read and adefinePropertyaccessor installed over a data property. Base, this PR, andnode all print
12053 1 7 42 11 undefined. This exercises the PIC miss arm,which is where two of the removed calls lived.
An empty trace now says so. Because registration was already compile-gated,
a default-built binary run with
PERRY_TYPED_FEEDBACK_TRACEcould only everproduce unattributed sites. With recording gated too it produces nothing,
which is the same amount of information and looks far more like success.
js_typed_feedback_maybe_dump_tracenow prints a one-line note instead ofwriting
"total_sites": 0.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; the wording is deliberately chosen so it
cannot send anyone chasing the wrong cause.
Validation
cargo test -p perry-codegen: 783/0 lib;--test typed_feedback17/0.cargo test -p perry-runtime --release: 1930/0.cargo fmt --all -- --check,scripts/check_file_size.sh,scripts/addr_class_inventory.py: clean.the base-compiler reproduction of the seven host-noise entries).
Three existing assertions changed, all of them the incidental
record_fallback_callrider on a test about something else(
typed_feedback_guards_direct_class_field_specialization,…_direct_closure_call_specialization, and the class-field fallback-lookupdata-flow test). Each keeps the assertion it actually exists for.
Note for review
The
changelog.d/fragment is keyed on a guessed PR number and should be renamedto the real one.