@@ -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;
27+ use crate :: options:: { get_option, rb_zjit_call_threshold } ;
2828use crate :: cast:: IntoUsize ;
2929
3030/// At the moment, we support recompiling each ISEQ only once.
@@ -52,6 +52,16 @@ pub extern "C" fn rb_zjit_count_side_exit(payload_raw: *mut std::ffi::c_void) {
5252static GLOBAL_RECOMPILE_COUNT : AtomicU64 = AtomicU64 :: new ( 0 ) ;
5353const MAX_GLOBAL_RECOMPILATIONS : u64 = 50 ;
5454
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+
5565fn trigger_recompilation ( payload_raw : * mut std:: ffi:: c_void , iseq : IseqPtr ) {
5666 if MAX_GLOBAL_RECOMPILATIONS > 0 {
5767 let prev = GLOBAL_RECOMPILE_COUNT . fetch_add ( 1 , Ordering :: Relaxed ) ;
@@ -64,6 +74,12 @@ fn trigger_recompilation(payload_raw: *mut std::ffi::c_void, iseq: IseqPtr) {
6474 debug ! ( "trigger_recompilation: recompiling {}" , iseq_get_location( iseq, 0 ) ) ;
6575 incr_counter ! ( recompile_count) ;
6676 payload. profile . reset_for_recompile ( ) ;
77+
78+ // Reset deferral state so V2 compilation goes straight to building the HIR.
79+ // If the HIR still has unresolved issues, the post-HIR deferral trigger handles escalation.
80+ payload. defer_count = 0 ;
81+ payload. deferred_stub_hits = 0 ;
82+
6783 if let Some ( version) = payload. versions . last_mut ( ) {
6884 let version = unsafe { version. as_mut ( ) } ;
6985 version. status = IseqStatus :: Invalidated ;
@@ -232,11 +248,56 @@ fn gen_iseq_entry_point(cb: &mut CodeBlock, iseq: IseqPtr, jit_exception: bool)
232248 return Err ( CompileError :: ExceptionHandler ) ;
233249 }
234250
251+ // If this ISEQ is in a deferred re-profiling window, don't compile yet.
252+ // Count this interpreter entry toward the threshold and keep the ISEQ
253+ // running in the interpreter with profiling active. Both interpreter
254+ // entries and stub fallbacks count toward the same escalating threshold.
255+ {
256+ let payload = get_or_create_iseq_payload ( iseq) ;
257+ if payload. defer_count > 0 {
258+ let threshold = deferred_threshold ( payload. defer_count ) ;
259+ if payload. deferred_stub_hits < threshold {
260+ let call_threshold = unsafe { rb_zjit_call_threshold as u32 } ;
261+ payload. deferred_stub_hits += call_threshold;
262+ unsafe { rb_iseq_reset_jit_func ( iseq) } ;
263+ return Err ( CompileError :: DeferredForReprofiling ) ;
264+ }
265+ }
266+ }
267+
235268 // Compile ISEQ into High-level IR
236269 let function = crate :: stats:: with_time_stat ( Counter :: compile_hir_time_ns, || compile_iseq ( iseq) . inspect_err ( |_| {
237270 incr_counter ! ( failed_iseq_count) ;
238271 } ) ) ?;
239272
273+ // Adaptive deferral for recompilations. First compilations never defer.
274+ // For recompilations (latest version invalidated), if the HIR has a
275+ // significant fraction of unresolved sends or any unresolved ivars,
276+ // defer for 1K interpreter calls to exercise cold branches.
277+ // A single dead-branch NoProfile send does NOT trigger deferral —
278+ // ISEQs where most sends are well-profiled compile immediately.
279+ if get_option ! ( recompile_threshold) > 0 {
280+ let payload = get_or_create_iseq_payload ( iseq) ;
281+ let is_recompile = payload
282+ . versions
283+ . last ( )
284+ . map ( |v| unsafe { v. as_ref ( ) } . status == IseqStatus :: Invalidated )
285+ . unwrap_or ( false ) ;
286+ // Use ratio-based check for sends: only defer if >25% of sends lack profiles.
287+ let ( no_profile_sends, total_sends) = function. count_no_profile_sends ( ) ;
288+ let sends_need_deferral = total_sends > 0 && no_profile_sends * 4 > total_sends;
289+ let has_unresolved = sends_need_deferral || function. has_not_monomorphic_ivars ( ) ;
290+ if is_recompile && payload. defer_count < 2 && has_unresolved {
291+ payload. defer_count = 2 ; // level 2: deferred_threshold(2) = 1K calls
292+ payload. deferred_stub_hits = 0 ;
293+ payload. profile . reset_for_recompile ( ) ;
294+ unsafe { rb_zjit_profile_enable ( iseq) } ;
295+ unsafe { rb_iseq_reset_jit_func ( iseq) } ;
296+ incr_counter ! ( recompile_count) ;
297+ return Err ( CompileError :: DeferredForReprofiling ) ;
298+ }
299+ }
300+
240301 // Compile the High-level IR
241302 let IseqCodePtrs { start_ptr, .. } = gen_iseq ( cb, iseq, Some ( & function) ) . inspect_err ( |err| {
242303 debug ! ( "{err:?}: gen_iseq failed: {}" , iseq_get_location( iseq, 0 ) ) ;
@@ -2881,6 +2942,35 @@ c_callable! {
28812942 // code path can be made read-only. But you still need the check as is while holding the VM lock in any case.
28822943 let cb = ZJITState :: get_code_block( ) ;
28832944 let payload = get_or_create_iseq_payload( iseq) ;
2945+
2946+ // If this ISEQ is being re-profiled after deferral, fall back to
2947+ // the interpreter — the zjit_* profiling instructions are active
2948+ // and collect type data on each fallback. The threshold escalates
2949+ // with each deferral level to give cold branches progressively more
2950+ // time to warm up. This gate fires for both first-compilation deferrals
2951+ // (versions empty) and inline-triggered recompilation deferrals
2952+ // (latest version invalidated).
2953+ let latest_invalidated = payload. versions. last( )
2954+ . map( |v| unsafe { v. as_ref( ) } . status == IseqStatus :: Invalidated )
2955+ . unwrap_or( false ) ;
2956+ if payload. defer_count > 0 && ( payload. versions. is_empty( ) || latest_invalidated) {
2957+ // Count stub hits toward the deferral threshold for BOTH initial
2958+ // deferrals (versions empty) and recompilation deferrals (latest
2959+ // invalidated). Previously, recompilation deferrals returned the
2960+ // exit trampoline unconditionally without counting, causing the
2961+ // method to stay in the interpreter indefinitely — a catastrophic
2962+ // overhead for hot methods (addressable-merge lost 2.5s).
2963+ let threshold = deferred_threshold( payload. defer_count) ;
2964+ payload. deferred_stub_hits += 1 ;
2965+ if payload. deferred_stub_hits <= threshold {
2966+ // Still collecting profile data — fall back to interpreter
2967+ unsafe { Rc :: increment_strong_count( iseq_call_ptr as * const IseqCall ) ; }
2968+ prepare_for_exit( iseq, cfp, sp, & CompileError :: DeferredForReprofiling ) ;
2969+ return ZJITState :: get_exit_trampoline( ) . raw_ptr( cb) ;
2970+ }
2971+ // Enough profile data collected — fall through to compile
2972+ }
2973+
28842974 let last_status = payload. versions. last( ) . map( |version| & unsafe { version. as_ref( ) } . status) ;
28852975 let compile_error = match last_status {
28862976 Some ( IseqStatus :: CantCompile ( err) ) => Some ( err) ,
0 commit comments