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/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 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 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/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/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..9b4658d7f82671 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() } { @@ -1737,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)) }) } @@ -2951,4 +2957,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"); + } } diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index a20ce27af512dd..a731aa4c99e878 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}}; @@ -1185,6 +1186,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 +1197,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 +1470,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 +1499,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 +1524,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 +1549,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 +1844,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 +2012,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 +2037,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)"); @@ -3421,6 +3448,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..55c07731f27f36 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, @@ -4073,16 +4071,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; } @@ -9751,6 +9745,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..0cee95a35629e2 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 "); @@ -8442,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 @@ -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 call site + Jump bb4(v27) + bb4(v20:BasicObject): CheckInterrupts Return v17 "); @@ -9761,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 @@ -16719,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) @@ -16772,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 @@ -16828,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 @@ -16881,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 @@ -16934,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 @@ -16983,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 @@ -18501,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 @@ -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 call site + 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/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/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: 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 diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index 6a05e5f50788f9..8a0c55c8d58667 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -480,6 +480,8 @@ make_counters! { getblockparamproxy_handler_polymorphic, getblockparamproxy_handler_megamorphic, getblockparamproxy_handler_no_profiles, + + total_native_stack_bytes, } /// Increase a counter by a specified amount