summaryrefslogtreecommitdiff
path: root/zjit.rb
diff options
context:
space:
mode:
Diffstat (limited to 'zjit.rb')
-rw-r--r--zjit.rb243
1 files changed, 217 insertions, 26 deletions
diff --git a/zjit.rb b/zjit.rb
index 871b1a91ca..9c7309d67b 100644
--- a/zjit.rb
+++ b/zjit.rb
@@ -7,8 +7,13 @@
# This module may not exist if ZJIT does not support the particular platform
# for which CRuby is built.
module RubyVM::ZJIT
+ # Blocks that are called when ZJIT is enabled
+ @jit_hooks = []
# Avoid calling a Ruby method here to avoid interfering with compilation tests
- if Primitive.rb_zjit_stats_enabled_p
+ if Primitive.rb_zjit_get_stats_file_path_p
+ at_exit { print_stats_file }
+ end
+ if Primitive.rb_zjit_print_stats_p
at_exit { print_stats }
end
end
@@ -19,46 +24,209 @@ class << RubyVM::ZJIT
Primitive.cexpr! 'RBOOL(rb_zjit_enabled_p)'
end
+ # Enable ZJIT compilation.
+ def enable
+ return false if enabled?
+
+ if Primitive.cexpr! 'RBOOL(rb_yjit_enabled_p)'
+ warn("Only one JIT can be enabled at the same time.")
+ return false
+ end
+
+ Primitive.rb_zjit_enable
+ end
+
+ # Check if `--zjit-trace-exits` is used
+ def trace_exit_locations_enabled?
+ Primitive.rb_zjit_trace_exit_locations_enabled_p
+ end
+
+ # A directive for the compiler to fail to compile the call to this method.
+ # To show this to ZJIT, say `::RubyVM::ZJIT.induce_compile_failure!` verbatim.
+ # Other forms are too dynamic to detect during compilation.
+ #
+ # Actually running this method does nothing, whether ZJIT sees the call or not.
+ def induce_compile_failure! = nil
+
+ # A directive for the compiler to exit out of compiled code at the call site of this method.
+ # To show this to ZJIT, say `::RubyVM::ZJIT.induce_side_exit!` verbatim.
+ # Other forms are too dynamic to detect during compilation.
+ #
+ # Actually running this method does nothing, whether ZJIT sees the call or not.
+ def induce_side_exit! = nil
+
+ # A directive for the compiler to emit a breakpoint instruction at the call site of this method.
+ # To show this to ZJIT, say `::RubyVM::ZJIT.induce_breakpoint!` verbatim.
+ # Other forms are too dynamic to detect during compilation.
+ def induce_breakpoint! = nil
+
# Check if `--zjit-stats` is used
def stats_enabled?
Primitive.rb_zjit_stats_enabled_p
end
# Return ZJIT statistics as a Hash
- def stats(key = nil)
- stats = Primitive.rb_zjit_stats(key)
- return stats if stats.nil? || !key.nil?
-
- if stats.key?(:vm_insns_count) && stats.key?(:zjit_insns_count)
- stats[:total_insns_count] = stats[:vm_insns_count] + stats[:zjit_insns_count]
- stats[:ratio_in_zjit] = 100.0 * stats[:zjit_insns_count] / stats[:total_insns_count]
- end
+ def stats(target_key = nil)
+ Primitive.rb_zjit_stats(target_key)
+ end
- stats
+ # Discard statistics collected for `--zjit-stats`.
+ def reset_stats!
+ Primitive.rb_zjit_reset_stats_bang
end
# Get the summary of ZJIT statistics as a String
def stats_string
- buf = +''
- stats = self.stats
+ return unless stats = self.stats
+ buf = +"***ZJIT: Printing ZJIT statistics on exit***\n"
+
+ if stats[:guard_type_count]&.nonzero?
+ stats[:guard_type_exit_ratio] = stats[:exit_guard_type_failure].to_f / stats[:guard_type_count] * 100
+ end
+ if stats[:guard_shape_count]&.nonzero?
+ stats[:guard_shape_exit_ratio] = stats[:exit_guard_shape_failure].to_f / stats[:guard_shape_count] * 100
+ end
+ if stats[:code_region_bytes]&.nonzero?
+ stats[:side_exit_size_ratio] = stats[:side_exit_size].to_f / stats[:code_region_bytes] * 100
+ end
+ if stats[:compile_time_ns]&.nonzero?
+ stats[:compile_side_exit_time_ratio] = stats[:compile_side_exit_time_ns].to_f / stats[:compile_time_ns] * 100
+ end
+
+ # Show counters independent from exit_* or dynamic_send_*
+ print_counters_with_prefix(prefix: 'not_inlined_cfuncs_', prompt: 'not inlined C methods', buf:, stats:, limit: 20)
+ print_counters_with_prefix(prefix: 'ccall_', prompt: 'calls to C functions from JIT code', buf:, stats:, limit: 20)
+ print_counters_with_prefix(prefix: 'iseq_calls_count_', prompt: 'most called JIT functions', buf:, stats:, limit: 20)
+ # Don't show not_annotated_cfuncs right now because it mostly duplicates not_inlined_cfuncs
+ # print_counters_with_prefix(prefix: 'not_annotated_cfuncs_', prompt: 'not annotated C methods', buf:, stats:, limit: 20)
+
+ # Show fallback counters, ordered by the typical amount of fallbacks for the prefix at the time
+ print_counters_with_prefix(prefix: 'unspecialized_send_def_type_', prompt: 'not optimized method types for send', buf:, stats:, limit: 20)
+ print_counters_with_prefix(prefix: 'unspecialized_send_without_block_def_type_', prompt: 'not optimized method types for send_without_block', buf:, stats:, limit: 20)
+ print_counters_with_prefix(prefix: 'unspecialized_super_def_type_', prompt: 'not optimized method types for super', buf:, stats:, limit: 20)
+ print_counters_with_prefix(prefix: 'uncategorized_fallback_yarv_insn_', prompt: 'instructions with uncategorized fallback reason', buf:, stats:, limit: 20)
+ print_counters_with_prefix(prefix: 'send_fallback_', prompt: 'send fallback reasons', buf:, stats:, limit: 20)
+ print_counters_with_prefix(prefix: 'setivar_fallback_', prompt: 'setivar fallback reasons', buf:, stats:, limit: 5)
+ print_counters_with_prefix(prefix: 'getivar_fallback_', prompt: 'getivar fallback reasons', buf:, stats:, limit: 5)
+ print_counters_with_prefix(prefix: 'definedivar_fallback_', prompt: 'definedivar fallback reasons', buf:, stats:, limit: 5)
+ print_counters_with_prefix(prefix: 'invokeblock_handler_', prompt: 'invokeblock handler', buf:, stats:, limit: 10)
+ print_counters_with_prefix(prefix: 'getblockparamproxy_handler_', prompt: 'getblockparamproxy handler', buf:, stats:, limit: 10)
+
+ # Show most popular unsupported call features. Because each call can
+ # use multiple complex features, a decrease in this number does not
+ # necessarily mean an increase in number of optimized calls.
+ print_counters_with_prefix(prefix: 'complex_arg_pass_', prompt: 'popular complex argument-parameter features not optimized', buf:, stats:, limit: 10)
+
+ # Show exit counters, ordered by the typical amount of exits for the prefix at the time
+ print_counters_with_prefix(prefix: 'compile_error_', prompt: 'compile error reasons', 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: 'unhandled_hir_insn_', prompt: 'unhandled HIR insns', buf:, stats:, limit: 20)
+ print_counters_with_prefix(prefix: 'exit_', prompt: 'side exit reasons', buf:, stats:, limit: 20)
+
+ # Show no-prefix counters, having the most important stat `ratio_in_zjit` at the end
+ print_counters([
+ :send_count,
+ :dynamic_send_count,
+ :optimized_send_count,
+ :dynamic_setivar_count,
+ :dynamic_getivar_count,
+ :dynamic_definedivar_count,
+ :iseq_optimized_send_count,
+ :inline_cfunc_optimized_send_count,
+ :inline_iseq_optimized_send_count,
+ :non_variadic_cfunc_optimized_send_count,
+ :variadic_cfunc_optimized_send_count,
+ ], buf:, stats:, right_align: true, base: :send_count)
+ print_counters([
+ :compiled_iseq_count,
+ :compiled_side_exit_count,
+ :failed_iseq_count,
- [
:compile_time_ns,
+ :compile_side_exit_time_ns,
+ :compile_side_exit_time_ratio,
+ :compile_hir_time_ns,
+ :compile_hir_build_time_ns,
+ :compile_hir_strength_reduce_time_ns,
+ :compile_hir_canonicalize_time_ns,
+ :compile_hir_fold_constants_time_ns,
+ :compile_hir_clean_cfg_time_ns,
+ :compile_hir_eliminate_dead_code_time_ns,
+ :compile_lir_time_ns,
:profile_time_ns,
:gc_time_ns,
:invalidation_time_ns,
- :total_insns_count,
- :vm_insns_count,
- :zjit_insns_count,
+
+ :vm_write_jit_frame_count,
+ :vm_write_sp_count,
+ :vm_write_locals_count,
+ :vm_write_stack_count,
+ :vm_write_to_parent_iseq_local_count,
+
+ :guard_type_count,
+ :guard_type_exit_ratio,
+ :guard_shape_count,
+ :guard_shape_exit_ratio,
+
+ :load_field_count,
+ :store_field_count,
+
+ :side_exit_size,
+ :code_region_bytes,
+ :side_exit_size_ratio,
+ :zjit_alloc_bytes,
+ :total_mem_bytes,
+
+ :side_exit_count,
+ :total_insn_count,
+ :vm_insn_count,
+ :zjit_insn_count,
:ratio_in_zjit,
- ].each do |key|
- # Some stats like vm_insns_count and ratio_in_zjit are not supported on the release build
+ ], buf:, stats:)
+
+ buf
+ end
+
+ # Assert that any future ZJIT compilation will return a function pointer
+ def assert_compiles # :nodoc:
+ Primitive.rb_zjit_assert_compiles
+ end
+
+ # :stopdoc:
+ private
+
+ # Register a block to be called when ZJIT is enabled
+ def add_jit_hook(hook)
+ @jit_hooks << hook
+ end
+
+ # Run ZJIT hooks registered by `#with_jit`
+ def call_jit_hooks
+ @jit_hooks.each(&:call)
+ @jit_hooks.clear
+ end
+
+ def print_counters(keys, buf:, stats:, right_align: false, base: nil)
+ key_pad = keys.map { |key| key.to_s.sub(/_time_ns\z/, '_time').size }.max + 1
+ key_align = '-' unless right_align
+ value_pad = keys.filter_map { |key| stats[key] }.map { |value| number_with_delimiter(value).size }.max
+
+ keys.each do |key|
+ # Some stats like vm_insn_count and ratio_in_zjit are not supported on the release build
next unless stats.key?(key)
value = stats[key]
+ if base && key != base
+ total = stats[base]
+ if total.nonzero?
+ ratio = " (%4.1f%%)" % (100.0 * value / total)
+ end
+ end
case key
when :ratio_in_zjit
value = '%0.1f%%' % value
+ when :guard_type_exit_ratio, :guard_shape_exit_ratio, :side_exit_size_ratio, :compile_side_exit_time_ratio
+ value = '%0.1f%%' % value
when /_time_ns\z/
key = key.to_s.sub(/_time_ns\z/, '_time')
value = "#{number_with_delimiter(value / 10**6)}ms"
@@ -66,18 +234,32 @@ class << RubyVM::ZJIT
value = number_with_delimiter(value)
end
- buf << "#{'%-18s' % "#{key}:"} #{value}\n"
+ buf << "%#{key_align}*s %*s%s\n" % [key_pad, "#{key}:", value_pad, value, ratio]
end
- buf
end
- # Assert that any future ZJIT compilation will return a function pointer
- def assert_compiles # :nodoc:
- Primitive.rb_zjit_assert_compiles
- end
+ def print_counters_with_prefix(buf:, stats:, prefix:, prompt:, limit: nil)
+ counters = stats.select { |key, value| key.start_with?(prefix) && value > 0 }
+ return if counters.empty?
- # :stopdoc:
- private
+ counters.transform_keys! { |key| key.to_s.delete_prefix(prefix) }
+ total = counters.values.sum
+
+ counters = counters.to_a
+ counters.sort_by! { |_, value| -value }
+ counters = counters.first(limit) if limit
+
+ key_pad = counters.map { |key, _| key.size }.max
+ value_pad = counters.map { |_, value| number_with_delimiter(value).size }.max
+
+ buf << "Top-#{counters.size} " if limit
+ buf << "#{prompt}"
+ buf << " (%.1f%% of total #{number_with_delimiter(total)})" % (100.0 * counters.map(&:last).sum / total) if limit
+ buf << ":\n"
+ counters.each do |key, value|
+ buf << " %*s: %*s (%4.1f%%)\n" % [key_pad, key, value_pad, number_with_delimiter(value), (100.0 * value / total)]
+ end
+ end
def number_with_delimiter(number)
s = number.to_s
@@ -90,4 +272,13 @@ class << RubyVM::ZJIT
def print_stats
$stderr.write stats_string
end
+
+ # Print ZJIT stats to file
+ def print_stats_file
+ filename = Primitive.rb_zjit_get_stats_file_path_p
+ File.open(filename, "wb") do |file|
+ file.write stats_string
+ end
+ end
+
end