diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index b8cde623447a8c..74fee197f85a75 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -415,7 +415,7 @@ def test n end 3.times.map{Ractor.receive}.tally -} unless yjit_enabled? || zjit_enabled? # YJIT: `[BUG] Bus Error at 0x000000010b7002d0` in jit_exec(), ZJIT hangs +} unless yjit_enabled? # YJIT: `[BUG] Bus Error at 0x000000010b7002d0` in jit_exec() # unshareable object are copied assert_equal 'false', %q{ diff --git a/gc/default/default.c b/gc/default/default.c index e443a0727e43a4..aa06b7cc06c888 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -4388,6 +4388,25 @@ gc_grey(rb_objspace_t *objspace, VALUE obj) push_mark_stack(&objspace->mark_stack, obj); } +static inline void +gc_mark_check_t_none(rb_objspace_t *objspace, VALUE obj) +{ + if (RB_UNLIKELY(RB_TYPE_P(obj, T_NONE))) { + char obj_info_buf[256]; + rb_raw_obj_info(obj_info_buf, 256, obj); + + char parent_obj_info_buf[256]; + if (objspace->rgengc.parent_object == Qfalse) { + strcpy(parent_obj_info_buf, "(none)"); + } + else { + rb_raw_obj_info(parent_obj_info_buf, 256, objspace->rgengc.parent_object); + } + + rb_bug("try to mark T_NONE object (obj: %s, parent: %s)", obj_info_buf, parent_obj_info_buf); + } +} + static void gc_mark(rb_objspace_t *objspace, VALUE obj) { @@ -4407,10 +4426,7 @@ gc_mark(rb_objspace_t *objspace, VALUE obj) } } - if (RB_UNLIKELY(RB_TYPE_P(obj, T_NONE))) { - rb_obj_info_dump(obj); - rb_bug("try to mark T_NONE object"); /* check here will help debugging */ - } + gc_mark_check_t_none(objspace, obj); gc_aging(objspace, obj); gc_grey(objspace, obj); @@ -4504,10 +4520,7 @@ rb_gc_impl_mark_weak(void *objspace_ptr, VALUE *ptr) VALUE obj = *ptr; - if (RB_UNLIKELY(RB_TYPE_P(obj, T_NONE))) { - rb_obj_info_dump(obj); - rb_bug("try to mark T_NONE object"); - } + gc_mark_check_t_none(objspace, obj); /* If we are in a minor GC and the other object is old, then obj should * already be marked and cannot be reclaimed in this GC cycle so we don't diff --git a/gc/gc.h b/gc/gc.h index fe9aaeb96538a5..8ca9987477488c 100644 --- a/gc/gc.h +++ b/gc/gc.h @@ -58,7 +58,7 @@ RUBY_SYMBOL_EXPORT_BEGIN // files in Ruby. size_t rb_size_mul_or_raise(size_t x, size_t y, VALUE exc); void rb_objspace_reachable_objects_from(VALUE obj, void (func)(VALUE, void *), void *data); -void rb_obj_info_dump(VALUE obj); +const char *rb_raw_obj_info(char *const buff, const size_t buff_size, VALUE obj); const char *rb_obj_info(VALUE obj); size_t rb_obj_memsize_of(VALUE obj); bool ruby_free_at_exit_p(void); diff --git a/gems/bundled_gems b/gems/bundled_gems index d25355a222360b..71f79f8d0476e4 100644 --- a/gems/bundled_gems +++ b/gems/bundled_gems @@ -18,7 +18,7 @@ net-pop 0.1.2 https://github.com/ruby/net-pop net-smtp 0.5.1 https://github.com/ruby/net-smtp matrix 0.4.3 https://github.com/ruby/matrix prime 0.1.4 https://github.com/ruby/prime -rbs 3.9.4 https://github.com/ruby/rbs 368bf4b1ab52a9335e2022618ac4545a4d9cacdf +rbs 3.9.4 https://github.com/ruby/rbs aa22d7ea0c992de7557c3e346c9100b8aa36d945 typeprof 0.30.1 https://github.com/ruby/typeprof debug 1.11.0 https://github.com/ruby/debug racc 1.8.1 https://github.com/ruby/racc diff --git a/test/ruby/test_thread_cv.rb b/test/ruby/test_thread_cv.rb index eb88b9606cebf1..81201f134f0c71 100644 --- a/test/ruby/test_thread_cv.rb +++ b/test/ruby/test_thread_cv.rb @@ -76,7 +76,7 @@ def test_condvar_wait_and_broadcast condvar.broadcast result << "P2" end - Timeout.timeout(5) do + Timeout.timeout(60) do nr_threads.times do |i| threads[i].join end diff --git a/test/ruby/test_transcode.rb b/test/ruby/test_transcode.rb index 71e1cc9e2a9aa4..15e290728beadc 100644 --- a/test/ruby/test_transcode.rb +++ b/test/ruby/test_transcode.rb @@ -2321,7 +2321,7 @@ def test_newline_options end def test_ractor_lazy_load_encoding - assert_ractor("#{<<~"begin;"}\n#{<<~'end;'}") + assert_ractor("#{<<~"begin;"}\n#{<<~'end;'}", timeout: 60) begin; rs = [] autoload_encodings = Encoding.list.select { |e| e.inspect.include?("(autoload)") }.freeze @@ -2385,7 +2385,7 @@ def test_ractor_asciicompat_encoding_exists end def test_ractor_asciicompat_encoding_doesnt_exist - assert_ractor("#{<<~"begin;"}\n#{<<~'end;'}") + assert_ractor("#{<<~"begin;"}\n#{<<~'end;'}", timeout: 60) begin; rs = [] NO_EXIST = "I".freeze diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 8e450458dd42fc..e79e80fb44ce9b 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -807,6 +807,60 @@ def test(n) = n..10 } end + def test_new_range_fixnum_both_literals_inclusive + assert_compiles '1..2', %q{ + def test() + (1..2) + end + test; test + }, call_threshold: 2 + end + + def test_new_range_fixnum_both_literals_exclusive + assert_compiles '1...2', %q{ + def test() + (1...2) + end + test; test + }, call_threshold: 2 + end + + def test_new_range_fixnum_low_literal_inclusive + assert_compiles '1..3', %q{ + def test(a) + (1..a) + end + test(2); test(3) + }, call_threshold: 2 + end + + def test_new_range_fixnum_low_literal_exclusive + assert_compiles '1...3', %q{ + def test(a) + (1...a) + end + test(2); test(3) + }, call_threshold: 2 + end + + def test_new_range_fixnum_high_literal_inclusive + assert_compiles '3..10', %q{ + def test(a) + (a..10) + end + test(2); test(3) + }, call_threshold: 2 + end + + def test_new_range_fixnum_high_literal_exclusive + assert_compiles '3...10', %q{ + def test(a) + (a...10) + end + test(2); test(3) + }, call_threshold: 2 + end + def test_if assert_compiles '[0, nil]', %q{ def test(n) diff --git a/test/socket/test_socket.rb b/test/socket/test_socket.rb index 4b85d432911b88..e746aca101f808 100644 --- a/test/socket/test_socket.rb +++ b/test/socket/test_socket.rb @@ -173,8 +173,11 @@ def random_port def errors_addrinuse errs = [Errno::EADDRINUSE] - # MinGW fails with "Errno::EACCES: Permission denied - bind(2) for 0.0.0.0:49721" - errs << Errno::EACCES if /mingw/ =~ RUBY_PLATFORM + # Windows can fail with "Errno::EACCES: Permission denied - bind(2) for 0.0.0.0:49721" + # or "Test::Unit::ProxyError: Permission denied - bind(2) for 0.0.0.0:55333" + if /mswin|mingw/ =~ RUBY_PLATFORM + errs += [Errno::EACCES, Test::Unit::ProxyError] + end errs end diff --git a/zjit.rb b/zjit.rb index bcd1b3eb492229..7e5807876ce9a8 100644 --- a/zjit.rb +++ b/zjit.rb @@ -34,13 +34,18 @@ def stats_string buf = +"***ZJIT: Printing ZJIT statistics on exit***\n" stats = self.stats - print_counters_with_prefix(prefix: 'failed_', prompt: 'compilation failure reasons', buf:, stats:) + # Show exit reasons, ordered by the typical amount of exits for the prefix at the time print_counters_with_prefix(prefix: 'unhandled_call_', prompt: 'unhandled call types', buf:, stats:, limit: 20) + print_counters_with_prefix(prefix: 'unhandled_yarv_insn_', prompt: 'unhandled YARV insns', buf:, stats:, limit: 20) + print_counters_with_prefix(prefix: 'compile_error_', prompt: 'compile error reasons', buf:, stats:, limit: 20) + print_counters_with_prefix(prefix: 'exit_', prompt: 'side exit reasons', buf:, stats:, limit: 20) + + # Show the most important stats ratio_in_zjit at the end print_counters([ :dynamic_send_count, :compiled_iseq_count, - :compilation_failure, + :failed_iseq_count, :compile_time_ns, :profile_time_ns, @@ -54,8 +59,6 @@ def stats_string :zjit_insn_count, :ratio_in_zjit, ], buf:, stats:) - print_counters_with_prefix(prefix: 'unhandled_yarv_insn_', prompt: 'unhandled YARV insns', buf:, stats:, limit: 20) - print_counters_with_prefix(prefix: 'exit_', prompt: 'side exit reasons', buf:, stats:, limit: 20) buf end diff --git a/zjit/src/backend/arm64/mod.rs b/zjit/src/backend/arm64/mod.rs index e243477ec8259c..584914c83392cb 100644 --- a/zjit/src/backend/arm64/mod.rs +++ b/zjit/src/backend/arm64/mod.rs @@ -4,6 +4,7 @@ use crate::asm::{CodeBlock, Label}; use crate::asm::arm64::*; use crate::cruby::*; use crate::backend::lir::*; +use crate::stats::CompileError; use crate::virtualmem::CodePtr; use crate::cast::*; @@ -1369,7 +1370,7 @@ impl Assembler } /// Optimize and compile the stored instructions - pub fn compile_with_regs(self, cb: &mut CodeBlock, regs: Vec) -> Option<(CodePtr, Vec)> { + pub fn compile_with_regs(self, cb: &mut CodeBlock, regs: Vec) -> Result<(CodePtr, Vec), CompileError> { let asm = self.arm64_split(); let mut asm = asm.alloc_regs(regs)?; asm.compile_side_exits(); @@ -1389,11 +1390,10 @@ impl Assembler // Invalidate icache for newly written out region so we don't run stale code. unsafe { rb_zjit_icache_invalidate(start_ptr.raw_ptr(cb) as _, cb.get_write_ptr().raw_ptr(cb) as _) }; - Some((start_ptr, gc_offsets)) + Ok((start_ptr, gc_offsets)) } else { cb.clear_labels(); - - None + Err(CompileError::OutOfMemory) } } } @@ -1521,7 +1521,7 @@ mod tests { let opnd = asm.add(Opnd::Reg(X0_REG), Opnd::Reg(X1_REG)); asm.store(Opnd::mem(64, Opnd::Reg(X2_REG), 0), opnd); - asm.compile_with_regs(&mut cb, vec![X3_REG]); + asm.compile_with_regs(&mut cb, vec![X3_REG]).unwrap(); // Assert that only 2 instructions were written. assert_eq!(8, cb.get_write_pos()); diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index 538faa41c75822..068e3f69dc76b6 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -6,7 +6,7 @@ use crate::cruby::{Qundef, RUBY_OFFSET_CFP_PC, RUBY_OFFSET_CFP_SP, SIZEOF_VALUE_ use crate::hir::SideExitReason; use crate::options::{debug, get_option}; use crate::cruby::VALUE; -use crate::stats::{exit_counter_ptr, exit_counter_ptr_for_opcode, exit_counter_ptr_for_call_type}; +use crate::stats::{exit_counter_ptr, exit_counter_ptr_for_call_type, exit_counter_ptr_for_opcode, CompileError}; use crate::virtualmem::CodePtr; use crate::asm::{CodeBlock, Label}; @@ -1280,7 +1280,7 @@ impl Assembler /// Sets the out field on the various instructions that require allocated /// registers because their output is used as the operand on a subsequent /// instruction. This is our implementation of the linear scan algorithm. - pub(super) fn alloc_regs(mut self, regs: Vec) -> Option { + pub(super) fn alloc_regs(mut self, regs: Vec) -> Result { // Dump live registers for register spill debugging. fn dump_live_regs(insns: Vec, live_ranges: Vec, num_regs: usize, spill_index: usize) { // Convert live_ranges to live_regs: the number of live registers at each index @@ -1331,7 +1331,7 @@ impl Assembler new_reg } else { debug!("spilling VReg is not implemented yet, can't evacuate C_RET_REG on CCall"); - return None; + return Err(CompileError::RegisterSpillOnCCall); }; asm.mov(Opnd::Reg(new_reg), C_RET_OPND); pool.dealloc_reg(&C_RET_REG); @@ -1436,7 +1436,7 @@ impl Assembler dump_live_regs(insns, live_ranges, regs.len(), index); } debug!("Register spill not supported"); - return None; + return Err(CompileError::RegisterSpillOnAlloc); } } }; @@ -1522,13 +1522,13 @@ impl Assembler } assert!(pool.is_empty(), "Expected all registers to be returned to the pool"); - Some(asm) + Ok(asm) } /// Compile the instructions down to machine code. /// Can fail due to lack of code memory and inopportune code placement, among other reasons. #[must_use] - pub fn compile(self, cb: &mut CodeBlock) -> Option<(CodePtr, Vec)> { + pub fn compile(self, cb: &mut CodeBlock) -> Result<(CodePtr, Vec), CompileError> { #[cfg(feature = "disasm")] let start_addr = cb.get_write_ptr(); let alloc_regs = Self::get_alloc_regs(); diff --git a/zjit/src/backend/x86_64/mod.rs b/zjit/src/backend/x86_64/mod.rs index 2a02e1b725db14..5517384d5f30f5 100644 --- a/zjit/src/backend/x86_64/mod.rs +++ b/zjit/src/backend/x86_64/mod.rs @@ -2,6 +2,7 @@ use std::mem::take; use crate::asm::*; use crate::asm::x86_64::*; +use crate::stats::CompileError; use crate::virtualmem::CodePtr; use crate::cruby::*; use crate::backend::lir::*; @@ -892,7 +893,7 @@ impl Assembler } /// Optimize and compile the stored instructions - pub fn compile_with_regs(self, cb: &mut CodeBlock, regs: Vec) -> Option<(CodePtr, Vec)> { + pub fn compile_with_regs(self, cb: &mut CodeBlock, regs: Vec) -> Result<(CodePtr, Vec), CompileError> { let asm = self.x86_split(); let mut asm = asm.alloc_regs(regs)?; asm.compile_side_exits(); @@ -908,12 +909,10 @@ impl Assembler if let (Some(gc_offsets), false) = (gc_offsets, cb.has_dropped_bytes()) { cb.link_labels(); - - Some((start_ptr, gc_offsets)) + Ok((start_ptr, gc_offsets)) } else { cb.clear_labels(); - - None + Err(CompileError::OutOfMemory) } } } diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 17d8b817bd5bc3..66ad5ba3a9d0cb 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -8,12 +8,12 @@ use crate::backend::current::{Reg, ALLOC_REGS}; use crate::invariants::{track_bop_assumption, track_cme_assumption, track_single_ractor_assumption, track_stable_constant_names_assumption}; use crate::gc::{append_gc_offsets, get_or_create_iseq_payload, get_or_create_iseq_payload_ptr, IseqPayload, IseqStatus}; use crate::state::ZJITState; -use crate::stats::incr_counter; -use crate::stats::{counter_ptr, with_time_stat, Counter, Counter::{compile_time_ns, exit_compilation_failure}}; +use crate::stats::{exit_counter_for_compile_error, incr_counter, incr_counter_by, CompileError}; +use crate::stats::{counter_ptr, with_time_stat, Counter, Counter::{compile_time_ns, exit_compile_error}}; use crate::{asm::CodeBlock, cruby::*, options::debug, virtualmem::CodePtr}; use crate::backend::lir::{self, asm_comment, asm_ccall, Assembler, Opnd, Target, CFP, C_ARG_OPNDS, C_RET_OPND, EC, NATIVE_STACK_PTR, NATIVE_BASE_PTR, SCRATCH_OPND, SP}; use crate::hir::{iseq_to_hir, Block, BlockId, BranchEdge, Invariant, RangeType, SideExitReason, SideExitReason::*, SpecialObjectType, SpecialBackrefSymbol, SELF_PARAM_IDX}; -use crate::hir::{Const, FrameState, Function, Insn, InsnId}; +use crate::hir::{Const, FrameState, Function, Insn, InsnId, ParseError}; use crate::hir_type::{types, Type}; use crate::options::get_option; @@ -79,7 +79,7 @@ pub extern "C" fn rb_zjit_iseq_gen_entry_point(iseq: IseqPtr, _ec: EcPtr) -> *co let cb = ZJITState::get_code_block(); let mut code_ptr = with_time_stat(compile_time_ns, || gen_iseq_entry_point(cb, iseq)); - if code_ptr.is_none() { + if let Err(err) = &code_ptr { // Assert that the ISEQ compiles if RubyVM::ZJIT.assert_compiles is enabled if ZJITState::assert_compiles_enabled() { let iseq_location = iseq_get_location(iseq, 0); @@ -88,7 +88,7 @@ pub extern "C" fn rb_zjit_iseq_gen_entry_point(iseq: IseqPtr, _ec: EcPtr) -> *co // For --zjit-stats, generate an entry that just increments exit_compilation_failure and exits if get_option!(stats) { - code_ptr = gen_compilation_failure_counter(cb); + code_ptr = gen_compile_error_counter(cb, err); } } @@ -102,37 +102,49 @@ pub extern "C" fn rb_zjit_iseq_gen_entry_point(iseq: IseqPtr, _ec: EcPtr) -> *co } /// Compile an entry point for a given ISEQ -fn gen_iseq_entry_point(cb: &mut CodeBlock, iseq: IseqPtr) -> Option { +fn gen_iseq_entry_point(cb: &mut CodeBlock, iseq: IseqPtr) -> Result { // Compile ISEQ into High-level IR - let Some(function) = compile_iseq(iseq) else { - incr_counter!(compilation_failure); - return None; + let function = match compile_iseq(iseq) { + Ok(function) => function, + Err(err) => { + incr_counter!(failed_iseq_count); + return Err(err); + } }; // Compile the High-level IR - let Some(start_ptr) = gen_iseq(cb, iseq, Some(&function)) else { - debug!("Failed to compile iseq: gen_iseq failed: {}", iseq_get_location(iseq, 0)); - return None; + let start_ptr = match gen_iseq(cb, iseq, Some(&function)) { + Ok(start_ptr) => start_ptr, + Err(err) => { + debug!("Failed to compile iseq: gen_iseq failed: {}", iseq_get_location(iseq, 0)); + return Err(err); + } }; // Compile an entry point to the JIT code - let Some(entry_ptr) = gen_entry(cb, iseq, &function, start_ptr) else { - debug!("Failed to compile iseq: gen_entry failed: {}", iseq_get_location(iseq, 0)); - return None; + let entry_ptr = match gen_entry(cb, iseq, &function, start_ptr) { + Ok(entry_ptr) => entry_ptr, + Err(err) => { + debug!("Failed to compile iseq: gen_entry failed: {}", iseq_get_location(iseq, 0)); + return Err(err); + } }; // Return a JIT code address - Some(entry_ptr) + Ok(entry_ptr) } /// Stub a branch for a JIT-to-JIT call -fn gen_iseq_call(cb: &mut CodeBlock, caller_iseq: IseqPtr, iseq_call: &Rc>) -> Option<()> { +fn gen_iseq_call(cb: &mut CodeBlock, caller_iseq: IseqPtr, iseq_call: &Rc>) -> Result<(), CompileError> { // Compile a function stub - let Some(stub_ptr) = gen_function_stub(cb, iseq_call.clone()) else { - // Failed to compile the stub. Bail out of compiling the caller ISEQ. - debug!("Failed to compile iseq: could not compile stub: {} -> {}", - iseq_get_location(caller_iseq, 0), iseq_get_location(iseq_call.borrow().iseq, 0)); - return None; + let stub_ptr = match gen_function_stub(cb, iseq_call.clone()) { + Ok(stub_ptr) => stub_ptr, + Err(err) => { + // Failed to compile the stub. Bail out of compiling the caller ISEQ. + debug!("Failed to compile iseq: could not compile stub: {} -> {}", + iseq_get_location(caller_iseq, 0), iseq_get_location(iseq_call.borrow().iseq, 0)); + return Err(err); + } }; // Update the JIT-to-JIT call to call the stub @@ -142,7 +154,7 @@ fn gen_iseq_call(cb: &mut CodeBlock, caller_iseq: IseqPtr, iseq_call: &Rc Option { +fn gen_entry(cb: &mut CodeBlock, iseq: IseqPtr, function: &Function, function_ptr: CodePtr) -> Result { // Set up registers for CFP, EC, SP, and basic block arguments let mut asm = Assembler::new(); gen_entry_prologue(&mut asm, iseq); @@ -187,33 +199,36 @@ fn gen_entry(cb: &mut CodeBlock, iseq: IseqPtr, function: &Function, function_pt let iseq_name = iseq_get_location(iseq, 0); register_with_perf(format!("entry for {iseq_name}"), start_ptr, code_size); } - Some(code_ptr) + Ok(code_ptr) } /// Compile an ISEQ into machine code if not compiled yet -fn gen_iseq(cb: &mut CodeBlock, iseq: IseqPtr, function: Option<&Function>) -> Option { +fn gen_iseq(cb: &mut CodeBlock, iseq: IseqPtr, function: Option<&Function>) -> Result { // Return an existing pointer if it's already compiled let payload = get_or_create_iseq_payload(iseq); - match payload.status { - IseqStatus::Compiled(start_ptr) => return Some(start_ptr), - IseqStatus::CantCompile => return None, + match &payload.status { + IseqStatus::Compiled(start_ptr) => return Ok(*start_ptr), + IseqStatus::CantCompile(err) => return Err(err.clone()), IseqStatus::NotCompiled => {}, } // Compile the ISEQ let code_ptr = gen_iseq_body(cb, iseq, function, payload); - if let Some(start_ptr) = code_ptr { - payload.status = IseqStatus::Compiled(start_ptr); - incr_counter!(compiled_iseq_count); - } else { - payload.status = IseqStatus::CantCompile; - incr_counter!(compilation_failure); + match &code_ptr { + Ok(start_ptr) => { + payload.status = IseqStatus::Compiled(*start_ptr); + incr_counter!(compiled_iseq_count); + } + Err(err) => { + payload.status = IseqStatus::CantCompile(err.clone()); + incr_counter!(failed_iseq_count); + } } code_ptr } /// Compile an ISEQ into machine code -fn gen_iseq_body(cb: &mut CodeBlock, iseq: IseqPtr, function: Option<&Function>, payload: &mut IseqPayload) -> Option { +fn gen_iseq_body(cb: &mut CodeBlock, iseq: IseqPtr, function: Option<&Function>, payload: &mut IseqPayload) -> Result { // Convert ISEQ into optimized High-level IR if not given let function = match function { Some(function) => function, @@ -231,11 +246,11 @@ fn gen_iseq_body(cb: &mut CodeBlock, iseq: IseqPtr, function: Option<&Function>, // Prepare for GC payload.iseq_calls.extend(iseq_calls.clone()); append_gc_offsets(iseq, &gc_offsets); - Some(start_ptr) + Ok(start_ptr) } /// Compile a function -fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, function: &Function) -> Option<(CodePtr, Vec, Vec>>)> { +fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, function: &Function) -> Result<(CodePtr, Vec, Vec>>), CompileError> { let c_stack_slots = max_num_params(function).saturating_sub(ALLOC_REGS.len()); let mut jit = JITState::new(iseq, function.num_insns(), function.num_blocks(), c_stack_slots); let mut asm = Assembler::new(); @@ -291,7 +306,7 @@ fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, function: &Function) -> Optio // Generate code if everything can be compiled let result = asm.compile(cb); - if let Some((start_ptr, _)) = result { + if let Ok((start_ptr, _)) = result { if get_option!(perf) { let start_usize = start_ptr.raw_ptr(cb) as usize; let end_usize = cb.get_write_ptr().raw_ptr(cb) as usize; @@ -303,8 +318,6 @@ fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, function: &Function) -> Optio let iseq_name = iseq_get_location(iseq, 0); ZJITState::log_compile(iseq_name); } - } else { - incr_counter!(failed_asm_compile); } result.map(|(start_ptr, gc_offsets)| (start_ptr, gc_offsets, jit.iseq_calls)) } @@ -342,6 +355,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::NewArray { elements, state } => gen_new_array(asm, opnds!(elements), &function.frame_state(*state)), Insn::NewHash { elements, state } => gen_new_hash(jit, asm, elements, &function.frame_state(*state)), Insn::NewRange { low, high, flag, state } => gen_new_range(jit, asm, opnd!(low), opnd!(high), *flag, &function.frame_state(*state)), + Insn::NewRangeFixnum { low, high, flag, state } => gen_new_range_fixnum(asm, opnd!(low), opnd!(high), *flag, &function.frame_state(*state)), Insn::ArrayDup { val, state } => gen_array_dup(asm, opnd!(val), &function.frame_state(*state)), Insn::StringCopy { val, chilled, state } => gen_string_copy(asm, opnd!(val), *chilled, &function.frame_state(*state)), // concatstrings shouldn't have 0 strings @@ -1136,6 +1150,17 @@ fn gen_new_range( asm_ccall!(asm, rb_range_new, low, high, (flag as i64).into()) } +fn gen_new_range_fixnum( + asm: &mut Assembler, + low: lir::Opnd, + high: lir::Opnd, + flag: RangeType, + state: &FrameState, +) -> lir::Opnd { + gen_prepare_call_with_gc(asm, state); + asm_ccall!(asm, rb_range_new, low, high, (flag as i64).into()) +} + /// Compile code that exits from JIT code with a return value fn gen_return(asm: &mut Assembler, val: lir::Opnd) { // Pop the current frame (ec->cfp++) @@ -1473,7 +1498,7 @@ pub fn local_size_and_idx_to_bp_offset(local_size: usize, local_idx: usize) -> i } /// Convert ISEQ into High-level IR -fn compile_iseq(iseq: IseqPtr) -> Option { +fn compile_iseq(iseq: IseqPtr) -> Result { // Convert ZJIT instructions back to bare instructions unsafe { crate::cruby::rb_zjit_profile_disable(iseq) }; @@ -1482,8 +1507,7 @@ fn compile_iseq(iseq: IseqPtr) -> Option { let stack_max = unsafe { rb_get_iseq_body_stack_max(iseq) }; if stack_max >= i8::MAX as u32 { debug!("ISEQ stack too large: {stack_max}"); - incr_counter!(failed_iseq_stack_too_large); - return None; + return Err(CompileError::IseqStackTooLarge); } let mut function = match iseq_to_hir(iseq) { @@ -1491,8 +1515,7 @@ fn compile_iseq(iseq: IseqPtr) -> Option { Err(err) => { let name = crate::cruby::iseq_get_location(iseq, 0); debug!("ZJIT: iseq_to_hir: {err:?}: {name}"); - incr_counter!(failed_hir_compile); - return None; + return Err(CompileError::ParseError(err)); } }; if !get_option!(disable_hir_opt) { @@ -1502,10 +1525,9 @@ fn compile_iseq(iseq: IseqPtr) -> Option { #[cfg(debug_assertions)] if let Err(err) = function.validate() { debug!("ZJIT: compile_iseq: {err:?}"); - incr_counter!(failed_hir_optimize); - return None; + return Err(CompileError::ParseError(ParseError::Validation(err))); } - Some(function) + Ok(function) } /// Build a Target::SideExit for non-PatchPoint instructions @@ -1592,7 +1614,7 @@ c_callable! { // JIT-to-JIT calls don't set SP or fill nils to uninitialized (non-argument) locals. // We need to set them if we side-exit from function_stub_hit. - fn spill_stack(iseq: IseqPtr, cfp: CfpPtr, sp: *mut VALUE) { + fn prepare_for_exit(iseq: IseqPtr, cfp: CfpPtr, sp: *mut VALUE, compile_error: &CompileError) { unsafe { // Set SP which gen_push_frame() doesn't set rb_set_cfp_sp(cfp, sp); @@ -1603,6 +1625,11 @@ c_callable! { let base = sp.offset(-local_size_and_idx_to_bp_offset(local_size, num_params) as isize); slice::from_raw_parts_mut(base, local_size - num_params).fill(Qnil); } + + // Increment a compile error counter for --zjit-stats + if get_option!(stats) { + incr_counter_by(exit_counter_for_compile_error(compile_error), 1); + } } // If we already know we can't compile the ISEQ, fail early without cb.mark_all_executable(). @@ -1610,23 +1637,27 @@ c_callable! { // code path can be made read-only. But you still need the check as is while holding the VM lock in any case. let cb = ZJITState::get_code_block(); let payload = get_or_create_iseq_payload(iseq); - if cb.has_dropped_bytes() || payload.status == IseqStatus::CantCompile { + let compile_error = match &payload.status { + IseqStatus::CantCompile(err) => Some(err), + _ if cb.has_dropped_bytes() => Some(&CompileError::OutOfMemory), + _ => None, + }; + if let Some(compile_error) = compile_error { // We'll use this Rc again, so increment the ref count decremented by from_raw. unsafe { Rc::increment_strong_count(iseq_call_ptr as *const RefCell); } - // Exit to the interpreter - spill_stack(iseq, cfp, sp); + prepare_for_exit(iseq, cfp, sp, compile_error); return ZJITState::get_exit_trampoline_with_counter().raw_ptr(cb); } // Otherwise, attempt to compile the ISEQ. We have to mark_all_executable() beyond this point. let code_ptr = with_time_stat(compile_time_ns, || function_stub_hit_body(cb, &iseq_call)); - let code_ptr = if let Some(code_ptr) = code_ptr { - code_ptr - } else { - // Exit to the interpreter - spill_stack(iseq, cfp, sp); - ZJITState::get_exit_trampoline_with_counter() + let code_ptr = match code_ptr { + Ok(code_ptr) => code_ptr, + Err(compile_error) => { + prepare_for_exit(iseq, cfp, sp, &compile_error); + ZJITState::get_exit_trampoline_with_counter() + } }; cb.mark_all_executable(); code_ptr.raw_ptr(cb) @@ -1635,11 +1666,14 @@ c_callable! { } /// Compile an ISEQ for a function stub -fn function_stub_hit_body(cb: &mut CodeBlock, iseq_call: &Rc>) -> Option { +fn function_stub_hit_body(cb: &mut CodeBlock, iseq_call: &Rc>) -> Result { // Compile the stubbed ISEQ - let Some(code_ptr) = gen_iseq(cb, iseq_call.borrow().iseq, None) else { - debug!("Failed to compile iseq: gen_iseq failed: {}", iseq_get_location(iseq_call.borrow().iseq, 0)); - return None; + let code_ptr = match gen_iseq(cb, iseq_call.borrow().iseq, None) { + Ok(code_ptr) => code_ptr, + Err(err) => { + debug!("Failed to compile iseq: gen_iseq failed: {}", iseq_get_location(iseq_call.borrow().iseq, 0)); + return Err(err); + } }; // Update the stub to call the code pointer @@ -1650,11 +1684,11 @@ fn function_stub_hit_body(cb: &mut CodeBlock, iseq_call: &Rc>) asm.ccall(code_addr, vec![]); }); - Some(code_ptr) + Ok(code_ptr) } /// Compile a stub for an ISEQ called by SendWithoutBlockDirect -fn gen_function_stub(cb: &mut CodeBlock, iseq_call: Rc>) -> Option { +fn gen_function_stub(cb: &mut CodeBlock, iseq_call: Rc>) -> Result { let mut asm = Assembler::new(); asm_comment!(asm, "Stub: {}", iseq_get_location(iseq_call.borrow().iseq, 0)); @@ -1670,7 +1704,7 @@ fn gen_function_stub(cb: &mut CodeBlock, iseq_call: Rc>) -> Op } /// Generate a trampoline that is used when a -pub fn gen_function_stub_hit_trampoline(cb: &mut CodeBlock) -> Option { +pub fn gen_function_stub_hit_trampoline(cb: &mut CodeBlock) -> Result { let mut asm = Assembler::new(); asm_comment!(asm, "function_stub_hit trampoline"); @@ -1705,7 +1739,7 @@ pub fn gen_function_stub_hit_trampoline(cb: &mut CodeBlock) -> Option { } /// Generate a trampoline that is used when a function exits without restoring PC and the stack -pub fn gen_exit_trampoline(cb: &mut CodeBlock) -> Option { +pub fn gen_exit_trampoline(cb: &mut CodeBlock) -> Result { let mut asm = Assembler::new(); asm_comment!(asm, "side-exit trampoline"); @@ -1719,11 +1753,11 @@ pub fn gen_exit_trampoline(cb: &mut CodeBlock) -> Option { } /// Generate a trampoline that increments exit_compilation_failure and jumps to exit_trampoline. -pub fn gen_exit_trampoline_with_counter(cb: &mut CodeBlock, exit_trampoline: CodePtr) -> Option { +pub fn gen_exit_trampoline_with_counter(cb: &mut CodeBlock, exit_trampoline: CodePtr) -> Result { let mut asm = Assembler::new(); asm_comment!(asm, "function stub exit trampoline"); - gen_incr_counter(&mut asm, exit_compilation_failure); + gen_incr_counter(&mut asm, exit_compile_error); asm.jmp(Target::CodePtr(exit_trampoline)); asm.compile(cb).map(|(code_ptr, gc_offsets)| { @@ -1789,9 +1823,10 @@ fn gen_string_concat(jit: &mut JITState, asm: &mut Assembler, strings: Vec } /// Generate a JIT entry that just increments exit_compilation_failure and exits -fn gen_compilation_failure_counter(cb: &mut CodeBlock) -> Option { +fn gen_compile_error_counter(cb: &mut CodeBlock, compile_error: &CompileError) -> Result { let mut asm = Assembler::new(); - gen_incr_counter(&mut asm, exit_compilation_failure); + gen_incr_counter(&mut asm, exit_compile_error); + gen_incr_counter(&mut asm, exit_counter_for_compile_error(compile_error)); asm.cret(Qundef.into()); asm.compile(cb).map(|(code_ptr, gc_offsets)| { diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index 4208af03f55857..899ed4d892895f 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -979,7 +979,7 @@ pub use manual_defs::*; pub mod test_utils { use std::{ptr::null, sync::Once}; - use crate::{options::rb_zjit_prepare_options, state::rb_zjit_enabled_p, state::ZJITState}; + use crate::{options::{internal_set_num_profiles, rb_zjit_call_threshold, rb_zjit_prepare_options, DEFAULT_CALL_THRESHOLD}, state::{rb_zjit_enabled_p, ZJITState}}; use super::*; @@ -1004,6 +1004,11 @@ pub mod test_utils { rb_zjit_prepare_options(); // enable `#with_jit` on builtins ruby_init(); + // The default rb_zjit_profile_threshold is too high, so lower it for HIR tests. + if rb_zjit_call_threshold == DEFAULT_CALL_THRESHOLD { + internal_set_num_profiles(1); + } + // Pass command line options so the VM loads core library methods defined in // ruby such as from `kernel.rb`. // We drive ZJIT manually in tests, so disable heuristic compilation triggers. diff --git a/zjit/src/gc.rs b/zjit/src/gc.rs index 3462b80232ba43..efc77fabc00345 100644 --- a/zjit/src/gc.rs +++ b/zjit/src/gc.rs @@ -4,6 +4,7 @@ use std::cell::RefCell; use std::rc::Rc; use std::{ffi::c_void, ops::Range}; use crate::codegen::IseqCall; +use crate::stats::CompileError; use crate::{cruby::*, profile::IseqProfile, state::ZJITState, stats::with_time_stat, virtualmem::CodePtr}; use crate::stats::Counter::gc_time_ns; @@ -38,7 +39,7 @@ impl IseqPayload { pub enum IseqStatus { /// CodePtr has the JIT code address of the first block Compiled(CodePtr), - CantCompile, + CantCompile(CompileError), NotCompiled, } diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 4a07a1aa742c71..afd158211dfd3f 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -12,7 +12,7 @@ use std::{ use crate::hir_type::{Type, types}; use crate::bitset::BitSet; use crate::profile::{TypeDistributionSummary, ProfiledType}; -use crate::stats::{incr_counter, Counter}; +use crate::stats::Counter; /// An index of an [`Insn`] in a [`Function`]. This is a popular /// type since this effectively acts as a pointer to an [`Insn`]. @@ -501,6 +501,7 @@ pub enum Insn { /// NewHash contains a vec of (key, value) pairs NewHash { elements: Vec<(InsnId,InsnId)>, state: InsnId }, NewRange { low: InsnId, high: InsnId, flag: RangeType, state: InsnId }, + NewRangeFixnum { low: InsnId, high: InsnId, flag: RangeType, state: InsnId }, ArrayDup { val: InsnId, state: InsnId }, ArrayMax { elements: Vec, state: InsnId }, /// Extend `left` with the elements from `right`. `left` and `right` must both be `Array`. @@ -689,6 +690,7 @@ impl Insn { // TODO: NewRange is effects free if we can prove the two ends to be Fixnum, // but we don't have type information here in `impl Insn`. See rb_range_new(). Insn::NewRange { .. } => true, + Insn::NewRangeFixnum { .. } => false, _ => true, } } @@ -734,6 +736,9 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { Insn::NewRange { low, high, flag, .. } => { write!(f, "NewRange {low} {flag} {high}") } + Insn::NewRangeFixnum { low, high, flag, .. } => { + write!(f, "NewRangeFixnum {low} {flag} {high}") + } Insn::ArrayMax { elements, .. } => { write!(f, "ArrayMax")?; let mut prefix = " "; @@ -1058,7 +1063,7 @@ impl + PartialEq> UnionFind { } } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub enum ValidationError { BlockHasNoTerminator(BlockId), // The terminator and its actual position @@ -1313,6 +1318,7 @@ impl Function { NewHash { elements: found_elements, state: find!(state) } } &NewRange { low, high, flag, state } => NewRange { low: find!(low), high: find!(high), flag, state: find!(state) }, + &NewRangeFixnum { low, high, flag, state } => NewRangeFixnum { low: find!(low), high: find!(high), flag, state: find!(state) }, &ArrayMax { ref elements, state } => ArrayMax { elements: find_vec!(elements), state: find!(state) }, &SetGlobal { id, val, state } => SetGlobal { id, val: find!(val), state }, &GetIvar { self_val, id, state } => GetIvar { self_val: find!(self_val), id, state }, @@ -1383,6 +1389,7 @@ impl Function { Insn::NewHash { .. } => types::HashExact, Insn::HashDup { .. } => types::HashExact, Insn::NewRange { .. } => types::RangeExact, + Insn::NewRangeFixnum { .. } => types::RangeExact, Insn::CCall { return_type, .. } => *return_type, Insn::GuardType { val, guard_type, .. } => self.type_of(*val).intersection(*guard_type), Insn::GuardBitEquals { val, expected, .. } => self.type_of(*val).intersection(Type::from_value(*expected)), @@ -1940,6 +1947,52 @@ impl Function { .unwrap_or(insn_id) } + fn optimize_ranges(&mut self) { + for block in self.rpo() { + let old_insns = std::mem::take(&mut self.blocks[block.0].insns); + assert!(self.blocks[block.0].insns.is_empty()); + + for insn_id in old_insns { + match self.find(insn_id) { + Insn::NewRange { low, high, flag, state } => { + + // The NewRange rewrite triggers mostly on literals because that is the + // case we can easily prove Fixnum statically and cheaply guard the other + // side. + let low_is_fix = self.is_a(low, types::Fixnum); + let high_is_fix = self.is_a(high, types::Fixnum); + + if low_is_fix && high_is_fix { + // Both statically fixnum => specialize directly + let repl = self.push_insn(block, Insn::NewRangeFixnum { low, high, flag, state }); + self.make_equal_to(insn_id, repl); + self.insn_types[repl.0] = self.infer_type(repl); + } else if low_is_fix { + // Only left is fixnum => guard right + let high_fix = self.coerce_to_fixnum(block, high, state); + let repl = self.push_insn(block, Insn::NewRangeFixnum { low, high: high_fix, flag, state }); + self.make_equal_to(insn_id, repl); + self.insn_types[repl.0] = self.infer_type(repl); + } else if high_is_fix { + // Only right is fixnum => guard left + let low_fix = self.coerce_to_fixnum(block, low, state); + let repl = self.push_insn(block, Insn::NewRangeFixnum { low: low_fix, high, flag, state }); + self.make_equal_to(insn_id, repl); + self.insn_types[repl.0] = self.infer_type(repl); + } else { + // Keep generic op + self.push_insn_id(block, insn_id); + } + } + _ => { + self.push_insn_id(block, insn_id); + } + } + } + } + self.infer_types(); + } + /// Use type information left by `infer_types` to fold away operations that can be evaluated at compile-time. /// /// It can fold fixnum math, truthiness tests, and branches with constant conditionals. @@ -2077,7 +2130,8 @@ impl Function { } worklist.push_back(state); } - &Insn::NewRange { low, high, state, .. } => { + &Insn::NewRange { low, high, state, .. } + | &Insn::NewRangeFixnum { low, high, state, .. } => { worklist.push_back(low); worklist.push_back(high); worklist.push_back(state); @@ -2331,6 +2385,8 @@ impl Function { #[cfg(debug_assertions)] self.assert_validates(); self.optimize_c_calls(); #[cfg(debug_assertions)] self.assert_validates(); + self.optimize_ranges(); + #[cfg(debug_assertions)] self.assert_validates(); self.fold_constants(); #[cfg(debug_assertions)] self.assert_validates(); self.clean_cfg(); @@ -2823,7 +2879,7 @@ pub enum CallType { Forwarding, } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub enum ParameterType { Optional, /// For example, `foo(...)`. Interaction of JIT @@ -2831,7 +2887,7 @@ pub enum ParameterType { Forwardable, } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub enum ParseError { StackUnderflow(FrameState), UnknownParameterType(ParameterType), @@ -3619,7 +3675,6 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { fun.profiles = Some(profiles); if let Err(e) = fun.validate() { - incr_counter!(failed_hir_compile_validate); return Err(ParseError::Validation(e)); } Ok(fun) @@ -6548,6 +6603,116 @@ mod opt_tests { "); } + #[test] + fn test_optimize_range_fixnum_inclusive_literals() { + eval(" + def test() + (1..2) + end + test; test + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:3: + bb0(v0:BasicObject): + v2:RangeExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + CheckInterrupts + Return v2 + "); + } + + #[test] + fn test_optimize_range_fixnum_exclusive_literals() { + eval(" + def test() + (1...2) + end + test; test + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:3: + bb0(v0:BasicObject): + v2:RangeExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + CheckInterrupts + Return v2 + "); + } + + #[test] + fn test_optimize_range_fixnum_inclusive_high_guarded() { + eval(" + def test(a) + (1..a) + end + test(2); test(3) + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:3: + bb0(v0:BasicObject, v1:BasicObject): + v3:Fixnum[1] = Const Value(1) + v9:Fixnum = GuardType v1, Fixnum + v10:RangeExact = NewRangeFixnum v3 NewRangeInclusive v9 + CheckInterrupts + Return v10 + "); + } + + #[test] + fn test_optimize_range_fixnum_exclusive_high_guarded() { + eval(" + def test(a) + (1...a) + end + test(2); test(3) + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:3: + bb0(v0:BasicObject, v1:BasicObject): + v3:Fixnum[1] = Const Value(1) + v9:Fixnum = GuardType v1, Fixnum + v10:RangeExact = NewRangeFixnum v3 NewRangeExclusive v9 + CheckInterrupts + Return v10 + "); + } + + #[test] + fn test_optimize_range_fixnum_inclusive_low_guarded() { + eval(" + def test(a) + (a..10) + end + test(2); test(3) + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:3: + bb0(v0:BasicObject, v1:BasicObject): + v3:Fixnum[10] = Const Value(10) + v9:Fixnum = GuardType v1, Fixnum + v10:RangeExact = NewRangeFixnum v9 NewRangeInclusive v3 + CheckInterrupts + Return v10 + "); + } + + #[test] + fn test_optimize_range_fixnum_exclusive_low_guarded() { + eval(" + def test(a) + (a...10) + end + test(2); test(3) + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:3: + bb0(v0:BasicObject, v1:BasicObject): + v3:Fixnum[10] = Const Value(10) + v9:Fixnum = GuardType v1, Fixnum + v10:RangeExact = NewRangeFixnum v9 NewRangeExclusive v3 + CheckInterrupts + Return v10 + "); + } + #[test] fn test_eliminate_new_array() { eval(" diff --git a/zjit/src/options.rs b/zjit/src/options.rs index 155c3805bad0fd..00fb9c50a57127 100644 --- a/zjit/src/options.rs +++ b/zjit/src/options.rs @@ -3,18 +3,25 @@ use std::os::raw::{c_char, c_int, c_uint}; use crate::cruby::*; use std::collections::HashSet; +/// Default --zjit-num-profiles +const DEFAULT_NUM_PROFILES: u8 = 5; + +/// Default --zjit-call-threshold. This should be large enough to avoid compiling +/// warmup code, but small enough to perform well on micro-benchmarks. +pub const DEFAULT_CALL_THRESHOLD: u64 = 30; + /// Number of calls to start profiling YARV instructions. /// They are profiled `rb_zjit_call_threshold - rb_zjit_profile_threshold` times, /// which is equal to --zjit-num-profiles. #[unsafe(no_mangle)] #[allow(non_upper_case_globals)] -pub static mut rb_zjit_profile_threshold: u64 = 1; +pub static mut rb_zjit_profile_threshold: u64 = DEFAULT_CALL_THRESHOLD - DEFAULT_NUM_PROFILES as u64; /// Number of calls to compile ISEQ with ZJIT at jit_compile() in vm.c. /// --zjit-call-threshold=1 compiles on first execution without profiling information. #[unsafe(no_mangle)] #[allow(non_upper_case_globals)] -pub static mut rb_zjit_call_threshold: u64 = 2; +pub static mut rb_zjit_call_threshold: u64 = DEFAULT_CALL_THRESHOLD; /// ZJIT command-line options. This is set before rb_zjit_init() sets /// ZJITState so that we can query some options while loading builtins. @@ -67,7 +74,7 @@ impl Default for Options { fn default() -> Self { Options { exec_mem_bytes: 64 * 1024 * 1024, - num_profiles: 1, + num_profiles: DEFAULT_NUM_PROFILES, stats: false, debug: false, disable_hir_opt: false, @@ -279,6 +286,7 @@ fn update_profile_threshold() { } } +#[cfg(test)] pub fn internal_set_num_profiles(n: u8) { let options = unsafe { OPTIONS.as_mut().unwrap() }; options.num_profiles = n; diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index ea979b534a32ca..77f6d99fc7efb8 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -1,6 +1,6 @@ use std::time::Instant; -use crate::{cruby::*, options::get_option, state::{zjit_enabled_p, ZJITState}}; +use crate::{cruby::*, hir::ParseError, options::get_option, state::{zjit_enabled_p, ZJITState}}; macro_rules! make_counters { ( @@ -72,7 +72,7 @@ make_counters! { // Default counters that are available without --zjit-stats default { compiled_iseq_count, - compilation_failure, + failed_iseq_count, compile_time_ns, profile_time_ns, @@ -83,7 +83,7 @@ make_counters! { // Exit counters that are summed as side_exit_count exit { // exit_: Side exits reasons - exit_compilation_failure, + exit_compile_error, exit_unknown_newarray_send, exit_unhandled_call_type, exit_unknown_special_variable, @@ -114,12 +114,17 @@ make_counters! { unhandled_call_splat_mut, unhandled_call_forwarding, - // failed_: Compilation failure reasons - failed_iseq_stack_too_large, - failed_hir_compile, - failed_hir_compile_validate, - failed_hir_optimize, - failed_asm_compile, + // compile_error_: Compile error reasons + compile_error_iseq_stack_too_large, + compile_error_out_of_memory, + compile_error_register_spill_on_ccall, + compile_error_register_spill_on_alloc, + compile_error_parse_stack_underflow, + compile_error_parse_malformed_iseq, + compile_error_parse_validation, + compile_error_parse_not_allowed, + compile_error_parse_parameter_type_optional, + compile_error_parse_parameter_type_forwardable, // The number of times YARV instructions are executed on JIT code zjit_insn_count, @@ -171,6 +176,40 @@ pub fn exit_counter_ptr_for_call_type(call_type: crate::hir::CallType) -> *mut u counter_ptr(counter) } +/// Reason why ZJIT failed to produce any JIT code +#[derive(Clone, Debug, PartialEq)] +pub enum CompileError { + IseqStackTooLarge, + OutOfMemory, + RegisterSpillOnAlloc, + RegisterSpillOnCCall, + ParseError(ParseError), +} + +/// Return a raw pointer to the exit counter for a given CompileError +pub fn exit_counter_for_compile_error(compile_error: &CompileError) -> Counter { + use crate::hir::ParseError::*; + use crate::hir::ParameterType::*; + use crate::stats::CompileError::*; + use crate::stats::Counter::*; + match compile_error { + IseqStackTooLarge => compile_error_iseq_stack_too_large, + OutOfMemory => compile_error_out_of_memory, + RegisterSpillOnAlloc => compile_error_register_spill_on_alloc, + RegisterSpillOnCCall => compile_error_register_spill_on_ccall, + ParseError(parse_error) => match parse_error { + StackUnderflow(_) => compile_error_parse_stack_underflow, + MalformedIseq(_) => compile_error_parse_malformed_iseq, + Validation(_) => compile_error_parse_validation, + NotAllowed => compile_error_parse_not_allowed, + UnknownParameterType(parameter_type) => match parameter_type { + Optional => compile_error_parse_parameter_type_optional, + Forwardable => compile_error_parse_parameter_type_forwardable, + } + } + } +} + pub fn exit_counter_ptr(reason: crate::hir::SideExitReason) -> *mut u64 { use crate::hir::SideExitReason::*; use crate::stats::Counter::*;