@@ -15,7 +15,7 @@ use crate::invariants::{
1515 track_root_box_assumption
1616} ;
1717use crate :: gc:: append_gc_offsets;
18- use crate :: payload:: { get_or_create_iseq_payload, IseqCodePtrs , IseqPayload , IseqVersion , IseqVersionRef , IseqStatus } ;
18+ use crate :: payload:: { get_or_create_iseq_payload, IseqCodePtrs , IseqPayload , IseqVersion , IseqVersionRef , IseqStatus , RecompileSignal , RecompileAction , RecompileState } ;
1919use crate :: state:: ZJITState ;
2020use crate :: stats:: { CompileError , exit_counter_for_compile_error, exit_counter_for_unhandled_hir_insn, incr_counter, incr_counter_by, send_fallback_counter, send_fallback_counter_for_method_type, send_fallback_counter_for_super_method_type, send_fallback_counter_ptr_for_opcode, send_without_block_fallback_counter_for_method_type, send_without_block_fallback_counter_for_optimized_method_type} ;
2121use crate :: stats:: { counter_ptr, with_time_stat, Counter , Counter :: { compile_time_ns, exit_compile_error} } ;
@@ -24,7 +24,7 @@ use crate::backend::lir::{self, Assembler, C_ARG_OPNDS, C_RET_OPND, CFP, EC, NAT
2424use crate :: hir:: { iseq_to_hir, BlockId , Invariant , RangeType , SideExitReason :: { self , * } , SpecialBackrefSymbol , SpecialObjectType } ;
2525use crate :: hir:: { Const , FrameState , Function , Insn , InsnId , SendFallbackReason } ;
2626use crate :: hir_type:: { types, Type } ;
27- use crate :: options:: { get_option, rb_zjit_call_threshold } ;
27+ use crate :: options:: get_option;
2828use crate :: profile:: ProfiledType ;
2929use crate :: cast:: IntoUsize ;
3030
@@ -36,32 +36,24 @@ pub const MAX_ISEQ_VERSIONS: usize = 2;
3636pub extern "C" fn rb_zjit_count_side_exit ( payload_raw : * mut std:: ffi:: c_void ) {
3737 if payload_raw. is_null ( ) { return ; }
3838 let payload = unsafe { & mut * ( payload_raw as * mut IseqPayload ) } ;
39- let threshold = get_option ! ( recompile_threshold) as u64 ;
40- if threshold == 0 || payload. side_exit_count >= threshold { return ; }
41- payload. side_exit_count += 1 ;
42- if payload. side_exit_count == threshold && payload. versions . len ( ) < MAX_ISEQ_VERSIONS {
39+ if let RecompileAction :: Recompile { preserve_profiles } =
40+ payload. recompile . on_signal ( RecompileSignal :: SideExit , payload. versions . len ( ) )
41+ {
4342 let iseq = match payload. versions . last ( ) {
4443 Some ( version_ref) => unsafe { version_ref. as_ref ( ) } . iseq ,
4544 None => return ,
4645 } ;
4746 with_vm_lock ( src_loc ! ( ) , || {
48- trigger_recompilation ( payload_raw, iseq, true ) ;
47+ trigger_recompilation ( payload_raw, iseq, preserve_profiles ) ;
4948 } ) ;
5049 }
5150}
5251
5352static GLOBAL_RECOMPILE_COUNT : AtomicU64 = AtomicU64 :: new ( 0 ) ;
5453
55- /// Escalating threshold for deferred re-profiling. Higher deferral levels
56- /// give cold branches progressively more time to warm up.
57- fn deferred_threshold ( defer_count : u32 ) -> u32 {
58- match defer_count {
59- 1 => unsafe { rb_zjit_call_threshold as u32 } ,
60- 2 => 1_000 ,
61- _ => 100_000 ,
62- }
63- }
64-
54+ /// Execute a recompilation: check global cap, reset profiles, invalidate version,
55+ /// reset JIT func, re-enable profiling. All decisions about *whether* to recompile
56+ /// are made by RecompileState before this is called.
6557/// When `preserve_profiles` is true, only counters are reset (type distributions survive).
6658/// When false, both counters and type distributions are cleared.
6759fn trigger_recompilation ( payload_raw : * mut std:: ffi:: c_void , iseq : IseqPtr , preserve_profiles : bool ) {
@@ -84,10 +76,8 @@ fn trigger_recompilation(payload_raw: *mut std::ffi::c_void, iseq: IseqPtr, pres
8476 payload. profile . reset_for_recompile ( ) ;
8577 }
8678
87- // Reset deferral state so V2 compilation goes straight to building the HIR.
88- // If the HIR still has unresolved issues, the post-HIR deferral trigger handles escalation.
89- payload. defer_count = 0 ;
90- payload. deferred_stub_hits = 0 ;
79+ // Reset the state machine so V2 compilation goes straight to the HIR check.
80+ payload. recompile . reset_after_trigger ( ) ;
9181
9282 if let Some ( version) = payload. versions . last_mut ( ) {
9383 let version = unsafe { version. as_mut ( ) } ;
@@ -98,9 +88,6 @@ fn trigger_recompilation(payload_raw: *mut std::ffi::c_void, iseq: IseqPtr, pres
9888}
9989
10090/// Runtime helper called from JIT code to collect inline type feedback for NoProfile sends.
101- /// When a NoProfile send executes, this records the receiver's class into the profiling data
102- /// structure. After enough observations, triggers recompilation so the previously-NoProfile
103- /// sends compile to direct calls using the collected type data.
10491#[ unsafe( no_mangle) ]
10592pub extern "C" fn rb_zjit_inline_profile_send (
10693 payload_raw : * mut std:: ffi:: c_void ,
@@ -112,26 +99,27 @@ pub extern "C" fn rb_zjit_inline_profile_send(
11299 let payload = unsafe { & mut * ( payload_raw as * mut IseqPayload ) } ;
113100 let insn_idx = insn_idx as usize ;
114101
115- let threshold = ( get_option ! ( recompile_threshold) as u64 ) / 2 ;
116- if threshold == 0 || payload. no_profile_send_hits >= threshold { return ; }
117-
118- payload. no_profile_send_hits += 1 ;
119-
120- if payload. no_profile_send_hits == threshold && payload. versions . len ( ) < MAX_ISEQ_VERSIONS {
121- if !payload. profile . inline_feedback_is_high_quality ( ) {
102+ let quality_ok = payload. profile . inline_feedback_is_high_quality ( ) ;
103+ let action = payload. recompile . on_signal (
104+ RecompileSignal :: InlineSend { quality_ok } ,
105+ payload. versions . len ( ) ,
106+ ) ;
107+ match action {
108+ RecompileAction :: Recompile { preserve_profiles } => {
109+ let iseq = match payload. versions . last ( ) {
110+ Some ( version_ref) => unsafe { version_ref. as_ref ( ) } . iseq ,
111+ None => return ,
112+ } ;
113+ with_vm_lock ( src_loc ! ( ) , || {
114+ trigger_recompilation ( payload_raw, iseq, preserve_profiles) ;
115+ } ) ;
122116 return ;
123117 }
124- payload. has_inline_feedback = true ;
125- let iseq = match payload. versions . last ( ) {
126- Some ( version_ref) => unsafe { version_ref. as_ref ( ) } . iseq ,
127- None => return ,
128- } ;
129- with_vm_lock ( src_loc ! ( ) , || {
130- trigger_recompilation ( payload_raw, iseq, true ) ;
131- } ) ;
132- return ;
118+ RecompileAction :: Ignore => return ,
119+ _ => { }
133120 }
134121
122+ // Type recording stays outside the state machine (data collection, not a decision).
135123 const INLINE_PROFILE_LIMIT : u32 = 5 ;
136124 if payload. profile . num_profiles_for ( insn_idx) >= INLINE_PROFILE_LIMIT { return ; }
137125
@@ -145,24 +133,20 @@ pub extern "C" fn rb_zjit_inline_profile_send(
145133}
146134
147135/// Lightweight runtime helper for not_monomorphic ivar fallbacks.
148- /// Only increments the recompilation trigger counter — no type recording.
149136#[ unsafe( no_mangle) ]
150137pub extern "C" fn rb_zjit_count_ivar_fallback ( payload_raw : * mut std:: ffi:: c_void ) {
151138 if payload_raw. is_null ( ) { return ; }
152139 let payload = unsafe { & mut * ( payload_raw as * mut IseqPayload ) } ;
153140
154- let threshold = get_option ! ( recompile_threshold) as u64 ;
155- if threshold == 0 || payload. no_profile_send_hits >= threshold { return ; }
156-
157- payload. no_profile_send_hits += 1 ;
158-
159- if payload. no_profile_send_hits == threshold && payload. versions . len ( ) < MAX_ISEQ_VERSIONS {
141+ if let RecompileAction :: Recompile { preserve_profiles } =
142+ payload. recompile . on_signal ( RecompileSignal :: IvarFallback , payload. versions . len ( ) )
143+ {
160144 let iseq = match payload. versions . last ( ) {
161145 Some ( version_ref) => unsafe { version_ref. as_ref ( ) } . iseq ,
162146 None => return ,
163147 } ;
164148 with_vm_lock ( src_loc ! ( ) , || {
165- trigger_recompilation ( payload_raw, iseq, true ) ;
149+ trigger_recompilation ( payload_raw, iseq, preserve_profiles ) ;
166150 } ) ;
167151 }
168152}
@@ -333,20 +317,16 @@ fn gen_iseq_entry_point(cb: &mut CodeBlock, iseq: IseqPtr, jit_exception: bool)
333317 return Err ( CompileError :: ExceptionHandler ) ;
334318 }
335319
336- // If this ISEQ is in a deferred re-profiling window, don't compile yet.
337- // Count this interpreter entry toward the threshold and keep the ISEQ
338- // running in the interpreter with profiling active. Both interpreter
339- // entries and stub fallbacks count toward the same escalating threshold.
320+ // Check the recompilation state machine for deferral.
340321 {
341322 let payload = get_or_create_iseq_payload ( iseq) ;
342- if payload. defer_count > 0 {
343- let threshold = deferred_threshold ( payload. defer_count ) ;
344- if payload. deferred_stub_hits < threshold {
345- let call_threshold = unsafe { rb_zjit_call_threshold as u32 } ;
346- payload. deferred_stub_hits += call_threshold;
323+ let call_threshold = unsafe { crate :: options:: rb_zjit_call_threshold as u32 } ;
324+ match payload. recompile . on_signal ( RecompileSignal :: Entry { credit : call_threshold } , payload. versions . len ( ) ) {
325+ RecompileAction :: DeferToInterpreter => {
347326 unsafe { rb_iseq_reset_jit_func ( iseq) } ;
348327 return Err ( CompileError :: DeferredForReprofiling ) ;
349328 }
329+ _ => { }
350330 }
351331 }
352332
@@ -355,29 +335,18 @@ fn gen_iseq_entry_point(cb: &mut CodeBlock, iseq: IseqPtr, jit_exception: bool)
355335 incr_counter ! ( failed_iseq_count) ;
356336 } ) ) ?;
357337
358- // Adaptive deferral for recompilations. First compilations never defer.
359- // For recompilations (latest version invalidated), if the HIR has a
360- // significant fraction of unresolved sends or any unresolved ivars,
361- // defer for 1K interpreter calls to exercise cold branches.
362- // A single dead-branch NoProfile send does NOT trigger deferral —
363- // ISEQs where most sends are well-profiled compile immediately.
338+ // Post-HIR quality check: decide if recompilation should be deferred.
364339 if get_option ! ( recompile_threshold) > 0 {
365340 let payload = get_or_create_iseq_payload ( iseq) ;
366341 let is_recompile = payload
367342 . versions
368343 . last ( )
369344 . map ( |v| unsafe { v. as_ref ( ) } . status == IseqStatus :: Invalidated )
370345 . unwrap_or ( false ) ;
371- // Use ratio-based check for sends: only defer if >25% of sends lack profiles.
372346 let ( no_profile_sends, total_sends) = function. count_no_profile_sends ( ) ;
373347 let sends_need_deferral = total_sends > 0 && no_profile_sends * 4 > total_sends;
374348 let has_unresolved = sends_need_deferral || function. has_not_monomorphic_ivars ( ) ;
375- let skip_deferral = payload. has_inline_feedback ;
376- if is_recompile && payload. defer_count < 2 && has_unresolved && !skip_deferral {
377- payload. defer_count = 2 ; // level 2: deferred_threshold(2) = 1K calls
378- payload. deferred_stub_hits = 0 ;
379- // Preserve inline feedback — only reset counters so the interpreter
380- // adds observations on top during the 1K-call deferral window.
349+ if let RecompileAction :: Defer = payload. recompile . post_hir_check ( is_recompile, has_unresolved) {
381350 payload. profile . reset_counters_for_recompile ( ) ;
382351 unsafe { rb_zjit_profile_enable ( iseq) } ;
383352 unsafe { rb_iseq_reset_jit_func ( iseq) } ;
@@ -1607,7 +1576,7 @@ fn gen_guarded_inline_profile(
16071576
16081577 asm_comment ! ( asm, "guard: skip inline profiling if self-disabled" ) ;
16091578 let payload_addr = asm. load ( Opnd :: UImm ( jit. payload_ptr as u64 ) ) ;
1610- let offset = std:: mem:: offset_of!( crate :: payload:: IseqPayload , no_profile_send_hits) as i32 ;
1579+ let offset = ( std:: mem:: offset_of!( crate :: payload:: IseqPayload , recompile ) + std :: mem :: offset_of! ( RecompileState , no_profile_send_hits) ) as i32 ;
16111580 let hits = asm. load ( Opnd :: mem ( 64 , payload_addr, offset) ) ;
16121581 asm. cmp ( hits, Opnd :: UImm ( threshold) ) ;
16131582 asm. jge ( jit, skip_edge ( ) ) ;
@@ -3100,32 +3069,14 @@ c_callable! {
31003069 let cb = ZJITState :: get_code_block( ) ;
31013070 let payload = get_or_create_iseq_payload( iseq) ;
31023071
3103- // If this ISEQ is being re-profiled after deferral, fall back to
3104- // the interpreter — the zjit_* profiling instructions are active
3105- // and collect type data on each fallback. The threshold escalates
3106- // with each deferral level to give cold branches progressively more
3107- // time to warm up. This gate fires for both first-compilation deferrals
3108- // (versions empty) and inline-triggered recompilation deferrals
3109- // (latest version invalidated).
3110- let latest_invalidated = payload. versions. last( )
3111- . map( |v| unsafe { v. as_ref( ) } . status == IseqStatus :: Invalidated )
3112- . unwrap_or( false ) ;
3113- if payload. defer_count > 0 && ( payload. versions. is_empty( ) || latest_invalidated) {
3114- // Count stub hits toward the deferral threshold for BOTH initial
3115- // deferrals (versions empty) and recompilation deferrals (latest
3116- // invalidated). Previously, recompilation deferrals returned the
3117- // exit trampoline unconditionally without counting, causing the
3118- // method to stay in the interpreter indefinitely — a catastrophic
3119- // overhead for hot methods (addressable-merge lost 2.5s).
3120- let threshold = deferred_threshold( payload. defer_count) ;
3121- payload. deferred_stub_hits += 1 ;
3122- if payload. deferred_stub_hits <= threshold {
3123- // Still collecting profile data — fall back to interpreter
3072+ // Check the recompilation state machine for deferral.
3073+ match payload. recompile. on_signal( RecompileSignal :: Entry { credit: 1 } , payload. versions. len( ) ) {
3074+ RecompileAction :: DeferToInterpreter => {
31243075 unsafe { Rc :: increment_strong_count( iseq_call_ptr as * const IseqCall ) ; }
31253076 prepare_for_exit( iseq, cfp, sp, & CompileError :: DeferredForReprofiling ) ;
31263077 return ZJITState :: get_exit_trampoline( ) . raw_ptr( cb) ;
31273078 }
3128- // Enough profile data collected — fall through to compile
3079+ _ => { }
31293080 }
31303081
31313082 let last_status = payload. versions. last( ) . map( |version| & unsafe { version. as_ref( ) } . status) ;
0 commit comments