123456789_123456789_123456789_123456789_123456789_

Module: RubyVM::YJIT

Relationships & Source Files
Defined in: yjit.rb

Overview

This module allows for introspection of YJIT, CRuby’s just-in-time compiler. Everything in the module is highly implementation specific and the API might be less stable compared to the standard library.

This module may not exist if YJIT does not support the particular platform for which CRuby is built.

Class Attribute Summary

Class Method Summary

Class Attribute Details

.enabled?Boolean (readonly)

Check if YJIT is enabled.

[ GitHub ]

  
# File 'yjit.rb', line 12

def self.enabled?
  Primitive.cexpr! 'RBOOL(rb_yjit_enabled_p)'
end

.log_enabled?Boolean (readonly)

Check if –yjit-log is used.

[ GitHub ]

  
# File 'yjit.rb', line 22

def self.log_enabled?
  Primitive.rb_yjit_log_enabled_p
end

.stats_enabled?Boolean (readonly)

Check if –yjit-stats is used.

[ GitHub ]

  
# File 'yjit.rb', line 17

def self.stats_enabled?
  Primitive.rb_yjit_stats_enabled_p
end

.trace_exit_locations_enabled?Boolean (readonly)

This method is for internal use only.

Check if rb_yjit_trace_exit_locations_enabled_p is enabled.

[ GitHub ]

  
# File 'yjit.rb', line 27

def self.trace_exit_locations_enabled? # :nodoc:
  Primitive.rb_yjit_trace_exit_locations_enabled_p
end

Class Method Details

._dump_locations (private)

This method is for internal use only.
[ GitHub ]

  
# File 'yjit.rb', line 279

def _dump_locations # :nodoc:
  return unless trace_exit_locations_enabled?

  filename = "yjit_exit_locations.dump"
  dump_exit_locations(filename)

  $stderr.puts("YJIT exit locations dumped to `#{filename}`.")
end

._print_stats(out: $stderr) (private)

This method is for internal use only.

Format and print out counters

[ GitHub ]

  
# File 'yjit.rb', line 331

