From b685059195cc1462c653c8fac2e0abe8b2f1cfe8 Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Thu, 16 Jul 2026 11:26:34 -0400 Subject: [PATCH 1/8] Use raise(3) instead of kill(2) for testing SEGV signal handler kill(getpid(), SIGSEGV) can be handled by other threads (as long as they don't block the signal) whereas raise(3) should be implemented as pthread_kill(getpid(), sig). I'm worried mainly about the timer thread handling the signal. We've been seeing CI failures in test_rubyoptions.rb that exercises the SEGV handler via `Process.kill` (which uses kill(2)). --- ext/-test-/fatal/depend | 161 ++++++++++++++++++++++++++++++++++ ext/-test-/fatal/segv.c | 15 ++++ test/ruby/test_rubyoptions.rb | 8 +- 3 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 ext/-test-/fatal/segv.c diff --git a/ext/-test-/fatal/depend b/ext/-test-/fatal/depend index 306bc9099c2dbd..cc534c2da4cdcb 100644 --- a/ext/-test-/fatal/depend +++ b/ext/-test-/fatal/depend @@ -483,4 +483,165 @@ rb_fatal.o: $(hdrdir)/ruby/ruby.h rb_fatal.o: $(hdrdir)/ruby/st.h rb_fatal.o: $(hdrdir)/ruby/subst.h rb_fatal.o: rb_fatal.c +segv.o: $(RUBY_EXTCONF_H) +segv.o: $(arch_hdrdir)/ruby/config.h +segv.o: $(hdrdir)/ruby.h +segv.o: $(hdrdir)/ruby/assert.h +segv.o: $(hdrdir)/ruby/backward.h +segv.o: $(hdrdir)/ruby/backward/2/assume.h +segv.o: $(hdrdir)/ruby/backward/2/attributes.h +segv.o: $(hdrdir)/ruby/backward/2/bool.h +segv.o: $(hdrdir)/ruby/backward/2/inttypes.h +segv.o: $(hdrdir)/ruby/backward/2/limits.h +segv.o: $(hdrdir)/ruby/backward/2/long_long.h +segv.o: $(hdrdir)/ruby/backward/2/stdalign.h +segv.o: $(hdrdir)/ruby/backward/2/stdarg.h +segv.o: $(hdrdir)/ruby/defines.h +segv.o: $(hdrdir)/ruby/intern.h +segv.o: $(hdrdir)/ruby/internal/abi.h +segv.o: $(hdrdir)/ruby/internal/anyargs.h +segv.o: $(hdrdir)/ruby/internal/arithmetic.h +segv.o: $(hdrdir)/ruby/internal/arithmetic/char.h +segv.o: $(hdrdir)/ruby/internal/arithmetic/double.h +segv.o: $(hdrdir)/ruby/internal/arithmetic/fixnum.h +segv.o: $(hdrdir)/ruby/internal/arithmetic/gid_t.h +segv.o: $(hdrdir)/ruby/internal/arithmetic/int.h +segv.o: $(hdrdir)/ruby/internal/arithmetic/intptr_t.h +segv.o: $(hdrdir)/ruby/internal/arithmetic/long.h +segv.o: $(hdrdir)/ruby/internal/arithmetic/long_long.h +segv.o: $(hdrdir)/ruby/internal/arithmetic/mode_t.h +segv.o: $(hdrdir)/ruby/internal/arithmetic/off_t.h +segv.o: $(hdrdir)/ruby/internal/arithmetic/pid_t.h +segv.o: $(hdrdir)/ruby/internal/arithmetic/short.h +segv.o: $(hdrdir)/ruby/internal/arithmetic/size_t.h +segv.o: $(hdrdir)/ruby/internal/arithmetic/st_data_t.h +segv.o: $(hdrdir)/ruby/internal/arithmetic/uid_t.h +segv.o: $(hdrdir)/ruby/internal/assume.h +segv.o: $(hdrdir)/ruby/internal/attr/alloc_size.h +segv.o: $(hdrdir)/ruby/internal/attr/artificial.h +segv.o: $(hdrdir)/ruby/internal/attr/cold.h +segv.o: $(hdrdir)/ruby/internal/attr/const.h +segv.o: $(hdrdir)/ruby/internal/attr/constexpr.h +segv.o: $(hdrdir)/ruby/internal/attr/deprecated.h +segv.o: $(hdrdir)/ruby/internal/attr/diagnose_if.h +segv.o: $(hdrdir)/ruby/internal/attr/enum_extensibility.h +segv.o: $(hdrdir)/ruby/internal/attr/error.h +segv.o: $(hdrdir)/ruby/internal/attr/flag_enum.h +segv.o: $(hdrdir)/ruby/internal/attr/forceinline.h +segv.o: $(hdrdir)/ruby/internal/attr/format.h +segv.o: $(hdrdir)/ruby/internal/attr/maybe_unused.h +segv.o: $(hdrdir)/ruby/internal/attr/noalias.h +segv.o: $(hdrdir)/ruby/internal/attr/nodiscard.h +segv.o: $(hdrdir)/ruby/internal/attr/noexcept.h +segv.o: $(hdrdir)/ruby/internal/attr/noinline.h +segv.o: $(hdrdir)/ruby/internal/attr/nonnull.h +segv.o: $(hdrdir)/ruby/internal/attr/noreturn.h +segv.o: $(hdrdir)/ruby/internal/attr/packed_struct.h +segv.o: $(hdrdir)/ruby/internal/attr/pure.h +segv.o: $(hdrdir)/ruby/internal/attr/restrict.h +segv.o: $(hdrdir)/ruby/internal/attr/returns_nonnull.h +segv.o: $(hdrdir)/ruby/internal/attr/warning.h +segv.o: $(hdrdir)/ruby/internal/attr/weakref.h +segv.o: $(hdrdir)/ruby/internal/cast.h +segv.o: $(hdrdir)/ruby/internal/compiler_is.h +segv.o: $(hdrdir)/ruby/internal/compiler_is/apple.h +segv.o: $(hdrdir)/ruby/internal/compiler_is/clang.h +segv.o: $(hdrdir)/ruby/internal/compiler_is/gcc.h +segv.o: $(hdrdir)/ruby/internal/compiler_is/intel.h +segv.o: $(hdrdir)/ruby/internal/compiler_is/msvc.h +segv.o: $(hdrdir)/ruby/internal/compiler_is/sunpro.h +segv.o: $(hdrdir)/ruby/internal/compiler_since.h +segv.o: $(hdrdir)/ruby/internal/config.h +segv.o: $(hdrdir)/ruby/internal/constant_p.h +segv.o: $(hdrdir)/ruby/internal/core.h +segv.o: $(hdrdir)/ruby/internal/core/rarray.h +segv.o: $(hdrdir)/ruby/internal/core/rbasic.h +segv.o: $(hdrdir)/ruby/internal/core/rbignum.h +segv.o: $(hdrdir)/ruby/internal/core/rclass.h +segv.o: $(hdrdir)/ruby/internal/core/rdata.h +segv.o: $(hdrdir)/ruby/internal/core/rfile.h +segv.o: $(hdrdir)/ruby/internal/core/rhash.h +segv.o: $(hdrdir)/ruby/internal/core/robject.h +segv.o: $(hdrdir)/ruby/internal/core/rregexp.h +segv.o: $(hdrdir)/ruby/internal/core/rstring.h +segv.o: $(hdrdir)/ruby/internal/core/rstruct.h +segv.o: $(hdrdir)/ruby/internal/core/rtypeddata.h +segv.o: $(hdrdir)/ruby/internal/ctype.h +segv.o: $(hdrdir)/ruby/internal/dllexport.h +segv.o: $(hdrdir)/ruby/internal/dosish.h +segv.o: $(hdrdir)/ruby/internal/error.h +segv.o: $(hdrdir)/ruby/internal/eval.h +segv.o: $(hdrdir)/ruby/internal/event.h +segv.o: $(hdrdir)/ruby/internal/fl_type.h +segv.o: $(hdrdir)/ruby/internal/gc.h +segv.o: $(hdrdir)/ruby/internal/glob.h +segv.o: $(hdrdir)/ruby/internal/globals.h +segv.o: $(hdrdir)/ruby/internal/has/attribute.h +segv.o: $(hdrdir)/ruby/internal/has/builtin.h +segv.o: $(hdrdir)/ruby/internal/has/c_attribute.h +segv.o: $(hdrdir)/ruby/internal/has/cpp_attribute.h +segv.o: $(hdrdir)/ruby/internal/has/declspec_attribute.h +segv.o: $(hdrdir)/ruby/internal/has/extension.h +segv.o: $(hdrdir)/ruby/internal/has/feature.h +segv.o: $(hdrdir)/ruby/internal/has/warning.h +segv.o: $(hdrdir)/ruby/internal/intern/array.h +segv.o: $(hdrdir)/ruby/internal/intern/bignum.h +segv.o: $(hdrdir)/ruby/internal/intern/class.h +segv.o: $(hdrdir)/ruby/internal/intern/compar.h +segv.o: $(hdrdir)/ruby/internal/intern/complex.h +segv.o: $(hdrdir)/ruby/internal/intern/cont.h +segv.o: $(hdrdir)/ruby/internal/intern/dir.h +segv.o: $(hdrdir)/ruby/internal/intern/enum.h +segv.o: $(hdrdir)/ruby/internal/intern/enumerator.h +segv.o: $(hdrdir)/ruby/internal/intern/error.h +segv.o: $(hdrdir)/ruby/internal/intern/eval.h +segv.o: $(hdrdir)/ruby/internal/intern/file.h +segv.o: $(hdrdir)/ruby/internal/intern/hash.h +segv.o: $(hdrdir)/ruby/internal/intern/io.h +segv.o: $(hdrdir)/ruby/internal/intern/load.h +segv.o: $(hdrdir)/ruby/internal/intern/marshal.h +segv.o: $(hdrdir)/ruby/internal/intern/numeric.h +segv.o: $(hdrdir)/ruby/internal/intern/object.h +segv.o: $(hdrdir)/ruby/internal/intern/parse.h +segv.o: $(hdrdir)/ruby/internal/intern/proc.h +segv.o: $(hdrdir)/ruby/internal/intern/process.h +segv.o: $(hdrdir)/ruby/internal/intern/random.h +segv.o: $(hdrdir)/ruby/internal/intern/range.h +segv.o: $(hdrdir)/ruby/internal/intern/rational.h +segv.o: $(hdrdir)/ruby/internal/intern/re.h +segv.o: $(hdrdir)/ruby/internal/intern/ruby.h +segv.o: $(hdrdir)/ruby/internal/intern/select.h +segv.o: $(hdrdir)/ruby/internal/intern/select/largesize.h +segv.o: $(hdrdir)/ruby/internal/intern/set.h +segv.o: $(hdrdir)/ruby/internal/intern/signal.h +segv.o: $(hdrdir)/ruby/internal/intern/sprintf.h +segv.o: $(hdrdir)/ruby/internal/intern/string.h +segv.o: $(hdrdir)/ruby/internal/intern/struct.h +segv.o: $(hdrdir)/ruby/internal/intern/thread.h +segv.o: $(hdrdir)/ruby/internal/intern/time.h +segv.o: $(hdrdir)/ruby/internal/intern/variable.h +segv.o: $(hdrdir)/ruby/internal/intern/vm.h +segv.o: $(hdrdir)/ruby/internal/interpreter.h +segv.o: $(hdrdir)/ruby/internal/iterator.h +segv.o: $(hdrdir)/ruby/internal/memory.h +segv.o: $(hdrdir)/ruby/internal/method.h +segv.o: $(hdrdir)/ruby/internal/module.h +segv.o: $(hdrdir)/ruby/internal/newobj.h +segv.o: $(hdrdir)/ruby/internal/scan_args.h +segv.o: $(hdrdir)/ruby/internal/special_consts.h +segv.o: $(hdrdir)/ruby/internal/static_assert.h +segv.o: $(hdrdir)/ruby/internal/stdalign.h +segv.o: $(hdrdir)/ruby/internal/stdbool.h +segv.o: $(hdrdir)/ruby/internal/stdckdint.h +segv.o: $(hdrdir)/ruby/internal/symbol.h +segv.o: $(hdrdir)/ruby/internal/value.h +segv.o: $(hdrdir)/ruby/internal/value_type.h +segv.o: $(hdrdir)/ruby/internal/variable.h +segv.o: $(hdrdir)/ruby/internal/warning_push.h +segv.o: $(hdrdir)/ruby/internal/xmalloc.h +segv.o: $(hdrdir)/ruby/missing.h +segv.o: $(hdrdir)/ruby/ruby.h +segv.o: $(hdrdir)/ruby/st.h +segv.o: $(hdrdir)/ruby/subst.h +segv.o: segv.c # AUTOGENERATED DEPENDENCIES END diff --git a/ext/-test-/fatal/segv.c b/ext/-test-/fatal/segv.c new file mode 100644 index 00000000000000..e381b4af4a5493 --- /dev/null +++ b/ext/-test-/fatal/segv.c @@ -0,0 +1,15 @@ +#include +#include + +static VALUE +bug_segv(VALUE self) +{ + raise(SIGSEGV); + return Qnil; /* never reached */ +} + +void +Init_segv(VALUE mBug) +{ + rb_define_singleton_method(mBug, "segv", bug_segv, 0); +} diff --git a/test/ruby/test_rubyoptions.rb b/test/ruby/test_rubyoptions.rb index 4a31f91b4a091c..d1b8ea67ec320e 100644 --- a/test/ruby/test_rubyoptions.rb +++ b/test/ruby/test_rubyoptions.rb @@ -814,7 +814,7 @@ module SEGVTest (?: --\sRuby\slevel\sbacktrace\sinformation\s----------------------------------------\n (?:-e:1:in\s\'(?:block\sin\s)?
\'\n)* - -e:1:in\s\'kill\'\n + -e:1:in\s\'segv\'\n \n )? )x, @@ -838,7 +838,10 @@ module SEGVTest )x, ] - KILL_SELF = "Process.kill :SEGV, $$" + # Crash the current thread with a thread-directed raise(SIGSEGV) so the + # signal is delivered synchronously on the calling thread. Requires + # -test-/fatal to be loaded. + KILL_SELF = "Bug.segv" end def assert_segv(args, message=nil, list: SEGVTest::ExpectedStderrList, **opt, &block) @@ -847,6 +850,7 @@ def assert_segv(args, message=nil, list: SEGVTest::ExpectedStderrList, **opt, &b env = Hash === args.first ? args.shift : {} args.unshift("--yjit") if JITSupport.yjit_enabled? args.unshift("--zjit") if JITSupport.zjit_enabled? + args.unshift("-r-test-/fatal") env.update({'RUBY_ON_BUG' => nil}) env['RUBY_CRASH_REPORT'] ||= nil # default to not passing down parent setting # ASAN registers a segv handler which prints out "AddressSanitizer: DEADLYSIGNAL" when From c4e1e1cad3549b2a57e1ca6e65b5f9238fb1eb75 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 16 Jul 2026 09:42:53 -0700 Subject: [PATCH 2/8] ZJIT: Re-profile whole ISEQ in non-final versions (#17915) Reset each existing instruction profile and reinstall zjit_* instructions after compiling a non-final version. Do the same for inlined ISEQs so materialized frames can collect fresh profiles. Let interpreter profiling instructions collect the samples and make recompile exits only observe whether the exiting instruction has finished profiling. --- zjit/bindgen/src/main.rs | 1 + zjit/src/codegen.rs | 5 ++++ zjit/src/codegen_tests.rs | 22 +++++++++++++++ zjit/src/cruby_bindings.inc.rs | 1 + zjit/src/hir.rs | 8 +++--- zjit/src/hir/opt_tests.rs | 49 +++++++++++++++++++--------------- zjit/src/profile.rs | 39 +++++++++------------------ 7 files changed, 74 insertions(+), 51 deletions(-) diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index 5892f61e39cedf..14dfea5c297a59 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -110,6 +110,7 @@ fn main() { .allowlist_function("rb_funcallv") .allowlist_function("rb_protect") .allowlist_function("rb_zjit_profile_disable") + .allowlist_function("rb_zjit_profile_enable") .allowlist_function("rb_zjit_insn_to_bare_insn") .allowlist_function("rb_zjit_hash_new_size") .allowlist_function("rb_zjit_class_allocate_instance_fastpath") diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index a20ce27af512dd..1293957d7e57e4 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -17,6 +17,7 @@ use crate::invariants::{ }; use crate::gc::append_gc_offsets; use crate::payload::{IseqCodePtrs, IseqStatus, IseqVersion, IseqVersionRef, JITFrame, get_or_create_iseq_payload}; +use crate::profile::reset_profiles_remaining; use crate::state::ZJITState; use 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_fallback_counter_for_optimized_method_type}; use crate::stats::{counter_ptr, with_time_stat, trace_compile_phase, Counter, Counter::{compile_time_ns, exit_compile_error}}; @@ -3421,6 +3422,10 @@ fn compile_iseq(iseq: IseqPtr) -> Result { trace_compile_phase("optimize", || function.optimize()); } function.dump_hir(); + let non_final_version = get_or_create_iseq_payload(iseq).versions.len() + 1 < max_iseq_versions(); + if non_final_version { + reset_profiles_remaining(iseq); + } Ok(function) } diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index 54ff7bd4f992e9..a0ba848f677bed 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -122,6 +122,28 @@ fn test_putobject() { "), @"1"); } +#[test] +fn test_recompile_exit_waits_for_interpreter_profiles() { + set_call_threshold(2); + eval(" + def recompile_profile_window(a, b) = a + b + recompile_profile_window(1, 2) + recompile_profile_window(1, 2) + "); + + let iseq = get_method_iseq("self", "recompile_profile_window"); + let num_profiles = get_option!(num_profiles); + for _ in 0..num_profiles { + eval("recompile_profile_window(1.5, 2.5)"); + } + let payload = get_or_create_iseq_payload(iseq); + assert!(!unsafe { payload.versions.last().unwrap().as_ref() }.is_invalidated()); + + eval("recompile_profile_window(1.5, 2.5)"); + let payload = get_or_create_iseq_payload(iseq); + assert!(unsafe { payload.versions.last().unwrap().as_ref() }.is_invalidated()); +} + #[test] fn test_dupstring() { eval(r##" diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 97f18434397f39..3a401fdef3bf03 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -2271,6 +2271,7 @@ unsafe extern "C" { pub fn rb_iseqw_to_iseq(iseqw: VALUE) -> *const rb_iseq_t; pub fn rb_iseq_label(iseq: *const rb_iseq_t) -> VALUE; pub fn rb_iseq_defined_string(type_: defined_type) -> VALUE; + pub fn rb_zjit_profile_enable(iseq: *const rb_iseq_t); pub fn rb_zjit_hash_new_size() -> usize; pub fn rb_zjit_class_allocate_instance_fastpath( klass: VALUE, diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 0a031aa9900c90..9c63a5fb0c4a45 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -6,9 +6,7 @@ #![allow(clippy::if_same_then_else)] #![allow(clippy::match_like_matches_macro)] use crate::{ - backend::lir::C_ARG_OPNDS, - cast::IntoUsize, codegen::{max_iseq_versions}, cruby::*, invariants::{self, iseq_seen_ep_escape}, payload::get_or_create_iseq_payload, options::{debug, get_option, DumpHIR, InlineDepth}, state::ZJITState, json::Json, - state, + backend::lir::C_ARG_OPNDS, cast::IntoUsize, codegen::max_iseq_versions, cruby::*, invariants::{self, iseq_seen_ep_escape}, json::Json, options::{DumpHIR, InlineDepth, debug, get_option}, payload::get_or_create_iseq_payload, profile::reset_profiles_remaining, state::{self, ZJITState}, }; use std::{ cell::RefCell, collections::{HashMap, HashSet, VecDeque}, ffi::{c_void, c_uint, c_int, CStr}, fmt::Display, mem::{align_of, size_of}, ptr, slice::Iter, @@ -9751,6 +9749,10 @@ fn add_iseq_to_hir( None => {}, } } + if matches!(mode, AddIseqMode::Inlined { .. }) { + // Materialized inlined frames also need fresh interpreter profiles. + reset_profiles_remaining(iseq); + } Ok(AddIseqResult { body_entry_block, profiles }) } diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 9c8113faeb8ab5..c9033d4154a4b4 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -5602,11 +5602,15 @@ mod hir_opt_tests { v36:NilClass = Const Value(nil) Jump bb8(v36, v13) bb8(v26:BasicObject, v27:BasicObject): - v39:BasicObject = Send v24, &block, :then, v26 # SendFallbackReason: Send: block argument is not nil + v56:NilClass = GuardBitEquals v26, Value(nil) recompile + PatchPoint MethodRedefined(Integer@0x1008, then@0x1010, cme:0x1018) + PushInlineFrame v24 (0x1040) + v76:BasicObject = InvokeBuiltin , v24 CheckInterrupts - Return v39 + PopInlineFrame + Return v76 bb4(v44:BasicObject, v45:Falsy, v46:BasicObject): - v50:StaticSymbol[:skip] = Const Value(VALUE(0x1008)) + v50:StaticSymbol[:skip] = Const Value(VALUE(0x1048)) CheckInterrupts Return v50 "); @@ -9175,12 +9179,17 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:HeapBasicObject, v10:BasicObject): v17:Fixnum[5] = Const Value(5) + v21:CBool = HasType v10, ObjectSubclass[class_exact:C] + CondBranch v21, bb5(), bb6() + bb5(): + v24:ObjectSubclass[class_exact:C] = RefineType v10, ObjectSubclass[class_exact:C] PatchPoint MethodRedefined(C@0x1008, foo=@0x1010, cme:0x1018) - v28:ObjectSubclass[class_exact:C] = GuardType v10, ObjectSubclass[class_exact:C] recompile - v30:CShape = LoadField v28, :shape_id@0x1040 - v31:CShape[0x1041] = GuardBitEquals v30, CShape(0x1041) - StoreField v28, :@foo@0x1042, v17 - WriteBarrier v28, v17 + SetIvar v24, :@foo, v17 + Jump bb4(v17) + bb6(): + v27:BasicObject = Send v10, :foo=, v17 # SendFallbackReason: Send: polymorphic fallback + Jump bb4(v27) + bb4(v20:BasicObject): CheckInterrupts Return v17 "); @@ -18809,27 +18818,25 @@ mod hir_opt_tests { bb4(): PatchPoint NoEPEscape(f) v44:Fixnum[1] = Const Value(1) - v48:CBool = HasType v12, Flonum + v48:CBool = HasType v12, Fixnum CondBranch v48, bb10(), bb11() bb10(): - v51:Flonum = RefineType v12, Flonum - PatchPoint MethodRedefined(Float@0x1008, +@0x1010, cme:0x1018) - v86:Float = FloatAdd v51, v44 + v51:Fixnum = RefineType v12, Fixnum + PatchPoint MethodRedefined(Integer@0x1008, +@0x1010, cme:0x1018) + v86:Fixnum = FixnumAdd v51, v44 Jump bb9(v86) bb11(): - v54:CBool = HasType v12, Fixnum + v54:CBool = HasType v12, Flonum CondBranch v54, bb12(), bb13() bb12(): - v57:Fixnum = RefineType v12, Fixnum - PatchPoint MethodRedefined(Integer@0x1040, +@0x1010, cme:0x1048) - v89:Fixnum = FixnumAdd v57, v44 + v57:Flonum = RefineType v12, Flonum + PatchPoint MethodRedefined(Float@0x1040, +@0x1010, cme:0x1048) + v89:Float = FloatAdd v57, v44 Jump bb9(v89) bb13(): - PatchPoint MethodRedefined(Float@0x1008, +@0x1010, cme:0x1018) - v92:Flonum = GuardType v12, Flonum recompile - v93:Float = FloatAdd v92, v44 - Jump bb9(v93) - bb9(v47:Float|Fixnum): + v60:BasicObject = Send v12, :+, v44 # SendFallbackReason: Send: polymorphic fallback + Jump bb9(v60) + bb9(v47:BasicObject): PatchPoint SingleRactorMode v69:CShape = LoadField v11, :shape_id@0x1001 v70:CShape[0x1002] = Const CShape(0x1002) diff --git a/zjit/src/profile.rs b/zjit/src/profile.rs index edf93dffa0b834..a8df3b6f1e5f54 100644 --- a/zjit/src/profile.rs +++ b/zjit/src/profile.rs @@ -122,36 +122,21 @@ fn profile_insn(bare_opcode: ruby_vminsn_type, ec: EcPtr) { } } -/// Profile the instruction at the current CFP for a recompile side exit. +/// Return whether the interpreter finished profiling the current instruction. pub fn profile_recompile_insn(ec: EcPtr) -> bool { - let profiler = &mut Profiler::new(ec); - let pc = unsafe { get_cfp_pc(profiler.cfp) }; - let bare_opcode = unsafe { - rb_zjit_insn_to_bare_insn(rb_iseq_opcode_at_pc(profiler.iseq, pc)) - } as ruby_vminsn_type; - let profile = &mut get_or_create_iseq_payload(profiler.iseq).profile; - - let is_send = matches!(bare_opcode, YARVINSN_send | YARVINSN_opt_send_without_block); - // For now, send recompile exits only fill in missing profiles. Once the send site - // has finished profiling, don't recompile it on later exits. - if is_send && profile.done_profiling_at(profiler.insn_idx) { - return false; - } - // For now, non-send recompile exits reset the profiling counter before requesting recompilation - // so that we can collect enough samples. - if !is_send && profile.done_profiling_at(profiler.insn_idx) { - profile.entry_mut(profiler.insn_idx) - .set_profiles_remaining(get_option!(num_profiles)); - } + let profiler = &Profiler::new(ec); + get_or_create_iseq_payload(profiler.iseq).profile.done_profiling_at(profiler.insn_idx) +} - // If this opcode can't be sampled here, this exit has no profile data to collect. - if !profile_insn_sample(bare_opcode, profiler, profile) { - return false; +/// Reset existing profile counters and install profiling instructions throughout an ISEQ. +/// Newly reached instructions initialize their counters from the same option. +pub(crate) fn reset_profiles_remaining(iseq: IseqPtr) { + let profile = &mut get_or_create_iseq_payload(iseq).profile; + let num_profiles = get_option!(num_profiles); + for entry in &mut profile.entries { + entry.profiles_remaining = num_profiles; } - - let entry = profile.entry_mut(profiler.insn_idx); - entry.profiles_remaining = entry.profiles_remaining.saturating_sub(1); - entry.profiles_remaining == 0 + unsafe { rb_zjit_profile_enable(iseq) }; } /// Return the argc as stated in the calldata plus: From 2fd3c2edea597c4e745f88f3813cdab5b70cceb3 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Thu, 16 Jul 2026 12:07:54 -0400 Subject: [PATCH 3/8] [PRISM] Ensure not writing beyond buffer in stream --- prism_compile.c | 69 ++++++++++++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/prism_compile.c b/prism_compile.c index d1ed2cbb339cb6..5746bc38c9a376 100644 --- a/prism_compile.c +++ b/prism_compile.c @@ -11630,31 +11630,48 @@ pm_parse_string(pm_parse_result_t *result, VALUE source, VALUE filepath, VALUE * return error; } -struct rb_stdin_wrapper { - VALUE rb_stdin; - int eof_seen; -}; +VALUE rb_io_gets_limit_internal(VALUE io, long limit); +/** + * Report whether the stream has reached EOF. The stream reader uses this to + * tell a line that is longer than the read buffer (keep reading) apart from the + * final line of input with no trailing newline (stop). We ask the IO directly + * rather than inferring EOF from a short read, which is unreliable once we + * request fewer bytes than the buffer can hold (see pm_parse_stdin_fgets). + */ static int pm_parse_stdin_eof(void *stream) { - struct rb_stdin_wrapper * wrapped_stdin = (struct rb_stdin_wrapper *)stream; - return wrapped_stdin->eof_seen; + return RTEST(rb_io_eof((VALUE) stream)); } -VALUE rb_io_gets_limit_internal(VALUE io, long limit); +/** + * The largest number of bytes a single character can occupy in any encoding + * that Ruby supports (CESU-8). Ruby's line reader treats a `gets` limit as a + * soft limit: to avoid splitting a multi-byte character it may return up to + * `MAX_ENC_LEN - 1` more bytes than requested. Reserving `MAX_ENC_LEN` bytes of + * headroom therefore leaves room for both that overshoot and the terminating + * NUL byte. + */ +#define MAX_ENC_LEN 6 /** - * An implementation of fgets that is suitable for use with Ruby IO objects. + * An implementation of fgets that is suitable for use with Ruby IO objects. As + * required by pm_source_stream_fgets_t, this never writes more than `size` + * bytes into `string` (including the terminating NUL byte). */ static char * pm_parse_stdin_fgets(char *string, int size, void *stream) { - RUBY_ASSERT(size > 0); - - struct rb_stdin_wrapper * wrapped_stdin = (struct rb_stdin_wrapper *)stream; + RUBY_ASSERT(size > MAX_ENC_LEN); - VALUE line = rb_io_gets_limit_internal(wrapped_stdin->rb_stdin, size - 1); + /* + * Request fewer bytes than the buffer can hold. Ruby's line reader may + * return more bytes than requested when the limit falls in the middle of a + * multi-byte character, and the reserved headroom guarantees the result + * still fits. + */ + VALUE line = rb_io_gets_limit_internal((VALUE) stream, size - MAX_ENC_LEN); if (NIL_P(line)) { return NULL; } @@ -11662,19 +11679,24 @@ pm_parse_stdin_fgets(char *string, int size, void *stream) const char *cstr = RSTRING_PTR(line); long length = RSTRING_LEN(line); - memcpy(string, cstr, length); - string[length] = '\0'; - - // We're reading strings from stdin via gets. We'll assume that if the - // string is smaller than the requested length, and doesn't end with a - // newline, that we hit EOF. - if (length < (size - 1) && string[length - 1] != '\n') { - wrapped_stdin->eof_seen = 1; + /* + * Defensively clamp the copy. The line reader relaxes the limit to avoid + * splitting a multi-byte character, so the returned string can be longer + * than requested; we must never write past the caller's buffer. One byte + * is reserved for the NUL terminator. + */ + if (length > (long) (size - 1)) { + length = (long) (size - 1); } + memcpy(string, cstr, (size_t) length); + string[length] = '\0'; + return string; } +#undef MAX_ENC_LEN + // We need access to this function when we're done parsing stdin. void rb_reset_argf_lineno(long n); @@ -11688,12 +11710,7 @@ pm_parse_stdin(pm_parse_result_t *result) { pm_options_frozen_string_literal_init(result->options); - struct rb_stdin_wrapper wrapped_stdin = { - rb_stdin, - 0 - }; - - result->source = pm_source_stream_new((void *) &wrapped_stdin, pm_parse_stdin_fgets, pm_parse_stdin_eof); + result->source = pm_source_stream_new((void *) rb_stdin, pm_parse_stdin_fgets, pm_parse_stdin_eof); pm_node_t *node = pm_parse_stream(&result->parser, result->arena, result->source, result->options); // When we're done parsing, we reset $. because we don't want the fact that From 61441f2d7242148cc45efdf6f3ebce4c43eaf7c6 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 16 Jul 2026 10:46:57 -0700 Subject: [PATCH 4/8] ZJIT: Add counters for throw and exception_handler (#17902) --- zjit/src/codegen.rs | 24 +++++++++++++++++++++--- zjit/src/hir.rs | 1 + zjit/src/stats.rs | 12 +++--------- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 1293957d7e57e4..a04061f06d6f60 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -213,7 +213,7 @@ pub extern "C" fn rb_zjit_iseq_gen_entry_point(iseq: IseqPtr, ec: EcPtr, jit_exc fn gen_iseq_entry_point(cb: &mut CodeBlock, iseq: IseqPtr, jit_exception: bool) -> Result { // We don't support exception handlers yet if jit_exception { - return Err(CompileError::ExceptionHandler); + return gen_exception_handler_counter(cb); } let iseq_name = iseq_get_location(iseq, 0); @@ -525,7 +525,6 @@ fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, version: IseqVersionRef, func debug!("ZJIT: gen_function: Failed to compile insn: {insn_id} {insn}. Generating side-exit."); gen_incr_counter(&mut asm, exit_counter_for_unhandled_hir_insn(&insn)); let reason = match insn { - Insn::Throw { .. } => SideExitReason::UnhandledHIRThrow, Insn::InvokeBuiltin { .. } => SideExitReason::UnhandledHIRInvokeBuiltin, _ => SideExitReason::UnhandledHIRUnknown(insn_id), }; @@ -784,7 +783,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio &Insn::IsA { val, class } => gen_is_a(jit, asm, opnd!(val), opnd!(class)), &Insn::ArrayMax { ref elements, state } => gen_array_max(jit, asm, function, opnds!(elements), &function.frame_state(state)), &Insn::ArrayMin { ref elements, state } => gen_array_min(jit, asm, function, opnds!(elements), &function.frame_state(state)), - &Insn::Throw { state, .. } => return Err(state), + &Insn::Throw { state, .. } => no_output!(gen_throw(jit, asm, function, &function.frame_state(state))), &Insn::CondBranch { .. } | &Insn::Jump { .. } | Insn::Entries { .. } => unreachable!(), }; @@ -2582,6 +2581,12 @@ fn gen_return(asm: &mut Assembler, val: lir::Opnd) { asm.cret(C_RET_OPND); } +fn gen_throw(jit: &mut JITState, asm: &mut Assembler, function: &Function, state: &FrameState) { + // TODO: Consider calling rb_vm_throw and propagating ec->tag->state to the interpreter. + // Also consider making it a jump on method inlining. + gen_side_exit(jit, asm, function, &SideExitReason::Throw, None, state); +} + /// Compile Fixnum + Fixnum fn gen_fixnum_add(jit: &mut JITState, asm: &mut Assembler, function: &Function, left: lir::Opnd, right: lir::Opnd, state: &FrameState) -> lir::Opnd { // Add left + right and test for overflow @@ -3971,6 +3976,19 @@ fn gen_compile_error_counter(cb: &mut CodeBlock, compile_error: &CompileError) - }) } +/// Generate a JIT entry that just increments exit_exception_handler and exits +fn gen_exception_handler_counter(cb: &mut CodeBlock) -> Result { + let mut asm = Assembler::new(); + asm.new_block_without_id("exception_handler_counter"); + gen_incr_counter(&mut asm, Counter::exit_exception_handler); + asm.cret(Qundef.into()); + + asm.compile(cb).map(|(code_ptr, gc_offsets)| { + assert_eq!(0, gc_offsets.len()); + code_ptr + }) +} + /// Given the number of spill slots needed for a function, return the number of bytes /// the function needs to allocate on the stack for the stack frame. fn aligned_stack_bytes(num_slots: usize) -> usize { diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 9c63a5fb0c4a45..ba35d3e5e7e06b 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -553,6 +553,7 @@ pub enum SideExitReason { PatchPoint(Invariant), CalleeSideExit, Interrupt, + Throw, BlockParamProxyNotIseqOrIfunc, BlockParamProxyNotNil, BlockParamProxyNotProc, diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 6a05e5f50788f9..5f964a2734e59e 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -185,6 +185,7 @@ make_counters! { exit { // exit_: Side exits reasons exit_compile_error, + exit_exception_handler, exit_unhandled_newarray_send_min, exit_unhandled_newarray_send_hash, exit_unhandled_newarray_send_pack, @@ -226,6 +227,7 @@ make_counters! { exit_patchpoint_root_box_only, exit_callee_side_exit, exit_interrupt, + exit_throw, exit_stackoverflow, exit_block_param_proxy_not_iseq_or_ifunc, exit_block_param_proxy_not_nil, @@ -349,7 +351,6 @@ make_counters! { compile_error_iseq_version_limit_reached, compile_error_iseq_stack_too_large, compile_error_native_stack_too_large, - compile_error_exception_handler, compile_error_out_of_memory, compile_error_label_linking_failure, compile_error_jit_to_jit_optional, @@ -369,9 +370,6 @@ make_counters! { compile_error_validation_misc_validation_error, // unhandled_hir_insn_: Unhandled HIR instructions - unhandled_hir_insn_array_max, - unhandled_hir_insn_fixnum_div, - unhandled_hir_insn_throw, unhandled_hir_insn_invokebuiltin, unhandled_hir_insn_unknown, @@ -523,7 +521,6 @@ pub enum CompileError { IseqVersionLimitReached, IseqStackTooLarge, NativeStackTooLarge, - ExceptionHandler, OutOfMemory, ParseError(ParseError), /// When a ZJIT function is too large, the branches may have @@ -542,7 +539,6 @@ pub fn exit_counter_for_compile_error(compile_error: &CompileError) -> Counter { IseqVersionLimitReached => compile_error_iseq_version_limit_reached, IseqStackTooLarge => compile_error_iseq_stack_too_large, NativeStackTooLarge => compile_error_native_stack_too_large, - ExceptionHandler => compile_error_exception_handler, OutOfMemory => compile_error_out_of_memory, LabelLinkingFailure => compile_error_label_linking_failure, ParseError(parse_error) => match parse_error { @@ -568,9 +564,6 @@ pub fn exit_counter_for_unhandled_hir_insn(insn: &crate::hir::Insn) -> Counter { use crate::hir::Insn::*; use crate::stats::Counter::*; match insn { - ArrayMax { .. } => unhandled_hir_insn_array_max, - FixnumDiv { .. } => unhandled_hir_insn_fixnum_div, - Throw { .. } => unhandled_hir_insn_throw, InvokeBuiltin { .. } => unhandled_hir_insn_invokebuiltin, _ => unhandled_hir_insn_unknown, } @@ -617,6 +610,7 @@ pub fn side_exit_counter(reason: crate::hir::SideExitReason) -> Counter { GuardSuperMethodEntry => exit_guard_super_method_entry, CalleeSideExit => exit_callee_side_exit, Interrupt => exit_interrupt, + Throw => exit_throw, StackOverflow => exit_stackoverflow, BlockParamProxyNotIseqOrIfunc => exit_block_param_proxy_not_iseq_or_ifunc, BlockParamProxyNotNil => exit_block_param_proxy_not_nil, From d5970a189a8c40c49a152d5905df6e05fe25dd0d Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Thu, 16 Jul 2026 10:56:01 -0700 Subject: [PATCH 5/8] ZJIT: Trace fallbacks in Perfetto (#17896) This lets us see stack traces for send, ivar fallbacks. --- zjit/src/codegen.rs | 26 ++++++++++++++++++++++++++ zjit/src/hir.rs | 8 ++------ zjit/src/hir/opt_tests.rs | 22 +++++++++++----------- zjit/src/options.rs | 19 +++++++++++++++++++ zjit/src/state.rs | 37 ++++++++++++++++++++++++++++++++++++- 5 files changed, 94 insertions(+), 18 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index a04061f06d6f60..d2dd2eb16971e3 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -1185,6 +1185,7 @@ fn gen_ccall_variadic( /// Emit an uncached instance variable lookup fn gen_getivar(asm: &mut Assembler, recv: Opnd, id: ID, ic: *const iseq_inline_iv_cache_entry, state: &FrameState) -> Opnd { + gen_trace_fallback(asm, "getivar"); if ic.is_null() { asm_ccall!(asm, rb_ivar_get, recv, id.0.into()) } else { @@ -1195,6 +1196,7 @@ fn gen_getivar(asm: &mut Assembler, recv: Opnd, id: ID, ic: *const iseq_inline_i /// Emit an uncached instance variable store fn gen_setivar(jit: &mut JITState, asm: &mut Assembler, function: &Function, recv: Opnd, id: ID, ic: *const iseq_inline_iv_cache_entry, val: Opnd, state: &FrameState) { + gen_trace_fallback(asm, "setivar"); // Setting an ivar can raise FrozenError, so we need proper frame state for exception handling. gen_prepare_non_leaf_call(jit, asm, function, state); if ic.is_null() { @@ -1467,6 +1469,24 @@ fn gen_param(asm: &mut Assembler, _idx: usize) -> lir::Opnd { vreg } +fn gen_trace_fallback(asm: &mut Assembler, reason: &str) { + if !get_option!(trace_fallbacks) { + return; + } + let reason_cstr = std::ffi::CString::new(reason.to_string()) + .unwrap_or_else(|_| std::ffi::CString::new("unknown").unwrap()); + let reason_ptr = reason_cstr.into_raw() as *const u8; + use crate::state::rb_zjit_record_fallback_stack; + asm_ccall!(asm, rb_zjit_record_fallback_stack, Opnd::const_ptr(reason_ptr)); +} + +fn gen_trace_send_fallback(asm: &mut Assembler, reason: &SendFallbackReason) { + if !get_option!(trace_fallbacks) { + return; + } + gen_trace_fallback(asm, &format!("{reason}")); +} + /// Compile a dynamic dispatch with block fn gen_send( jit: &mut JITState, @@ -1478,6 +1498,7 @@ fn gen_send( reason: SendFallbackReason, ) -> lir::Opnd { gen_incr_send_fallback_counter(asm, reason); + gen_trace_send_fallback(asm, &reason); gen_prepare_fallback_call(jit, asm, function, state); asm_comment!(asm, "call #{} with dynamic dispatch", ruby_call_method_name(cd)); @@ -1502,6 +1523,7 @@ fn gen_send_forward( reason: SendFallbackReason, ) -> lir::Opnd { gen_incr_send_fallback_counter(asm, reason); + gen_trace_send_fallback(asm, &reason); gen_prepare_fallback_call(jit, asm, function, state); @@ -1526,6 +1548,7 @@ fn gen_send_without_block( reason: SendFallbackReason, ) -> lir::Opnd { gen_incr_send_fallback_counter(asm, reason); + gen_trace_send_fallback(asm, &reason); gen_prepare_fallback_call(jit, asm, function, state); asm_comment!(asm, "call #{} with dynamic dispatch", ruby_call_method_name(cd)); @@ -1820,6 +1843,7 @@ fn gen_invokeblock( reason: SendFallbackReason, ) -> lir::Opnd { gen_incr_send_fallback_counter(asm, reason); + gen_trace_send_fallback(asm, &reason); gen_prepare_fallback_call(jit, asm, function, state); @@ -1987,6 +2011,7 @@ fn gen_invokesuper( reason: SendFallbackReason, ) -> lir::Opnd { gen_incr_send_fallback_counter(asm, reason); + gen_trace_send_fallback(asm, &reason); gen_prepare_fallback_call(jit, asm, function, state); asm_comment!(asm, "call super with dynamic dispatch"); @@ -2011,6 +2036,7 @@ fn gen_invokesuperforward( reason: SendFallbackReason, ) -> lir::Opnd { gen_incr_send_fallback_counter(asm, reason); + gen_trace_send_fallback(asm, &reason); gen_prepare_fallback_call(jit, asm, function, state); asm_comment!(asm, "call super with dynamic dispatch (forwarding)"); diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index ba35d3e5e7e06b..9e0d07e71f9812 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -4072,16 +4072,12 @@ impl Function { | ReceiverTypeResolution::SkewedPolymorphic { profiled_type } => (profiled_type.class(), Some(profiled_type)), ReceiverTypeResolution::SkewedMegamorphic { .. } | ReceiverTypeResolution::Megamorphic => { - if get_option!(stats) { - self.set_dynamic_send_reason(insn_id, SendMegamorphic); - } + self.set_dynamic_send_reason(insn_id, SendMegamorphic); self.push_insn_id(block, insn_id); continue; } ReceiverTypeResolution::Polymorphic => { - if get_option!(stats) { - self.set_dynamic_send_reason(insn_id, SendPolymorphic); - } + self.set_dynamic_send_reason(insn_id, SendPolymorphic); self.push_insn_id(block, insn_id); continue; } diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index c9033d4154a4b4..0cee95a35629e2 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -8446,7 +8446,7 @@ mod hir_opt_tests { v60:TrueClass = Const Value(true) Jump bb7(v60) bb11(): - v48:BasicObject = Send v31, :! # SendFallbackReason: Send: polymorphic fallback + v48:BasicObject = Send v31, :! # SendFallbackReason: Send: polymorphic call site Jump bb7(v48) bb7(v35:BasicObject): CheckInterrupts @@ -9187,7 +9187,7 @@ mod hir_opt_tests { SetIvar v24, :@foo, v17 Jump bb4(v17) bb6(): - v27:BasicObject = Send v10, :foo=, v17 # SendFallbackReason: Send: polymorphic fallback + v27:BasicObject = Send v10, :foo=, v17 # SendFallbackReason: Send: polymorphic call site Jump bb4(v27) bb4(v20:BasicObject): CheckInterrupts @@ -9770,7 +9770,7 @@ mod hir_opt_tests { v31:BasicObject = GetIvar v19, :@foo Jump bb4(v31) bb6(): - v22:BasicObject = Send v10, :foo # SendFallbackReason: Send: polymorphic fallback + v22:BasicObject = Send v10, :foo # SendFallbackReason: Send: polymorphic call site Jump bb4(v22) bb4(v15:BasicObject): CheckInterrupts @@ -16728,7 +16728,7 @@ mod hir_opt_tests { v45:Fixnum[4] = Const Value(4) Jump bb4(v45) bb8(): - v28:BasicObject = Send v10, :foo # SendFallbackReason: Send: polymorphic fallback + v28:BasicObject = Send v10, :foo # SendFallbackReason: Send: polymorphic call site Jump bb4(v28) bb4(v15:BasicObject): v31:Fixnum[2] = Const Value(2) @@ -16781,7 +16781,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Integer@0x1040, itself@0x1010, cme:0x1018) Jump bb4(v25) bb8(): - v28:BasicObject = Send v10, :itself # SendFallbackReason: Send: polymorphic fallback + v28:BasicObject = Send v10, :itself # SendFallbackReason: Send: polymorphic call site Jump bb4(v28) bb4(v15:BasicObject): CheckInterrupts @@ -16837,7 +16837,7 @@ mod hir_opt_tests { v40:StringExact = CCallVariadic v25, :Integer#to_s@0x1040 Jump bb4(v40) bb8(): - v28:BasicObject = Send v10, :to_s # SendFallbackReason: Send: polymorphic fallback + v28:BasicObject = Send v10, :to_s # SendFallbackReason: Send: polymorphic call site Jump bb4(v28) bb4(v15:BasicObject): CheckInterrupts @@ -16890,7 +16890,7 @@ mod hir_opt_tests { v40:BasicObject = CCallWithFrame v25, :Float#to_s@0x1040 Jump bb4(v40) bb8(): - v28:BasicObject = Send v10, :to_s # SendFallbackReason: Send: polymorphic fallback + v28:BasicObject = Send v10, :to_s # SendFallbackReason: Send: polymorphic call site Jump bb4(v28) bb4(v15:BasicObject): CheckInterrupts @@ -16943,7 +16943,7 @@ mod hir_opt_tests { v38:StringExact = InvokeBuiltin leaf , v25 Jump bb4(v38) bb8(): - v28:BasicObject = Send v10, :to_s # SendFallbackReason: Send: polymorphic fallback + v28:BasicObject = Send v10, :to_s # SendFallbackReason: Send: polymorphic call site Jump bb4(v28) bb4(v15:BasicObject): CheckInterrupts @@ -16992,7 +16992,7 @@ mod hir_opt_tests { v31:Fixnum[3] = Const Value(3) Jump bb4(v31) bb6(): - v22:BasicObject = Send v10, :foo # SendFallbackReason: Send: polymorphic fallback + v22:BasicObject = Send v10, :foo # SendFallbackReason: Send: polymorphic call site Jump bb4(v22) bb4(v15:BasicObject): CheckInterrupts @@ -18510,7 +18510,7 @@ mod hir_opt_tests { v45:BasicObject = CCallWithFrame v30, :Float#*@0x1040, v13 Jump bb4(v45) bb8(): - v33:BasicObject = Send v12, :*, v13 # SendFallbackReason: Send: polymorphic fallback + v33:BasicObject = Send v12, :*, v13 # SendFallbackReason: Send: polymorphic call site Jump bb4(v33) bb4(v20:BasicObject): CheckInterrupts @@ -18834,7 +18834,7 @@ mod hir_opt_tests { v89:Float = FloatAdd v57, v44 Jump bb9(v89) bb13(): - v60:BasicObject = Send v12, :+, v44 # SendFallbackReason: Send: polymorphic fallback + v60:BasicObject = Send v12, :+, v44 # SendFallbackReason: Send: polymorphic call site Jump bb9(v60) bb9(v47:BasicObject): PatchPoint SingleRactorMode diff --git a/zjit/src/options.rs b/zjit/src/options.rs index bffecaeb99b0fe..c0cd8c4aabc7f3 100644 --- a/zjit/src/options.rs +++ b/zjit/src/options.rs @@ -123,6 +123,12 @@ pub struct Options { /// Frequency of tracing side exits. pub trace_side_exits_sample_interval: usize, + /// Trace and write fallback source maps to /tmp for stackprof. + pub trace_fallbacks: bool, + + /// Frequency of tracing fallbacks. + pub trace_fallbacks_sample_interval: usize, + /// Trace compilation phases as Perfetto duration events. pub trace_compiles: bool, @@ -203,6 +209,8 @@ impl Default for Options { dump_disasm: None, trace_side_exits: None, trace_side_exits_sample_interval: 0, + trace_fallbacks: false, + trace_fallbacks_sample_interval: 0, trace_compiles: false, trace_invalidation: false, perf: None, @@ -503,6 +511,17 @@ fn parse_option(str_ptr: *const std::os::raw::c_char) -> Option<()> { options.trace_side_exits_sample_interval = sample_interval.parse::().ok()?; } + ("trace-fallbacks", "") => { + options.trace_fallbacks = true; + } + + ("trace-fallbacks-sample-rate", sample_interval) => { + // If not already set, then set it. + options.trace_fallbacks = true; + // `sample_interval ` must provide a string that can be validly parsed to a `usize`. + options.trace_fallbacks_sample_interval = sample_interval.parse::().ok()?; + } + ("trace-compiles", "") => options.trace_compiles = true, ("trace-invalidation", "") => options.trace_invalidation = true, diff --git a/zjit/src/state.rs b/zjit/src/state.rs index 18b6157f91eae7..183b3c206ee813 100644 --- a/zjit/src/state.rs +++ b/zjit/src/state.rs @@ -132,7 +132,7 @@ impl ZJITState { let materialize_exit_trampoline = gen_materialize_exit_trampoline(&mut cb, exit_trampoline).unwrap(); let function_stub_hit_trampoline = gen_function_stub_hit_trampoline(&mut cb).unwrap(); - let perfetto_tracer = if get_option!(trace_side_exits).is_some() || get_option!(trace_compiles) || get_option!(trace_invalidation) { + let perfetto_tracer = if get_option!(trace_side_exits).is_some() || get_option!(trace_compiles) || get_option!(trace_invalidation) || get_option!(trace_fallbacks) { Some(PerfettoTracer::new()) } else { None @@ -496,6 +496,41 @@ pub extern "C" fn rb_zjit_record_exit_stack(reason: *const std::ffi::c_char) { tracer.write_event("side_exit", reason_str, &frames); } +/// Record a backtrace with ZJIT fallbacks as a Perfetto trace event +#[unsafe(no_mangle)] +pub extern "C" fn rb_zjit_record_fallback_stack(reason: *const std::ffi::c_char) { + if !zjit_enabled_p() || !get_option!(trace_fallbacks) { + return; + } + + let tracer = match ZJITState::get_tracer() { + Some(t) => t, + None => return, + }; + + // When `trace_fallbacks_sample_interval` is non-zero, apply sampling. + if get_option!(trace_fallbacks_sample_interval) != 0 { + if tracer.skipped_samples < get_option!(trace_fallbacks_sample_interval) { + tracer.skipped_samples += 1; + return; + } else { + tracer.skipped_samples = 0; + } + } + + // Collect profile frames + let frames = capture_ruby_frames(); + + // Get the reason string + let reason_str = if reason.is_null() { + "unknown" + } else { + unsafe { std::ffi::CStr::from_ptr(reason).to_str().unwrap_or("unknown") } + }; + + tracer.write_event("fallback", reason_str, &frames); +} + /// Wrap a closure in a Perfetto duration event with category "invalidation" /// and a Ruby backtrace captured on the begin event. pub fn trace_invalidation(reason_func: RF, func: F) -> R From bd62a1fa1899317e17f819f663e165be68992a38 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 8 Jul 2026 10:29:12 -0400 Subject: [PATCH 6/8] ZJIT: Loop, not bail, for large FrameSetup Sometimes we have too many stack slots to fit in one sub operand on ARM. That's fine. We can iteratively subtract instead of doing it all in one instruction. --- zjit/src/asm/arm64/arg/shifted_imm.rs | 8 ++- zjit/src/backend/arm64/mod.rs | 75 ++++++++++++++++++++++++--- 2 files changed, 73 insertions(+), 10 deletions(-) diff --git a/zjit/src/asm/arm64/arg/shifted_imm.rs b/zjit/src/asm/arm64/arg/shifted_imm.rs index 06daefdef7715d..9befe3a3041a09 100644 --- a/zjit/src/asm/arm64/arg/shifted_imm.rs +++ b/zjit/src/asm/arm64/arg/shifted_imm.rs @@ -13,16 +13,20 @@ pub struct ShiftedImmediate { value: u16 } +impl ShiftedImmediate { + pub const MAX: u64 = 2_u64.pow(12) - 1; +} + impl TryFrom for ShiftedImmediate { type Error = (); fn try_from(value: u64) -> Result { let current = value; - if current < 2_u64.pow(12) { + if current <= Self::MAX { return Ok(ShiftedImmediate { shift: Shift::LSL0, value: current as u16 }); } - if (current & (2_u64.pow(12) - 1) == 0) && ((current >> 12) < 2_u64.pow(12)) { + if (current & Self::MAX == 0) && ((current >> 12) <= Self::MAX) { return Ok(ShiftedImmediate { shift: Shift::LSL12, value: (current >> 12) as u16 }); } diff --git a/zjit/src/backend/arm64/mod.rs b/zjit/src/backend/arm64/mod.rs index a91d0e8b375896..30bd46fe142dd1 100644 --- a/zjit/src/backend/arm64/mod.rs +++ b/zjit/src/backend/arm64/mod.rs @@ -1164,12 +1164,18 @@ impl Assembler { slot_count += 1 } if slot_count > 0 { - let slot_offset = (slot_count * SIZEOF_VALUE) as u64; - // Bail when asked to reserve too many slots in one instruction. - if ShiftedImmediate::try_from(slot_offset).is_err() { - return Err(CompileError::NativeStackTooLarge); + let mut slot_offset = (slot_count * SIZEOF_VALUE) as u64; + // Loop when asked to reserve too many slots in one instruction. + // TODO(max): Use a scratch reg instead of iterated subtraction + while slot_offset > 0 { + let reserve_step = if ShiftedImmediate::try_from(slot_offset).is_ok() { + slot_offset + } else { + ShiftedImmediate::MAX + }; + sub(cb, C_SP_REG, C_SP_REG, A64Opnd::new_uimm(reserve_step)); + slot_offset = slot_offset.saturating_sub(reserve_step); } - sub(cb, C_SP_REG, C_SP_REG, A64Opnd::new_uimm(slot_offset)); } } Insn::FrameTeardown { preserved } => { @@ -1646,9 +1652,6 @@ impl Assembler { asm.stack_state.num_spill_slots = num_stack_slots; asm.stack_state.num_side_exit_stack_map_slots = asm.side_exit_stack_map_slots(&assignments); let stack_slot_count = asm.stack_state.stack_slot_count(); - if stack_slot_count > Self::MAX_FRAME_STACK_SLOTS { - return Err(CompileError::NativeStackTooLarge); - } // Dump vreg-to-physical-register mapping if requested if let Some(crate::options::Options { dump_lir: Some(dump_lirs), .. }) = unsafe { crate::options::OPTIONS.as_ref() } { @@ -2951,4 +2954,60 @@ mod tests { "); assert_snapshot!(cb.hexdump(), @"00000891000040f8"); } + + #[test] + fn test_frame_setup() { + let (mut asm, mut cb) = setup_asm(); + + asm.stack_state.stack_base_idx = 16; + let _ = asm.frame_setup(&[]); + asm.compile(&mut cb).unwrap(); + + assert_disasm_snapshot!(cb.disasm(), @" + 0x0: stp x29, x30, [sp, #-0x10]! + 0x4: mov x29, sp + 0x8: sub sp, sp, #0x80 + "); + assert_snapshot!(cb.hexdump(), @"fd7bbfa9fd030091ff0302d1"); + } + + #[test] + fn test_frame_setup_with_large_frame_fits_in_immediate() { + let (mut asm, mut cb) = setup_asm(); + + asm.stack_state.stack_base_idx = ShiftedImmediate::MAX.to_usize(); + let _ = asm.frame_setup(&[]); + asm.compile(&mut cb).unwrap(); + + assert_disasm_snapshot!(cb.disasm(), @" + 0x0: stp x29, x30, [sp, #-0x10]! + 0x4: mov x29, sp + 0x8: sub sp, sp, #8, lsl #12 + "); + assert_snapshot!(cb.hexdump(), @"fd7bbfa9fd030091ff2340d1"); + } + + #[test] + fn test_frame_setup_with_large_frame_too_big_for_immediate() { + let (mut asm, mut cb) = setup_asm(); + + asm.stack_state.stack_base_idx = (ShiftedImmediate::MAX + 8).to_usize(); + let _ = asm.frame_setup(&[]); + asm.compile(&mut cb).unwrap(); + + assert_disasm_snapshot!(cb.disasm(), @" + 0x0: stp x29, x30, [sp, #-0x10]! + 0x4: mov x29, sp + 0x8: sub sp, sp, #0xfff + 0xc: sub sp, sp, #0xfff + 0x10: sub sp, sp, #0xfff + 0x14: sub sp, sp, #0xfff + 0x18: sub sp, sp, #0xfff + 0x1c: sub sp, sp, #0xfff + 0x20: sub sp, sp, #0xfff + 0x24: sub sp, sp, #0xfff + 0x28: sub sp, sp, #0x48 + "); + assert_snapshot!(cb.hexdump(), @"fd7bbfa9fd030091ffff3fd1ffff3fd1ffff3fd1ffff3fd1ffff3fd1ffff3fd1ffff3fd1ffff3fd1ff2301d1"); + } } From 5e92b4b6d2e64797540d45c703a1121dede44a78 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 8 Jul 2026 10:51:58 -0400 Subject: [PATCH 7/8] ZJIT: Measure total amount of stack space reserved This is useful for seeing general impact of optimizations. For example, upcoming SSA minimization. --- zjit.rb | 1 + zjit/src/backend/arm64/mod.rs | 3 +++ zjit/src/stats.rs | 2 ++ 3 files changed, 6 insertions(+) diff --git a/zjit.rb b/zjit.rb index 30ad7f51b75387..4f8dabc4aaed4d 100644 --- a/zjit.rb +++ b/zjit.rb @@ -182,6 +182,7 @@ def stats_string :side_exit_size_ratio, :zjit_alloc_bytes, :total_mem_bytes, + :total_native_stack_bytes, :side_exit_count, :total_insn_count, diff --git a/zjit/src/backend/arm64/mod.rs b/zjit/src/backend/arm64/mod.rs index 30bd46fe142dd1..9b4658d7f82671 100644 --- a/zjit/src/backend/arm64/mod.rs +++ b/zjit/src/backend/arm64/mod.rs @@ -1740,6 +1740,9 @@ impl Assembler { unsafe { rb_jit_icache_invalidate(start_ptr.raw_ptr(cb) as _, cb.get_write_ptr().raw_ptr(cb) as _) }; }); + if crate::state::ZJITState::has_instance() { + crate::stats::incr_counter_by(crate::stats::Counter::total_native_stack_bytes, (asm.stack_state.stack_slot_count() * (VALUE_BITS as usize)).try_into().unwrap()); + } Ok((start_ptr, gc_offsets)) }) } diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 5f964a2734e59e..7ebfb62309a0d9 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -478,6 +478,8 @@ make_counters! { getblockparamproxy_handler_polymorphic, getblockparamproxy_handler_megamorphic, getblockparamproxy_handler_no_profiles, + + total_native_stack_bytes, } /// Increase a counter by a specified amount From ff6ac97529962b824d528b4bf32485179bb25805 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 16 Jul 2026 11:33:46 -0700 Subject: [PATCH 8/8] Revert "ZJIT: Add counters for throw and exception_handler" (#17945) Revert "ZJIT: Add counters for throw and exception_handler (#17902)" This reverts commit 61441f2d7242148cc45efdf6f3ebce4c43eaf7c6. --- zjit/src/codegen.rs | 24 +++--------------------- zjit/src/hir.rs | 1 - zjit/src/stats.rs | 12 +++++++++--- 3 files changed, 12 insertions(+), 25 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index d2dd2eb16971e3..a731aa4c99e878 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -213,7 +213,7 @@ pub extern "C" fn rb_zjit_iseq_gen_entry_point(iseq: IseqPtr, ec: EcPtr, jit_exc fn gen_iseq_entry_point(cb: &mut CodeBlock, iseq: IseqPtr, jit_exception: bool) -> Result { // We don't support exception handlers yet if jit_exception { - return gen_exception_handler_counter(cb); + return Err(CompileError::ExceptionHandler); } let iseq_name = iseq_get_location(iseq, 0); @@ -525,6 +525,7 @@ fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, version: IseqVersionRef, func debug!("ZJIT: gen_function: Failed to compile insn: {insn_id} {insn}. Generating side-exit."); gen_incr_counter(&mut asm, exit_counter_for_unhandled_hir_insn(&insn)); let reason = match insn { + Insn::Throw { .. } => SideExitReason::UnhandledHIRThrow, Insn::InvokeBuiltin { .. } => SideExitReason::UnhandledHIRInvokeBuiltin, _ => SideExitReason::UnhandledHIRUnknown(insn_id), }; @@ -783,7 +784,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio &Insn::IsA { val, class } => gen_is_a(jit, asm, opnd!(val), opnd!(class)), &Insn::ArrayMax { ref elements, state } => gen_array_max(jit, asm, function, opnds!(elements), &function.frame_state(state)), &Insn::ArrayMin { ref elements, state } => gen_array_min(jit, asm, function, opnds!(elements), &function.frame_state(state)), - &Insn::Throw { state, .. } => no_output!(gen_throw(jit, asm, function, &function.frame_state(state))), + &Insn::Throw { state, .. } => return Err(state), &Insn::CondBranch { .. } | &Insn::Jump { .. } | Insn::Entries { .. } => unreachable!(), }; @@ -2607,12 +2608,6 @@ fn gen_return(asm: &mut Assembler, val: lir::Opnd) { asm.cret(C_RET_OPND); } -fn gen_throw(jit: &mut JITState, asm: &mut Assembler, function: &Function, state: &FrameState) { - // TODO: Consider calling rb_vm_throw and propagating ec->tag->state to the interpreter. - // Also consider making it a jump on method inlining. - gen_side_exit(jit, asm, function, &SideExitReason::Throw, None, state); -} - /// Compile Fixnum + Fixnum fn gen_fixnum_add(jit: &mut JITState, asm: &mut Assembler, function: &Function, left: lir::Opnd, right: lir::Opnd, state: &FrameState) -> lir::Opnd { // Add left + right and test for overflow @@ -4002,19 +3997,6 @@ fn gen_compile_error_counter(cb: &mut CodeBlock, compile_error: &CompileError) - }) } -/// Generate a JIT entry that just increments exit_exception_handler and exits -fn gen_exception_handler_counter(cb: &mut CodeBlock) -> Result { - let mut asm = Assembler::new(); - asm.new_block_without_id("exception_handler_counter"); - gen_incr_counter(&mut asm, Counter::exit_exception_handler); - asm.cret(Qundef.into()); - - asm.compile(cb).map(|(code_ptr, gc_offsets)| { - assert_eq!(0, gc_offsets.len()); - code_ptr - }) -} - /// Given the number of spill slots needed for a function, return the number of bytes /// the function needs to allocate on the stack for the stack frame. fn aligned_stack_bytes(num_slots: usize) -> usize { diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 9e0d07e71f9812..55c07731f27f36 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -553,7 +553,6 @@ pub enum SideExitReason { PatchPoint(Invariant), CalleeSideExit, Interrupt, - Throw, BlockParamProxyNotIseqOrIfunc, BlockParamProxyNotNil, BlockParamProxyNotProc, diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 7ebfb62309a0d9..8a0c55c8d58667 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -185,7 +185,6 @@ make_counters! { exit { // exit_: Side exits reasons exit_compile_error, - exit_exception_handler, exit_unhandled_newarray_send_min, exit_unhandled_newarray_send_hash, exit_unhandled_newarray_send_pack, @@ -227,7 +226,6 @@ make_counters! { exit_patchpoint_root_box_only, exit_callee_side_exit, exit_interrupt, - exit_throw, exit_stackoverflow, exit_block_param_proxy_not_iseq_or_ifunc, exit_block_param_proxy_not_nil, @@ -351,6 +349,7 @@ make_counters! { compile_error_iseq_version_limit_reached, compile_error_iseq_stack_too_large, compile_error_native_stack_too_large, + compile_error_exception_handler, compile_error_out_of_memory, compile_error_label_linking_failure, compile_error_jit_to_jit_optional, @@ -370,6 +369,9 @@ make_counters! { compile_error_validation_misc_validation_error, // unhandled_hir_insn_: Unhandled HIR instructions + unhandled_hir_insn_array_max, + unhandled_hir_insn_fixnum_div, + unhandled_hir_insn_throw, unhandled_hir_insn_invokebuiltin, unhandled_hir_insn_unknown, @@ -523,6 +525,7 @@ pub enum CompileError { IseqVersionLimitReached, IseqStackTooLarge, NativeStackTooLarge, + ExceptionHandler, OutOfMemory, ParseError(ParseError), /// When a ZJIT function is too large, the branches may have @@ -541,6 +544,7 @@ pub fn exit_counter_for_compile_error(compile_error: &CompileError) -> Counter { IseqVersionLimitReached => compile_error_iseq_version_limit_reached, IseqStackTooLarge => compile_error_iseq_stack_too_large, NativeStackTooLarge => compile_error_native_stack_too_large, + ExceptionHandler => compile_error_exception_handler, OutOfMemory => compile_error_out_of_memory, LabelLinkingFailure => compile_error_label_linking_failure, ParseError(parse_error) => match parse_error { @@ -566,6 +570,9 @@ pub fn exit_counter_for_unhandled_hir_insn(insn: &crate::hir::Insn) -> Counter { use crate::hir::Insn::*; use crate::stats::Counter::*; match insn { + ArrayMax { .. } => unhandled_hir_insn_array_max, + FixnumDiv { .. } => unhandled_hir_insn_fixnum_div, + Throw { .. } => unhandled_hir_insn_throw, InvokeBuiltin { .. } => unhandled_hir_insn_invokebuiltin, _ => unhandled_hir_insn_unknown, } @@ -612,7 +619,6 @@ pub fn side_exit_counter(reason: crate::hir::SideExitReason) -> Counter { GuardSuperMethodEntry => exit_guard_super_method_entry, CalleeSideExit => exit_callee_side_exit, Interrupt => exit_interrupt, - Throw => exit_throw, StackOverflow => exit_stackoverflow, BlockParamProxyNotIseqOrIfunc => exit_block_param_proxy_not_iseq_or_ifunc, BlockParamProxyNotNil => exit_block_param_proxy_not_nil,