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.
This commit is contained in:
Takashi Kokubun 2026-03-19 09:51:06 -07:00
parent e7fa7293fd
commit af459daea4
Notes: git 2026-03-20 01:06:38 +00:00
5 changed files with 41 additions and 11 deletions

11
vm.c
View File

@ -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++;

View File

@ -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")

View File

@ -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(

View File

@ -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<Function, ParseError> {
}
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<Function, ParseError> {
}
}
// 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<Function, ParseError> {
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<Function, ParseError> {
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<Function, ParseError> {
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<Function, ParseError> {
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<Function, ParseError> {
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))?;

View File

@ -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,
}
}