def _print_stats(out: $stderr) # :nodoc:
  stats = runtime_stats()
  return unless Primitive.rb_yjit_stats_enabled_p

  out.puts("***YJIT: Printing YJIT statistics on exit***")

  _print_stats_reasons(stats, out)

  # Number of failed compiler invocations
  compilation_failure = stats[:compilation_failure]

  code_region_overhead = stats[:code_region_size] - (stats[:inline_code_size] + stats[:outlined_code_size])

  out.puts "num_send:              " + format_number(13, stats[:num_send])
  out.puts "num_send_known_class:  " + format_number_pct(13, stats[:num_send_known_class], stats[:num_send])
  out.puts "num_send_polymorphic:  " + format_number_pct(13, stats[:num_send_polymorphic], stats[:num_send])
  out.puts "num_send_megamorphic:  " + format_number_pct(13, stats[:send_megamorphic], stats[:num_send])
  out.puts "num_send_dynamic:      " + format_number_pct(13, stats[:num_send_dynamic], stats[:num_send])
  out.puts "num_send_cfunc:        " + format_number_pct(13, stats[:num_send_cfunc], stats[:num_send])
  out.puts "num_send_cfunc_inline: " + format_number_pct(13, stats[:num_send_cfunc_inline], stats[:num_send_cfunc])
  out.puts "num_send_iseq:         " + format_number_pct(13, stats[:num_send_iseq], stats[:num_send])
  out.puts "num_send_iseq_leaf:    " + format_number_pct(13, stats[:num_send_iseq_leaf], stats[:num_send_iseq])
  out.puts "num_send_iseq_inline:  " + format_number_pct(13, stats[:num_send_iseq_inline], stats[:num_send_iseq])
  if stats[:num_send_x86_rel32] != 0 || stats[:num_send_x86_reg] != 0
    out.puts "num_send_x86_rel32:    " + format_number(13,  stats[:num_send_x86_rel32])
    out.puts "num_send_x86_reg:      " + format_number(13, stats[:num_send_x86_reg])
  end
  out.puts "num_getivar_megamorphic: " + format_number(11, stats[:num_getivar_megamorphic])
  out.puts "num_setivar_megamorphic: " + format_number(11, stats[:num_setivar_megamorphic])
  out.puts "num_opt_case_megamorphic: " + format_number(10, stats[:num_opt_case_dispatch_megamorphic])
  out.puts "num_throw:             " + format_number(13, stats[:num_throw])
  out.puts "num_throw_break:       " + format_number_pct(13, stats[:num_throw_break], stats[:num_throw])
  out.puts "num_throw_retry:       " + format_number_pct(13, stats[:num_throw_retry], stats[:num_throw])
  out.puts "num_throw_return:      " + format_number_pct(13, stats[:num_throw_return], stats[:num_throw])
  out.puts "num_lazy_frame_check:  " + format_number(13, stats[:num_lazy_frame_check])
  out.puts "num_lazy_frame_push:   " + format_number_pct(13, stats[:num_lazy_frame_push], stats[:num_lazy_frame_check])
  out.puts "lazy_frame_count:      " + format_number(13, stats[:lazy_frame_count])
  out.puts "lazy_frame_failure:    " + format_number(13, stats[:lazy_frame_failure])

  out.puts "iseq_stack_too_large:  " + format_number(13, stats[:iseq_stack_too_large])
  out.puts "iseq_too_long:         " + format_number(13, stats[:iseq_too_long])
  out.puts "temp_reg_opnd:         " + format_number(13, stats[:temp_reg_opnd])
  out.puts "temp_mem_opnd:         " + format_number(13, stats[:temp_mem_opnd])
  out.puts "temp_spill:            " + format_number(13, stats[:temp_spill])
  out.puts "bindings_allocations:  " + format_number(13, stats[:binding_allocations])
  out.puts "bindings_set:          " + format_number(13, stats[:binding_set])
  out.puts "compilation_failure:   " + format_number(13, compilation_failure) if compilation_failure != 0
  out.puts "live_iseq_count:       " + format_number(13, stats[:live_iseq_count])
  out.puts "iseq_alloc_count:      " + format_number(13, stats[:iseq_alloc_count])
  out.puts "compiled_iseq_entry:   " + format_number(13, stats[:compiled_iseq_entry])
  out.puts "cold_iseq_entry:       " + format_number_pct(13, stats[:cold_iseq_entry], stats[:compiled_iseq_entry] + stats[:cold_iseq_entry])
  out.puts "compiled_iseq_count:   " + format_number(13, stats[:compiled_iseq_count])
  out.puts "compiled_blockid_count:" + format_number(13, stats[:compiled_blockid_count])
  out.puts "compiled_block_count:  " + format_number(13, stats[:compiled_block_count])
  out.puts "deleted_defer_block_count:" + format_number_pct(10, stats[:deleted_defer_block_count], stats[:compiled_block_count])
  if stats[:compiled_blockid_count] != 0
    out.puts "versions_per_block:    " + format_number(13, "%4.3f" % (stats[:compiled_block_count].fdiv(stats[:compiled_blockid_count])))
  end
  out.puts "max_inline_versions:   " + format_number(13, stats[:max_inline_versions])
  out.puts "compiled_branch_count: " + format_number(13, stats[:compiled_branch_count])
  out.puts "compile_time_ms:       " + format_number(13, stats[:compile_time_ns] / (1000 * 1000))
  out.puts "block_next_count:      " + format_number(13, stats[:block_next_count])
  out.puts "defer_count:           " + format_number(13, stats[:defer_count])
  out.puts "defer_empty_count:     " + format_number(13, stats[:defer_empty_count])

  out.puts "branch_insn_count:     " + format_number(13, stats[:branch_insn_count])
  out.puts "branch_known_count:    " + format_number_pct(13, stats[:branch_known_count], stats[:branch_insn_count])

  out.puts "freed_iseq_count:      " + format_number(13, stats[:freed_iseq_count])
  out.puts "invalidation_count:    " + format_number(13, stats[:invalidation_count])
  out.puts "inline_code_size:      " + format_number(13, stats[:inline_code_size])
  out.puts "outlined_code_size:    " + format_number(13, stats[:outlined_code_size])
  out.puts "code_region_size:      " + format_number(13, stats[:code_region_size])
  out.puts "code_region_overhead:  " + format_number_pct(13, code_region_overhead, stats[:code_region_size])

  out.puts "freed_code_size:       " + format_number(13, stats[:freed_code_size])
  out.puts "yjit_alloc_size:       " + format_number(13, stats[:yjit_alloc_size]) if stats.key?(:yjit_alloc_size)

  bytes_per_context = stats[:context_data_bytes].fdiv(stats[:num_contexts_encoded])
  out.puts "context_data_bytes:    " + format_number(13, stats[:context_data_bytes])
  out.puts "context_cache_bytes:   " + format_number(13, stats[:context_cache_bytes])
  out.puts "num_contexts_encoded:  " + format_number(13, stats[:num_contexts_encoded])
  out.puts "bytes_per_context:     " + ("%13.2f" % bytes_per_context)
  out.puts "context_cache_hit_rate:" + format_number_pct(13, stats[:context_cache_hits], stats[:num_contexts_encoded])

  out.puts "live_page_count:       " + format_number(13, stats[:live_page_count])
  out.puts "freed_page_count:      " + format_number(13, stats[:freed_page_count])
  out.puts "code_gc_count:         " + format_number(13, stats[:code_gc_count])
  out.puts "num_gc_obj_refs:       " + format_number(13, stats[:num_gc_obj_refs])
  out.puts "object_shape_count:    " + format_number(13, stats[:object_shape_count])
  out.puts "side_exit_count:       " + format_number(13, stats[:side_exit_count])
  out.puts "total_exit_count:      " + format_number(13, stats[:total_exit_count])
  out.puts "total_insns_count:     " + format_number(13, stats[:total_insns_count])
  out.puts "vm_insns_count:        " + format_number(13, stats[:vm_insns_count])
  out.puts "yjit_insns_count:      " + format_number(13, stats[:yjit_insns_count])
  out.puts "ratio_in_yjit:         " + ("%12.1f" % stats[:ratio_in_yjit]) + "%"
  out.puts "avg_len_in_yjit:       " + ("%13.1f" % stats[:avg_len_in_yjit])

  print_sorted_exit_counts(stats, out: out, prefix: "exit_")

  print_sorted_method_calls(stats[:cfunc_calls], stats[:num_send_cfunc], out: out, type: 'C')
  print_sorted_method_calls(stats[:iseq_calls], stats[:num_send_iseq], out: out, type: 'ISEQ')
