Replace to RBOOL macro
[ruby-80x24.org.git] / yjit.rb
blob09ed3faaf3b835de6d4730951ab90a7f12f665b4
1 # frozen_string_literal: true
3 # This module allows for introspection of YJIT, CRuby's experimental in-process
4 # just-in-time compiler. This module exists only to help develop YJIT, as such,
5 # everything in the module is highly implementation specific and comes with no
6 # API stability guarantee whatsoever.
8 # This module may not exist if YJIT does not support the particular platform
9 # for which CRuby is built. There is also no API stability guarantee as to in
10 # what situations this module is defined.
11 module RubyVM::YJIT
12   if defined?(Disasm)
13     def self.disasm(iseq, tty: $stdout && $stdout.tty?)
14       iseq = RubyVM::InstructionSequence.of(iseq)
16       blocks = blocks_for(iseq)
17       return if blocks.empty?
19       str = String.new
20       str << iseq.disasm
21       str << "\n"
23       # Sort the blocks by increasing addresses
24       sorted_blocks = blocks.sort_by(&:address)
26       highlight = ->(str) {
27         if tty
28           "\x1b[1m#{str}\x1b[0m"
29         else
30           str
31         end
32       }
34       cs = Disasm.new
35       sorted_blocks.each_with_index do |block, i|
36         str << "== BLOCK #{i+1}/#{blocks.length}: #{block.code.length} BYTES, ISEQ RANGE [#{block.iseq_start_index},#{block.iseq_end_index}) ".ljust(80, "=")
37         str << "\n"
39         comments = comments_for(block.address, block.address + block.code.length)
40         comment_idx = 0
41         cs.disasm(block.code, block.address).each do |i|
42           while (comment = comments[comment_idx]) && comment.address <= i.address
43             str << "  ; #{highlight.call(comment.comment)}\n"
44             comment_idx += 1
45           end
47           str << sprintf(
48             "  %<address>08x:  %<instruction>s\t%<details>s\n",
49             address: i.address,
50             instruction: i.mnemonic,
51             details: i.op_str
52           )
53         end
54       end
56       block_sizes = blocks.map { |block| block.code.length }
57       total_bytes = block_sizes.sum
58       str << "\n"
59       str << "Total code size: #{total_bytes} bytes"
60       str << "\n"
62       str
63     end
65     def self.comments_for(start_address, end_address)
66       Primitive.comments_for(start_address, end_address)
67     end
69     def self.graphviz_for(iseq)
70       iseq = RubyVM::InstructionSequence.of(iseq)
71       cs = Disasm.new
73       highlight = ->(comment) { "<b>#{comment}</b>" }
74       linebreak = "<br align=\"left\"/>\n"
76       buff = +''
77       blocks = blocks_for(iseq).sort_by(&:id)
78       buff << "digraph g {\n"
80       # Write the iseq info as a legend
81       buff << "  legend [shape=record fontsize=\"30\" fillcolor=\"lightgrey\" style=\"filled\"];\n"
82       buff << "  legend [label=\"{ Instruction Disassembly For: | {#{iseq.base_label}@#{iseq.absolute_path}:#{iseq.first_lineno}}}\"];\n"
84       # Subgraph contains disassembly
85       buff << "  subgraph disasm {\n"
86       buff << "  node [shape=record fontname=\"courier\"];\n"
87       buff << "  edge [fontname=\"courier\" penwidth=3];\n"
88       blocks.each do |block|
89         disasm = disasm_block(cs, block, highlight)
91         # convert newlines to breaks that graphviz understands
92         disasm.gsub!(/\n/, linebreak)
94         # strip leading whitespace
95         disasm.gsub!(/^\s+/, '')
97         buff << "b#{block.id} [label=<#{disasm}>];\n"
98         buff << block.outgoing_ids.map { |id|
99           next_block = blocks.bsearch { |nb| id <=> nb.id }
100           if next_block.address == (block.address + block.code.length)
101             "b#{block.id} -> b#{id}[label=\"Fall\"];"
102           else
103             "b#{block.id} -> b#{id}[label=\"Jump\" style=dashed];"
104           end
105         }.join("\n")
106         buff << "\n"
107       end
108       buff << "  }"
109       buff << "}"
110       buff
111     end
113     def self.disasm_block(cs, block, highlight)
114       comments = comments_for(block.address, block.address + block.code.length)
115       comment_idx = 0
116       str = +''
117       cs.disasm(block.code, block.address).each do |i|
118         while (comment = comments[comment_idx]) && comment.address <= i.address
119           str << "  ; #{highlight.call(comment.comment)}\n"
120           comment_idx += 1
121         end
123         str << sprintf(
124           "  %<address>08x:  %<instruction>s\t%<details>s\n",
125           address: i.address,
126           instruction: i.mnemonic,
127           details: i.op_str
128         )
129       end
130       str
131     end
132   end
134   # Return a hash for statistics generated for the --yjit-stats command line option.
135   # Return nil when option is not passed or unavailable.
136   def self.runtime_stats
137     # defined in yjit_iface.c
138     Primitive.get_yjit_stats
139   end
141   # Discard statistics collected for --yjit-stats.
142   def self.reset_stats!
143     # defined in yjit_iface.c
144     Primitive.reset_stats_bang
145   end
147   def self.stats_enabled?
148     Primitive.yjit_stats_enabled_p
149   end
151   def self.enabled?
152     Primitive.cexpr! 'RBOOL(rb_yjit_enabled_p())'
153   end
155   def self.simulate_oom!
156     Primitive.simulate_oom_bang
157   end
159   # Avoid calling a method here to not interfere with compilation tests
160   if Primitive.yjit_stats_enabled_p
161     at_exit { _print_stats }
162   end
164   class << self
165     private
167     # Format and print out counters
168     def _print_stats
169       stats = runtime_stats
170       return unless stats
172       $stderr.puts("***YJIT: Printing YJIT statistics on exit***")
174       print_counters(stats, prefix: 'send_', prompt: 'method call exit reasons: ')
175       print_counters(stats, prefix: 'invokesuper_', prompt: 'invokesuper exit reasons: ')
176       print_counters(stats, prefix: 'leave_', prompt: 'leave exit reasons: ')
177       print_counters(stats, prefix: 'gbpp_', prompt: 'getblockparamproxy exit reasons: ')
178       print_counters(stats, prefix: 'getivar_', prompt: 'getinstancevariable exit reasons:')
179       print_counters(stats, prefix: 'setivar_', prompt: 'setinstancevariable exit reasons:')
180       print_counters(stats, prefix: 'oaref_', prompt: 'opt_aref exit reasons: ')
181       print_counters(stats, prefix: 'expandarray_', prompt: 'expandarray exit reasons: ')
182       print_counters(stats, prefix: 'opt_getinlinecache_', prompt: 'opt_getinlinecache exit reasons: ')
183       print_counters(stats, prefix: 'invalidate_', prompt: 'invalidation reasons: ')
185       side_exits = total_exit_count(stats)
186       total_exits = side_exits + stats[:leave_interp_return]
188       # Number of instructions that finish executing in YJIT.
189       # See :count-placement: about the subtraction.
190       retired_in_yjit = stats[:exec_instruction] - side_exits
192       # Average length of instruction sequences executed by YJIT
193       avg_len_in_yjit = retired_in_yjit.to_f / total_exits
195       # Proportion of instructions that retire in YJIT
196       total_insns_count = retired_in_yjit + stats[:vm_insns_count]
197       yjit_ratio_pct = 100.0 * retired_in_yjit.to_f / total_insns_count
199       # Number of failed compiler invocations
200       compilation_failure = stats[:compilation_failure]
202       $stderr.puts "bindings_allocations:  " + ("%10d" % stats[:binding_allocations])
203       $stderr.puts "bindings_set:          " + ("%10d" % stats[:binding_set])
204       $stderr.puts "compilation_failure:   " + ("%10d" % compilation_failure) if compilation_failure != 0
205       $stderr.puts "compiled_iseq_count:   " + ("%10d" % stats[:compiled_iseq_count])
206       $stderr.puts "compiled_block_count:  " + ("%10d" % stats[:compiled_block_count])
207       $stderr.puts "invalidation_count:    " + ("%10d" % stats[:invalidation_count])
208       $stderr.puts "constant_state_bumps:  " + ("%10d" % stats[:constant_state_bumps])
209       $stderr.puts "inline_code_size:      " + ("%10d" % stats[:inline_code_size])
210       $stderr.puts "outlined_code_size:    " + ("%10d" % stats[:outlined_code_size])
212       $stderr.puts "total_exit_count:      " + ("%10d" % total_exits)
213       $stderr.puts "total_insns_count:     " + ("%10d" % total_insns_count)
214       $stderr.puts "vm_insns_count:        " + ("%10d" % stats[:vm_insns_count])
215       $stderr.puts "yjit_insns_count:      " + ("%10d" % stats[:exec_instruction])
216       $stderr.puts "ratio_in_yjit:         " + ("%9.1f" % yjit_ratio_pct) + "%"
217       $stderr.puts "avg_len_in_yjit:       " + ("%10.1f" % avg_len_in_yjit)
219       print_sorted_exit_counts(stats, prefix: "exit_")
220     end
222     def print_sorted_exit_counts(stats, prefix:, how_many: 20, left_pad: 4)
223       exits = []
224       stats.each do |k, v|
225         if k.start_with?(prefix)
226           exits.push [k.to_s.delete_prefix(prefix), v]
227         end
228       end
230       exits = exits.sort_by { |name, count| -count }[0...how_many]
231       total_exits = total_exit_count(stats)
233       top_n_total = exits.map { |name, count| count }.sum
234       top_n_exit_pct = 100.0 * top_n_total / total_exits
236       $stderr.puts "Top-#{how_many} most frequent exit ops (#{"%.1f" % top_n_exit_pct}% of exits):"
238       longest_insn_name_len = exits.map { |name, count| name.length }.max
239       exits.each do |name, count|
240         padding = longest_insn_name_len + left_pad
241         padded_name = "%#{padding}s" % name
242         padded_count = "%10d" % count
243         percent = 100.0 * count / total_exits
244         formatted_percent = "%.1f" % percent
245         $stderr.puts("#{padded_name}: #{padded_count} (#{formatted_percent}%)" )
246       end
247     end
249     def total_exit_count(stats, prefix: "exit_")
250       total = 0
251       stats.each do |k,v|
252         total += v if k.start_with?(prefix)
253       end
254       total
255     end
257     def print_counters(counters, prefix:, prompt:)
258       $stderr.puts(prompt)
259       counters = counters.filter { |key, _| key.start_with?(prefix) }
260       counters.filter! { |_, value| value != 0 }
261       counters.transform_keys! { |key| key.to_s.delete_prefix(prefix) }
263       if counters.empty?
264         $stderr.puts("    (all relevant counters are zero)")
265         return
266       end
268       counters = counters.to_a
269       counters.sort_by! { |(_, counter_value)| counter_value }
270       longest_name_length = counters.max_by { |(name, _)| name.length }.first.length
271       total = counters.sum { |(_, counter_value)| counter_value }
273       counters.reverse_each do |(name, value)|
274         percentage = value.fdiv(total) * 100
275         $stderr.printf("    %*s %10d (%4.1f%%)\n", longest_name_length, name, value, percentage);
276       end
277     end
278   end