@@ -181,6 +181,8 @@ pub struct JitNyiDoc {
181181 pub reason : & ' static str ,
182182}
183183
184+ const CALLABLE_SIDE_EXIT_BACKOFF_THRESHOLD : u32 = 64 ;
185+
184186pub struct TraceJitEngine {
185187 config : JitConfig ,
186188 hot_counts : HashMap < TraceEntryKey , u32 > ,
@@ -189,6 +191,8 @@ pub struct TraceJitEngine {
189191 loop_headers : Option < Vec < bool > > ,
190192 non_yielding_host_imports : Vec < bool > ,
191193 traces : Vec < JitTrace > ,
194+ callable_side_exit_streaks : Vec < u32 > ,
195+ blocked_callable_frames : Vec < bool > ,
192196 attempts : Vec < JitAttempt > ,
193197}
194198
@@ -208,6 +212,8 @@ impl TraceJitEngine {
208212 loop_headers : None ,
209213 non_yielding_host_imports : Vec :: new ( ) ,
210214 traces : Vec :: new ( ) ,
215+ callable_side_exit_streaks : Vec :: new ( ) ,
216+ blocked_callable_frames : Vec :: new ( ) ,
211217 attempts : Vec :: new ( ) ,
212218 }
213219 }
@@ -223,6 +229,8 @@ impl TraceJitEngine {
223229 self . blocked_entries . clear ( ) ;
224230 self . loop_headers = None ;
225231 self . traces . clear ( ) ;
232+ self . callable_side_exit_streaks . clear ( ) ;
233+ self . blocked_callable_frames . clear ( ) ;
226234 self . attempts . clear ( ) ;
227235 }
228236
@@ -236,6 +244,8 @@ impl TraceJitEngine {
236244 self . blocked_entries . clear ( ) ;
237245 self . loop_headers = None ;
238246 self . traces . clear ( ) ;
247+ self . callable_side_exit_streaks . clear ( ) ;
248+ self . blocked_callable_frames . clear ( ) ;
239249 self . attempts . clear ( ) ;
240250 true
241251 }
@@ -262,7 +272,10 @@ impl TraceJitEngine {
262272 entry_local_types : Option < & [ crate :: ValueType ] > ,
263273 program : & Program ,
264274 ) -> Option < usize > {
265- if !self . config . enabled || !native_jit_supported ( ) {
275+ if !self . config . enabled
276+ || !native_jit_supported ( )
277+ || self . callable_frame_is_blocked ( frame_key)
278+ {
266279 return None ;
267280 }
268281 let key = TraceEntryKey {
@@ -323,7 +336,10 @@ impl TraceJitEngine {
323336 entry_local_types : Option < & [ crate :: ValueType ] > ,
324337 program : & Program ,
325338 ) -> Option < usize > {
326- if !self . config . enabled || !native_jit_supported ( ) {
339+ if !self . config . enabled
340+ || !native_jit_supported ( )
341+ || self . callable_frame_is_blocked ( frame_key)
342+ {
327343 return None ;
328344 }
329345 let key = TraceEntryKey {
@@ -385,6 +401,60 @@ impl TraceJitEngine {
385401 }
386402 }
387403
404+ pub ( crate ) fn record_native_side_exit ( & mut self , trace_id : usize ) -> bool {
405+ if self
406+ . traces
407+ . get ( trace_id)
408+ . is_none_or ( |trace| trace. frame_key == ROOT_FRAME_KEY )
409+ {
410+ return false ;
411+ }
412+ let Some ( streak) = self . callable_side_exit_streaks . get_mut ( trace_id) else {
413+ return false ;
414+ } ;
415+ * streak = streak. saturating_add ( 1 ) ;
416+ * streak >= CALLABLE_SIDE_EXIT_BACKOFF_THRESHOLD
417+ }
418+
419+ pub ( crate ) fn record_native_loop_back ( & mut self , trace_id : usize ) {
420+ if let Some ( streak) = self . callable_side_exit_streaks . get_mut ( trace_id) {
421+ * streak = 0 ;
422+ }
423+ }
424+
425+ pub ( crate ) fn callable_frame_is_blocked ( & self , frame_key : u64 ) -> bool {
426+ if frame_key == ROOT_FRAME_KEY {
427+ return false ;
428+ }
429+ usize:: try_from ( frame_key)
430+ . ok ( )
431+ . and_then ( |index| self . blocked_callable_frames . get ( index) )
432+ . copied ( )
433+ . unwrap_or ( false )
434+ }
435+
436+ pub ( crate ) fn block_callable_frame ( & mut self , trace_id : usize ) {
437+ let Some ( frame_key) = self . traces . get ( trace_id) . map ( |trace| trace. frame_key ) else {
438+ return ;
439+ } ;
440+ if frame_key == ROOT_FRAME_KEY {
441+ self . block_trace ( trace_id) ;
442+ return ;
443+ }
444+ let Ok ( frame_index) = usize:: try_from ( frame_key) else {
445+ self . block_trace ( trace_id) ;
446+ return ;
447+ } ;
448+ if self . blocked_callable_frames . len ( ) <= frame_index {
449+ self . blocked_callable_frames . resize ( frame_index + 1 , false ) ;
450+ }
451+ self . blocked_callable_frames [ frame_index] = true ;
452+ for entries in & mut self . compiled_by_ip {
453+ entries. retain ( |( entry_frame_key, _, _) | * entry_frame_key != frame_key) ;
454+ }
455+ self . hot_counts . retain ( |key, _| key. frame_key != frame_key) ;
456+ }
457+
388458 pub ( crate ) fn block_trace ( & mut self , trace_id : usize ) {
389459 if let Some ( trace) = self . traces . get ( trace_id) {
390460 let key = TraceEntryKey {
@@ -570,6 +640,7 @@ impl TraceJitEngine {
570640 . and_then ( |debug| debug. line_for_offset ( key. root_ip ) ) ;
571641 let trace = build_jit_trace ( id, key, start_line, recorded) ;
572642 self . traces . push ( trace) ;
643+ self . callable_side_exit_streaks . push ( 0 ) ;
573644 Ok ( id)
574645 }
575646
@@ -779,6 +850,67 @@ mod tests {
779850 assert ! ( !headers[ branch_ip as usize ] ) ;
780851 }
781852
853+ #[ test]
854+ fn callable_side_exit_backoff_resets_on_native_loopback ( ) {
855+ if !native_jit_supported ( ) {
856+ return ;
857+ }
858+ let mut bc = BytecodeBuilder :: new ( ) ;
859+ let root_ip = bc. position ( ) ;
860+ bc. ldloc ( 0 ) ;
861+ bc. ldc ( 0 ) ;
862+ bc. add ( ) ;
863+ bc. stloc ( 0 ) ;
864+ bc. br ( root_ip) ;
865+ let mut program = Program :: new ( vec ! [ Value :: Int ( 1 ) ] , bc. finish ( ) ) . with_local_count ( 1 ) ;
866+ program. script_functions . push ( ScriptFunction {
867+ entry_ip : root_ip,
868+ end_ip : program. code . len ( ) as u32 ,
869+ } ) ;
870+ program. callable_prototypes . push ( CallablePrototype {
871+ kind : CallableKind :: FunctionItem ,
872+ target : CallableTarget :: ScriptFunction ( 0 ) ,
873+ arity : 0 ,
874+ frame_local_count : 1 ,
875+ parameter_slots : Vec :: new ( ) ,
876+ capture_source_slots : Vec :: new ( ) ,
877+ capture_slots : Vec :: new ( ) ,
878+ capture_modes : Vec :: new ( ) ,
879+ self_slot : None ,
880+ schema : None ,
881+ } ) ;
882+ let mut engine = TraceJitEngine :: new ( JitConfig {
883+ enabled : true ,
884+ hot_loop_threshold : 1 ,
885+ max_trace_len : 64 ,
886+ } ) ;
887+ let trace_id = engine
888+ . observe_hot_entry ( 0 , root_ip as usize , 0 , & program)
889+ . expect ( "script-frame trace should compile" ) ;
890+
891+ for _ in 1 ..CALLABLE_SIDE_EXIT_BACKOFF_THRESHOLD {
892+ assert ! ( !engine. record_native_side_exit( trace_id) ) ;
893+ }
894+ engine. record_native_loop_back ( trace_id) ;
895+ for _ in 1 ..CALLABLE_SIDE_EXIT_BACKOFF_THRESHOLD {
896+ assert ! ( !engine. record_native_side_exit( trace_id) ) ;
897+ }
898+ assert ! ( engine. record_native_side_exit( trace_id) ) ;
899+ engine. block_callable_frame ( trace_id) ;
900+ assert ! ( engine. callable_frame_is_blocked( 0 ) ) ;
901+ assert_eq ! (
902+ engine. compiled_trace_for_entry( 0 , root_ip as usize , 0 ) ,
903+ None
904+ ) ;
905+
906+ let root_trace_id = engine
907+ . observe_hot_entry ( ROOT_FRAME_KEY , root_ip as usize , 0 , & program)
908+ . expect ( "root trace should compile" ) ;
909+ for _ in 0 ..CALLABLE_SIDE_EXIT_BACKOFF_THRESHOLD * 2 {
910+ assert ! ( !engine. record_native_side_exit( root_trace_id) ) ;
911+ }
912+ }
913+
782914 #[ test]
783915 fn trace_entry_cache_separates_root_and_script_frames ( ) {
784916 if !native_jit_supported ( ) {
0 commit comments