summaryrefslogtreecommitdiff
path: root/lib/ruby_vm/rjit/stats.rb
blob: 7e353c698e6c4ebf7984788da9714514b5078a9e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# frozen_string_literal: true
module RubyVM::RJIT
  # Return a Hash for \RJIT statistics. \--rjit-stats makes more information available.
  def self.runtime_stats
    stats = {}

    # Insn exits
    INSNS.each_value do |insn|
      exits = C.rjit_insn_exits[insn.bin]
      if exits > 0
        stats[:"exit_#{insn.name}"] = exits
      end
    end

    # Runtime stats
    C.rb_rjit_runtime_counters.members.each do |member|
      stats[member] = C.rb_rjit_counters.public_send(member)
    end
    stats[:vm_insns_count] = C.rb_vm_insns_count

    # Other stats are calculated here
    stats[:side_exit_count] = stats.select { |name, _count| name.start_with?('exit_') }.sum(&:last)
    if stats[:vm_insns_count] > 0
      retired_in_rjit = stats[:rjit_insns_count] - stats[:side_exit_count]
      stats[:total_insns_count] = retired_in_rjit + stats[:vm_insns_count]
      stats[:ratio_in_rjit] = 100.0 * retired_in_rjit / stats[:total_insns_count]
    else
      stats.delete(:vm_insns_count)
    end

    stats
  end

  # :nodoc: all
  class << self
    private

    # --yjit-stats at_exit
    def print_stats
      stats = runtime_stats
      $stderr.puts("***RJIT: Printing RJIT statistics on exit***")

      print_counters(stats, prefix: 'send_', prompt: 'method call exit reasons')
      print_counters(stats, prefix: 'invokeblock_', prompt: 'invokeblock exit reasons')
      print_counters(stats, prefix: 'invokesuper_', prompt: 'invokesuper exit reasons')
      print_counters(stats, prefix: 'getblockpp_', prompt: 'getblockparamproxy exit reasons')
      print_counters(stats, prefix: 'getivar_', prompt: 'getinstancevariable exit reasons')
      print_counters(stats, prefix: 'setivar_', prompt: 'setinstancevariable exit reasons')
      print_counters(stats, prefix: 'optaref_', prompt: 'opt_aref exit reasons')
      print_counters(stats, prefix: 'optgetconst_', prompt: 'opt_getconstant_path exit reasons')
      print_counters(stats, prefix: 'expandarray_', prompt: 'expandarray exit reasons')

      $stderr.puts "compiled_block_count:  #{format_number(13, stats[:compiled_block_count])}"
      $stderr.puts "side_exit_count:       #{format_number(13, stats[:side_exit_count])}"
      $stderr.puts "total_insns_count:     #{format_number(13, stats[:total_insns_count])}" if stats.key?(:total_insns_count)
      $stderr.puts "vm_insns_count:        #{format_number(13, stats[:vm_insns_count])}" if stats.key?(:vm_insns_count)
      $stderr.puts "rjit_insns_count:      #{format_number(13, stats[:rjit_insns_count])}"
      $stderr.puts "ratio_in_rjit:         #{format('%12.1f', stats[:ratio_in_rjit])}%" if stats.key?(:ratio_in_rjit)

      print_exit_counts(stats)
    end

    def print_counters(stats, prefix:, prompt:)
      $stderr.puts("#{prompt}: ")
      counters = stats.filter { |key, _| key.start_with?(prefix) }
      counters.filter! { |_, value| value != 0 }
      counters.transform_keys! { |key| key.to_s.delete_prefix(prefix) }

      if counters.empty?
        $stderr.puts("    (all relevant counters are zero)")
        return
      end

      counters = counters.to_a
      counters.sort_by! { |(_, counter_value)| counter_value }
      longest_name_length = counters.max_by { |(name, _)| name.length }.first.length
      total = counters.sum { |(_, counter_value)| counter_value }

      counters.reverse_each do |(name, value)|
        percentage = value.fdiv(total) * 100
        $stderr.printf("    %*s %s (%4.1f%%)\n", longest_name_length, name, format_number(10, value), percentage)
      end
    end

    def print_exit_counts(stats, how_many: 20, padding: 2)
      exits = stats.filter_map { |name, count| [name.to_s.delete_prefix('exit_'), count] if name.start_with?('exit_') }.to_h
      return if exits.empty?

      top_exits = exits.sort_by { |_name, count| -count }.first(how_many).to_h
      total_exits = exits.values.sum
      $stderr.puts "Top-#{top_exits.size} most frequent exit ops (#{format("%.1f", 100.0 * top_exits.values.sum / total_exits)}% of exits):"

      name_width  = top_exits.map { |name, _count| name.length }.max + padding
      count_width = top_exits.map { |_name, count| format_number(10, count).length }.max + padding
      top_exits.each do |name, count|
        ratio = 100.0 * count / total_exits
        $stderr.puts "#{format("%#{name_width}s", name)}: #{format_number(count_width, count)} (#{format('%4.1f', ratio)}%)"
      end
    end

    # Format large numbers with comma separators for readability
    def format_number(pad, number)
      integer, decimal = number.to_s.split('.')
      d_groups = integer.chars.reverse.each_slice(3)
      with_commas = d_groups.map(&:join).join(',').reverse
      [with_commas, decimal].compact.join('.').rjust(pad, ' ')
    end

    # --yjit-trace-exits at_exit
    def dump_trace_exits
      filename = "#{Dir.pwd}/rjit_exit_locations.dump"
      File.binwrite(filename, Marshal.dump(exit_traces))
      $stderr.puts("RJIT exit locations dumped to:\n#{filename}")
    end

    # Convert rb_rjit_raw_samples and rb_rjit_line_samples into a StackProf format.
    def exit_traces
      results = C.rjit_exit_traces
      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] + 1
        i += 1 # consume the 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] += 1
          end

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

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

          prev_frame_id = frame_id
        end

        i += stack_length # consume the stack

        top_frame_id = prev_frame_id
        top_frame_line = 1

        sample_count = raw_samples[i]

        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
  end
end