From af459daea4afd67ada06c5fbde11501cd96db96e Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 19 Mar 2026 09:51:06 -0700 Subject: [PATCH] ZJIT: Use counted side exits for send fallbacks while tracing Instead of blocking compilation entirely when tracing is active, emit counted side exits in HIR before send fallback instructions that use VM_EXEC (which sets FLAG_FINISH on the callee frame, breaking throw TAG_RETURN semantics). This gives us runtime stats on how often tracing causes side exits while still allowing compilation. The tracing check is done at HIR construction time so that Send fallback instructions are never emitted into the IR when tracing is active, replaced by SideExit instructions instead. Affected YARV instructions: opt_neq, opt_send_without_block (and all opt_* variants), send, sendforward, invokesuper, invokesuperforward, invokeblock. --- vm.c | 11 ---------- zjit/bindgen/src/main.rs | 1 + zjit/src/cruby_bindings.inc.rs | 1 + zjit/src/hir.rs | 37 ++++++++++++++++++++++++++++++++++ zjit/src/stats.rs | 2 ++ 5 files changed, 41 insertions(+), 11 deletions(-) diff --git a/vm.c b/vm.c index 0fa87a63ef..916e379d67 100644 --- a/vm.c +++ b/vm.c @@ -533,23 +533,12 @@ yjit_compile(rb_execution_context_t *ec) #endif #if USE_ZJIT -bool rb_zjit_iseq_tracing_currently_enabled(void); - static inline rb_jit_func_t zjit_compile(rb_execution_context_t *ec) { const rb_iseq_t *iseq = ec->cfp->iseq; struct rb_iseq_constant_body *body = ISEQ_BODY(iseq); - // Don't compile while tracing is active. ZJIT's send fallback uses - // rb_vm_opt_send_without_block which calls VM_EXEC, setting FLAG_FINISH - // on the callee frame. This changes exception handling semantics for - // throw TAG_RETURN (e.g. return from rescue), causing TracePoint to - // report nil instead of the actual return value. [Bug #21389] - if (rb_zjit_iseq_tracing_currently_enabled()) { - return NULL; - } - if (body->jit_entry == NULL) { body->jit_entry_calls++; diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index fa61f61481..3e82efd8f6 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -292,6 +292,7 @@ fn main() { .allowlist_function("rb_set_cfp_(pc|sp)") .allowlist_function("rb_c_method_tracing_currently_enabled") .allowlist_function("rb_zjit_method_tracing_currently_enabled") + .allowlist_function("rb_zjit_iseq_tracing_currently_enabled") .allowlist_function("rb_full_cfunc_return") .allowlist_function("rb_assert_(iseq|cme)_handle") .allowlist_function("rb_IMEMO_TYPE_P") diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 2b643d22dd..41ebdb0f55 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -2151,6 +2151,7 @@ unsafe extern "C" { pub fn rb_zjit_singleton_class_p(klass: VALUE) -> bool; pub fn rb_zjit_defined_ivar(obj: VALUE, id: ID, pushval: VALUE) -> VALUE; pub fn rb_zjit_method_tracing_currently_enabled() -> bool; + pub fn rb_zjit_iseq_tracing_currently_enabled() -> bool; pub fn rb_zjit_insn_leaf(insn: ::std::os::raw::c_int, opes: *const VALUE) -> bool; pub fn rb_zjit_local_id(iseq: *const rb_iseq_t, idx: ::std::os::raw::c_uint) -> ID; pub fn rb_zjit_cme_is_cfunc( diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 10c420336b..4471827986 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -532,6 +532,7 @@ pub enum SideExitReason { SplatKwPolymorphic, SplatKwNotProfiled, DirectiveInduced, + SendWhileTracing, } #[derive(Debug, Clone, Copy)] @@ -7509,6 +7510,11 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { } let argc = unsafe { vm_ci_argc((*cd).ci) }; + // Side-exit send fallbacks while tracing to avoid FLAG_FINISH breaking throw TAG_RETURN semantics + if unsafe { rb_zjit_iseq_tracing_currently_enabled() } { + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SendWhileTracing }); + break; + } let args = state.stack_pop_n(argc as usize)?; let recv = state.stack_pop()?; let send = fun.push_insn(block, Insn::Send { recv, cd, blockiseq: None, args, state: exit_id, reason: Uncategorized(opcode) }); @@ -7638,6 +7644,12 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { } } + // Side-exit send fallbacks while tracing to avoid FLAG_FINISH breaking throw TAG_RETURN semantics + if unsafe { rb_zjit_iseq_tracing_currently_enabled() } { + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SendWhileTracing }); + break; + } + { fn new_branch_block( fun: &mut Function, @@ -7724,6 +7736,11 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledCallType(call_type) }); break; // End the block } + // Side-exit send fallbacks while tracing to avoid FLAG_FINISH breaking throw TAG_RETURN semantics + if unsafe { rb_zjit_iseq_tracing_currently_enabled() } { + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SendWhileTracing }); + break; + } let argc = unsafe { vm_ci_argc((*cd).ci) }; let block_arg = (flags & VM_CALL_ARGS_BLOCKARG) != 0; @@ -7758,6 +7775,11 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledCallType(call_type) }); break; // End the block } + // Side-exit send fallbacks while tracing to avoid FLAG_FINISH breaking throw TAG_RETURN semantics + if unsafe { rb_zjit_iseq_tracing_currently_enabled() } { + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SendWhileTracing }); + break; + } let argc = unsafe { vm_ci_argc((*cd).ci) }; let args = state.stack_pop_n(argc as usize + usize::from(forwarding))?; @@ -7787,6 +7809,11 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledCallType(call_type) }); break; // End the block } + // Side-exit send fallbacks while tracing to avoid FLAG_FINISH breaking throw TAG_RETURN semantics + if unsafe { rb_zjit_iseq_tracing_currently_enabled() } { + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SendWhileTracing }); + break; + } let argc = unsafe { vm_ci_argc((*cd).ci) }; let block_arg = (flags & VM_CALL_ARGS_BLOCKARG) != 0; let args = state.stack_pop_n(argc as usize + usize::from(block_arg))?; @@ -7821,6 +7848,11 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledCallType(call_type) }); break; // End the block } + // Side-exit send fallbacks while tracing to avoid FLAG_FINISH breaking throw TAG_RETURN semantics + if unsafe { rb_zjit_iseq_tracing_currently_enabled() } { + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SendWhileTracing }); + break; + } let argc = unsafe { vm_ci_argc((*cd).ci) }; let args = state.stack_pop_n(argc as usize + usize::from(forwarding))?; let recv = state.stack_pop()?; @@ -7851,6 +7883,11 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnhandledCallType(call_type) }); break; // End the block } + // Side-exit send fallbacks while tracing to avoid FLAG_FINISH breaking throw TAG_RETURN semantics + if unsafe { rb_zjit_iseq_tracing_currently_enabled() } { + fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::SendWhileTracing }); + break; + } let argc = unsafe { vm_ci_argc((*cd).ci) }; let block_arg = (flags & VM_CALL_ARGS_BLOCKARG) != 0; let args = state.stack_pop_n(argc as usize + usize::from(block_arg))?; diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 47a434360d..66f418067c 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -232,6 +232,7 @@ make_counters! { exit_splatkw_polymorphic, exit_splatkw_not_profiled, exit_directive_induced, + exit_send_while_tracing, } // Send fallback counters that are summed as dynamic_send_count @@ -616,6 +617,7 @@ pub fn side_exit_counter(reason: crate::hir::SideExitReason) -> Counter { => exit_patchpoint_no_singleton_class, PatchPoint(Invariant::RootBoxOnly) => exit_patchpoint_root_box_only, + SendWhileTracing => exit_send_while_tracing, } }