end

._print_stats_reasons(stats, out) (private)

This method is for internal use only.

Print a summary of reasons for adverse performance events (e.g. exits)

[ GitHub ]

  
# File 'yjit.rb', line 289

def _print_stats_reasons(stats, out) # :nodoc:
  print_counters(stats, out: out, prefix: 'send_', prompt: 'method call fallback reasons: ')
  print_counters(stats, out: out, prefix: 'invokeblock_', prompt: 'invokeblock fallback reasons: ')
  print_counters(stats, out: out, prefix: 'invokesuper_', prompt: 'invokesuper fallback reasons: ')
  print_counters(stats, out: out, prefix: 'guard_send_', prompt: 'method call exit reasons: ')
  print_counters(stats, out: out, prefix: 'guard_invokeblock_', prompt: 'invokeblock exit reasons: ')
  print_counters(stats, out: out, prefix: 'guard_invokesuper_', prompt: 'invokesuper exit reasons: ')
  print_counters(stats, out: out, prefix: 'gbpp_', prompt: 'getblockparamproxy exit reasons: ')
  print_counters(stats, out: out, prefix: 'getivar_', prompt: 'getinstancevariable exit reasons:')
  print_counters(stats, out: out, prefix: 'setivar_', prompt: 'setinstancevariable exit reasons:')
  %w[
    branchif
    branchnil
    branchunless
    definedivar
    expandarray
    invokebuiltin
    jump
    leave
    objtostring
    opt_aref
    opt_aref_with
    opt_aset
    opt_case_dispatch
    opt_div
    opt_getconstant_path
    opt_minus
    opt_mod
    opt_mult
    opt_plus
    opt_succ
    setlocal
    splatkw
  ].each do |insn|
    print_counters(stats, out: out, prefix: "#{insn}_", prompt: "#{insn} exit reasons:", optional: true)
  end
  print_counters(stats, out: out, prefix: 'lshift_', prompt: 'left shift (opt_ltlt) exit reasons: ')
  print_counters(stats, out: out, prefix: 'rshift_', prompt: 'right shift (>>) exit reasons: ')
  print_counters(stats, out: out, prefix: 'invalidate_', prompt: 'invalidation reasons: ')
end

.add_yjit_hook(hook) (private)

This method is for internal use only.

Register a block to be called when YJIT is enabled

[ GitHub ]

  
# File 'yjit.rb', line 259

def add_yjit_hook(hook)
  @yjit_hooks << hook
end

.call_yjit_hooks (private)

This method is for internal use only.

Run YJIT hooks registered by RubyVM::YJIT.with_yjit

[ GitHub ]

  
# File 'yjit.rb', line 264

