mirror of
https://github.com/ruby/ruby.git
synced 2026-08-01 02:20:48 +08:00
167 lines
5.5 KiB
Ruby
Executable File
167 lines
5.5 KiB
Ruby
Executable File
#!/usr/bin/env ruby
|
|
|
|
class JITPerf
|
|
INTERPRETER_SYMBOLS = [
|
|
# rb_* entry points
|
|
"rb_call0",
|
|
"rb_funcallv_scope",
|
|
"rb_yield",
|
|
|
|
# VM helpers without rb_vm_/vm_ prefixes
|
|
"callable_method_entry_or_negative",
|
|
"invoke_block_from_c_bh",
|
|
"setup_parameters_complex",
|
|
].freeze
|
|
|
|
def initialize
|
|
@total_cycles = 0
|
|
@category_cycles = Hash.new(0)
|
|
@detailed_category_cycles = Hash.new { |hash, category| hash[category] = Hash.new(0) }
|
|
@categories = {}
|
|
end
|
|
|
|
def read(path)
|
|
File.foreach(path).with_index(1) do |line, lineno|
|
|
next if line.strip.empty?
|
|
|
|
process_event(parse_line(line))
|
|
rescue ArgumentError => error
|
|
abort "#{path}:#{lineno}: #{error.message}"
|
|
end
|
|
rescue SystemCallError => error
|
|
abort "#{path}: #{error.message}"
|
|
end
|
|
|
|
def print_report
|
|
return if @total_cycles == 0
|
|
|
|
puts "Aggregated Event Data:"
|
|
puts format("%-20s %-50s %20s %15s", "[dso]", "[symbol or category]", "[top-most cycle ratio]", "[num cycles]")
|
|
|
|
most_common(@category_cycles).each do |category, cycles|
|
|
ratio = cycles.to_f / @total_cycles * 100
|
|
dsos = @detailed_category_cycles[category].each_key.map(&:first).uniq
|
|
dso_display = dsos.length == 1 ? dsos.first : "Multiple DSOs"
|
|
puts format("%-20s %-50s %20.2f%% %15d", dso_display, truncate_symbol(category), ratio, cycles)
|
|
end
|
|
|
|
most_common(@category_cycles).each do |category, _cycles|
|
|
next unless @categories.key?(category)
|
|
|
|
symbols = @detailed_category_cycles[category]
|
|
category_total = symbols.values.sum
|
|
category_ratio = category_total.to_f / @total_cycles * 100
|
|
|
|
puts
|
|
puts format("Category: %s (%.2f%%)", category, category_ratio)
|
|
puts format("%-20s %-50s %20s %15s", "[dso]", "[symbol]", "[top-most cycle ratio]", "[num cycles]")
|
|
|
|
most_common(symbols).each do |(dso, symbol), cycles|
|
|
symbol_ratio = cycles.to_f / category_total * 100
|
|
puts format("%-20s %-50s %20.2f%% %15d", dso, truncate_symbol(symbol), symbol_ratio, cycles)
|
|
end
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def parse_line(line)
|
|
# Example:
|
|
# ruby 78207 3482.848465: 1212775 cpu_core/cycles:P/: 5c0333f682e1 [JIT] getlocal_WC_0+0x0 (/tmp/perf-78207.map)
|
|
#
|
|
# Split into command, pid, timestamp, period, event, ip, and the remaining
|
|
# "symbol (dso)" text. The final field is kept intact because symbols can
|
|
# contain spaces.
|
|
fields = line.split(nil, 7)
|
|
raise ArgumentError, "unexpected perf script line: #{line.chomp}" if fields.length < 7
|
|
|
|
begin
|
|
period = Integer(fields[3])
|
|
rescue ArgumentError, TypeError
|
|
raise ArgumentError, "unexpected sample period in perf script line: #{line.chomp}"
|
|
end
|
|
|
|
# Parse the trailing "symbol (dso)" text from the right, then drop the
|
|
# instruction offset after "+" from the symbol name.
|
|
dso_start = fields[6].rindex(" (")
|
|
raise ArgumentError, "missing dso in perf script line: #{line.chomp}" unless dso_start
|
|
|
|
dso_with_suffix = fields[6][(dso_start + 2)..-1]
|
|
dso_end = dso_with_suffix.index(")")
|
|
raise ArgumentError, "missing dso terminator in perf script line: #{line.chomp}" unless dso_end
|
|
|
|
symbol = fields[6][0...dso_start].split("+", 2).first
|
|
dso = dso_with_suffix[0...dso_end]
|
|
|
|
[dso, symbol, period]
|
|
end
|
|
|
|
def process_event(event)
|
|
full_dso, symbol, cycles = event
|
|
dso = File.basename(full_dso || "Unknown_dso")
|
|
symbol ||= "[unknown]"
|
|
|
|
@total_cycles += cycles
|
|
|
|
category = categorize_symbol(dso, symbol)
|
|
@category_cycles[category] += cycles
|
|
@detailed_category_cycles[category][[dso, symbol]] += cycles
|
|
|
|
@categories[category] = true if category.start_with?("[") && category.end_with?("]")
|
|
end
|
|
|
|
def truncate_symbol(symbol, max_length = 50)
|
|
symbol.length <= max_length ? symbol : "#{symbol[0...(max_length - 3)]}..."
|
|
end
|
|
|
|
def categorize_symbol(dso, symbol)
|
|
if dso == "sqlite3_native.so"
|
|
"[sqlite3]"
|
|
elsif symbol.include?("SHA256")
|
|
"[sha256]"
|
|
elsif symbol.start_with?("[JIT] gen_send")
|
|
"[JIT send]"
|
|
elsif symbol.start_with?("[JIT]") || symbol.start_with?("ZJIT: ") || dso.start_with?("perf-")
|
|
"[JIT code]"
|
|
elsif symbol.include?("::") || symbol.start_with?("_ZN4yjit") || symbol.start_with?("_ZN4zjit")
|
|
"[JIT compile]"
|
|
elsif symbol.start_with?("rb_vm_") || symbol.start_with?("vm_") || INTERPRETER_SYMBOLS.include?(symbol)
|
|
"[interpreter]"
|
|
elsif symbol.start_with?("rb_hash_") || symbol.start_with?("hash_")
|
|
"[rb_hash_*]"
|
|
elsif symbol.start_with?("rb_ary_") || symbol.start_with?("ary_")
|
|
"[rb_ary_*]"
|
|
elsif symbol.start_with?("rb_str_") || symbol.start_with?("str_")
|
|
"[rb_str_*]"
|
|
elsif symbol.start_with?("rb_sym") || symbol.start_with?("sym_")
|
|
"[rb_sym_*]"
|
|
elsif symbol.start_with?("rb_st_") || symbol.start_with?("st_")
|
|
"[rb_st_*]"
|
|
elsif symbol.start_with?("rb_ivar_") || symbol.include?("shape")
|
|
"[ivars]"
|
|
elsif symbol.include?("match") || symbol.start_with?("rb_reg") || symbol.start_with?("onig")
|
|
"[regexp]"
|
|
elsif symbol.include?("alloc") || symbol.include?("free") || symbol.include?("gc")
|
|
"[GC]"
|
|
elsif symbol.include?("pthread") && symbol.include?("lock")
|
|
"[pthread lock]"
|
|
else
|
|
symbol
|
|
end
|
|
end
|
|
|
|
def most_common(counter)
|
|
counter.each.with_index
|
|
.sort_by { |((_key, cycles), index)| [-cycles, index] }
|
|
.map(&:first)
|
|
end
|
|
end
|
|
|
|
if ARGV.length != 1
|
|
abort "Usage: #{File.basename($PROGRAM_NAME)} <perf-script-output>"
|
|
end
|
|
|
|
jit_perf = JITPerf.new
|
|
jit_perf.read(ARGV[0])
|
|
jit_perf.print_report
|