def call_yjit_hooks
  # Skip using builtin methods in Ruby if --yjit-c-builtin is given
  return if Primitive.yjit_c_builtin_p
  @yjit_hooks.each(&:call)
  @yjit_hooks.clear
end

.code_gc

Discard existing compiled code to reclaim memory and allow for recompilations in the future.

[ GitHub ]

  
# File 'yjit.rb', line 238

def self.code_gc
  Primitive.rb_yjit_code_gc
end

.disasm(iseq)

This method is for internal use only.

Produce disassembly for an iseq. This requires a –enable-yjit=dev build.

[ GitHub ]

  
# File 'yjit.rb', line 200

def self.disasm(iseq) # :nodoc:
  # If a method or proc is passed in, get its iseq
  iseq = RubyVM::InstructionSequence.of(iseq)

  if !self.enabled?
    warn(
      "YJIT needs to be enabled to produce disasm output, e.g.\n" +
      "ruby --yjit-call-threshold=1 my_script.rb (see doc/yjit/yjit.md)"
    )
    return nil
  end

  disasm_str = Primitive.rb_yjit_disasm_iseq(iseq)

  if !disasm_str
    warn(
      "YJIT disasm is only available when YJIT is built in dev mode, i.e.\n" +
      "./configure --enable-yjit=dev (see doc/yjit/yjit.md)\n"
    )
    return nil
  end

  # Produce the disassembly string
  # Include the YARV iseq disasm in the string for additional context
  iseq.disasm + "\n" + disasm_str
end

.dump_exit_locations(filename)

::Marshal dumps exit locations to the given filename.

Usage:

If –yjit-exit-locations is passed, a file named “yjit_exit_locations.dump” will automatically be generated.

If you want to collect traces manually, call dump_exit_locations directly.

Note that calling this in a script will generate stats after the dump is created, so the stats data may include exits from the dump itself.

In a script call:

at_exit do
  RubyVM::YJIT.dump_exit_locations("my_file.dump")
end

Then run the file with the following options:

ruby --yjit --yjit-trace-exits test.rb

Once the code is done running, use Stackprof to read the dump file. See Stackprof documentation for options.

[ GitHub ]

  
# File 'yjit.rb', line 161

def self.dump_exit_locations(filename)
  unless trace_exit_locations_enabled?
    raise ArgumentError, "--yjit-trace-exits must be enabled to use dump_exit_locations."
  end

  File.binwrite(filename, Marshal.dump(RubyVM::YJIT.exit_locations))
end

.enable(stats: false, log: false)

Enable YJIT compilation. stats option decides whether to enable YJIT stats or not. compilation_log decides whether to enable YJIT compilation logging or not.

stats:

  • false: Don’t enable stats.

  • true: Enable stats. Print stats at exit.

  • :quiet: Enable stats. Do not print stats at exit.

.log:

  • false: Don’t enable the log.

  • true: Enable the log. Print log at exit.

  • :quiet: Enable the log. Do not print log at exit.

[ GitHub ]

  
# File 'yjit.rb', line 48

def self.enable(stats: false, log: false)
  return false if enabled?
  at_exit { print_and_dump_stats } if stats
  call_yjit_hooks
  Primitive.rb_yjit_enable(stats, stats != :quiet, log, log != :quiet)
end

.exit_locations

This method is for internal use only.

If –yjit-trace-exits is enabled parse the hashes from Primitive.rb_yjit_get_exit_locations into a format readable by Stackprof. This will allow us to find the exact location of a side exit in YJIT based on the instruction that is exiting.

[ GitHub ]

  
# File 'yjit.rb', line 59

def self.exit_locations # :nodoc:
  return unless trace_exit_locations_enabled?

  results = Primitive.rb_yjit_get_exit_locations
  raw_samples = results[:raw].dup
  line_samples = results[:lines].dup
  frames = results[:frames].dup
  samples_count = 0

  # Loop through the instructions and set the frame hash with the data.
  # We use nonexistent.def for the file name, otherwise insns.def will be displayed
  # and that information isn't useful in this context.
  RubyVM::INSTRUCTION_NAMES.each_with_index do |name, frame_id|
    frame_hash = { samples: 0, total_samples: 0, edges: {}, name: name, file: "nonexistent.def", line: nil, lines: {} }
    results[:frames][frame_id] = frame_hash
    frames[frame_id] = frame_hash
  end

  # Loop through the raw_samples and build the hashes for StackProf.
  # The loop is based off an example in the StackProf documentation and therefore
  # this functionality can only work with that library.
  #
  # Raw Samples:
  # [ length, frame1, frame2, frameN, ..., instruction, count
  #
  # Line Samples
  # [ length, line_1, line_2, line_n, ..., dummy value, count
  i = 0
  while i < raw_samples.length
    stack_length = raw_samples[i]
    i += 1 # consume the stack length

    sample_count = raw_samples[i + stack_length]

    prev_frame_id = nil
    stack_length.times do |idx|
      idx += i
      frame_id = raw_samples[idx]

      if prev_frame_id
        prev_frame = frames[prev_frame_id]
        prev_frame[:edges][frame_id] ||= 0
        prev_frame[:edges][frame_id] += sample_count
      end

      frame_info = frames[frame_id]
      frame_info[:total_samples] += sample_count

      frame_info[:lines][line_samples[idx]] ||= [0, 0]
      frame_info[:lines][line_samples[idx]][0] += sample_count

      prev_frame_id = frame_id
    end

    i += stack_length # consume the stack

    top_frame_id = prev_frame_id
    top_frame_line = 1

    frames[top_frame_id][:samples] += sample_count
    frames[top_frame_id][:lines] ||= {}
    frames[top_frame_id][:lines][top_frame_line] ||= [0, 0]
    frames[top_frame_id][:lines][top_frame_line][1] += sample_count

    samples_count += sample_count
    i += 1
  end

  results[:samples] = samples_count
  # Set missed_samples and gc_samples to 0 as their values
  # don't matter to us in this context.
  results[:missed_samples] = 0
  results[:gc_samples] = 0
  results
end

.format_number(pad, number) (private)

This method is for internal use only.

Format large numbers with comma separators for readability

[ GitHub ]

  
# File 'yjit.rb', line 511

def format_number(pad, number) # :nodoc:
  s = number.to_s
  i = s.index('.') || s.size
  s.insert(i -= 3, ',') while i > 3
  s.rjust(pad, ' ')
end

.format_number_pct(pad, number, total) (private)

This method is for internal use only.

Format a number along with a percentage over a total value

[ GitHub ]

  
# File 'yjit.rb', line 519

def format_number_pct(pad, number, total) # :nodoc:
  padded_count = format_number(pad, number)
  percentage = number.fdiv(total) * 100
  formatted_pct = "%4.1f%%" % percentage
  "#{padded_count} (#{formatted_pct})"
end

.insns_compiled(iseq)

This method is for internal use only.

Produce a list of instructions compiled by YJIT for an iseq

[ GitHub ]

  
# File 'yjit.rb', line 228

def self.insns_compiled(iseq) # :nodoc:
  return nil unless self.enabled?

  # If a method or proc is passed in, get its iseq
  iseq = RubyVM::InstructionSequence.of(iseq)
  Primitive.rb_yjit_insns_compiled(iseq)
end

.log

Return an array of log entries. Return nil when option is not passed or unavailable.

[ GitHub ]

  
# File 'yjit.rb', line 191

def self.log
  return nil unless log_enabled?

  Primitive.rb_yjit_get_log.map do |timestamp, path|
    [Time.at(timestamp), path]
  end
end

.reset_stats!

Discard statistics collected for –yjit-stats.

[ GitHub ]

  
# File 'yjit.rb', line 32

def self.reset_stats!
  Primitive.rb_yjit_reset_stats_bang
end

.runtime_stats(key = nil)

Return a hash for statistics generated for the –yjit-stats command line option. Return nil when option is not passed or unavailable. If a symbol argument is provided, return only the value for the named stat. If any other type is provided, raises ::TypeError.

Raises:

[ GitHub ]

  
# File 'yjit.rb', line 173

def self.runtime_stats(key = nil)
  raise TypeError, "non-symbol given" unless key.nil? || Symbol === key

  Primitive.rb_yjit_get_stats(key)
end

.simulate_oom!

This method is for internal use only.
[ GitHub ]

  
# File 'yjit.rb', line 242

def self.simulate_oom! # :nodoc:
  Primitive.rb_yjit_simulate_oom_bang
end

.stats_string

Format and print out counters as a ::String. This returns a non-empty content only when –yjit-stats is enabled.

[ GitHub ]

  
# File 'yjit.rb', line 181

def self.stats_string
  # Lazily require StringIO to avoid breaking miniruby
  require 'stringio'
  strio = StringIO.new
  _print_stats(out: strio)
  strio.string
end