lib/stackprof/report.rb in stackprof-0.1.0 vs lib/stackprof/report.rb in stackprof-0.2.0

- old
+ new

@@ -1,71 +1,279 @@ require 'pp' +require 'digest/md5' module StackProf class Report def initialize(data) @data = data + + frames = {} + @data[:frames].each{ |k,v| frames[k.to_s] = v } + @data[:frames] = frames end + attr_reader :data - def frames - @data[:frames].sort_by{ |iseq, stats| -stats[:samples] } + def frames(sort_by_total=false) + Hash[ *@data[:frames].sort_by{ |iseq, stats| -stats[sort_by_total ? :total_samples : :samples] }.flatten(1) ] end + def normalized_frames + id2hash = {} + @data[:frames].each do |frame, info| + id2hash[frame.to_s] = info[:hash] = Digest::MD5.hexdigest("#{info[:name]}#{info[:file]}#{info[:line]}") + end + @data[:frames].inject(Hash.new) do |hash, (frame, info)| + info = hash[id2hash[frame.to_s]] = info.dup + info[:edges] = info[:edges].inject(Hash.new){ |edges, (edge, weight)| edges[id2hash[edge.to_s]] = weight; edges } if info[:edges] + hash + end + end + + def version + @data[:version] + end + + def modeline + "#{@data[:mode]}(#{@data[:interval]})" + end + def overall_samples @data[:samples] end + def max_samples + @data[:max_samples] ||= frames.max_by{ |addr, frame| frame[:samples] }.last[:samples] + end + + def files + @data[:files] ||= @data[:frames].inject(Hash.new) do |hash, (addr, frame)| + if file = frame[:file] and lines = frame[:lines] + hash[file] ||= Hash.new + lines.each do |line, weight| + hash[file][line] = add_lines(hash[file][line], weight) + end + end + hash + end + end + + def add_lines(a, b) + return b if a.nil? + return a+b if a.is_a? Fixnum + return [ a[0], a[1]+b ] if b.is_a? Fixnum + [ a[0]+b[0], a[1]+b[1] ] + end + def print_debug pp @data end - def print_graphviz - f = STDOUT + def print_graphviz(filter = nil, f = STDOUT) + if filter + mark_stack = [] + list = frames + list.each{ |addr, frame| mark_stack << addr if frame[:name] =~ filter } + while addr = mark_stack.pop + frame = list[addr] + unless frame[:marked] + $stderr.puts frame[:edges].inspect + mark_stack += frame[:edges].map{ |addr, weight| addr.to_s if list[addr.to_s][:total_samples] <= weight*1.2 }.compact if frame[:edges] + frame[:marked] = true + end + end + list = list.select{ |addr, frame| frame[:marked] } + list.each{ |addr, frame| frame[:edges] && frame[:edges].delete_if{ |k,v| list[k.to_s].nil? } } + list + else + list = frames + end + f.puts "digraph profile {" - frames.each do |frame, info| + list.each do |frame, info| call, total = info.values_at(:samples, :total_samples) sample = '' sample << "#{call} (%2.1f%%)\\rof " % (call*100.0/overall_samples) if call < total sample << "#{total} (%2.1f%%)\\r" % (total*100.0/overall_samples) - size = (1.0 * call / overall_samples) * 28 + 10 + fontsize = (1.0 * call / max_samples) * 28 + 10 + size = (1.0 * total / overall_samples) * 2.0 + 0.5 - f.puts " #{frame} [size=#{size}] [fontsize=#{size}] [shape=box] [label=\"#{info[:name]}\\n#{sample}\"];" + f.puts " #{frame} [size=#{size}] [fontsize=#{fontsize}] [penwidth=\"#{size}\"] [shape=box] [label=\"#{info[:name]}\\n#{sample}\"];" if edges = info[:edges] edges.each do |edge, weight| - size = (1.0 * weight / overall_samples) * 28 - f.puts " #{frame} -> #{edge} [label=\"#{weight}\"];" + size = (1.0 * weight / overall_samples) * 2.0 + 0.5 + f.puts " #{frame} -> #{edge} [label=\"#{weight}\"] [weight=\"#{weight}\"] [penwidth=\"#{size}\"];" end end end f.puts "}" end - def print_text - printf "% 10s (pct) % 10s (pct) FRAME\n" % ["TOTAL", "SAMPLES"] - frames.each do |frame, info| + def print_text(sort_by_total=false, limit=nil, f = STDOUT) + f.puts "==================================" + f.printf " Mode: #{modeline}\n" + f.printf " Samples: #{@data[:samples]} (%.2f%% miss rate)\n", 100.0*@data[:missed_samples]/(@data[:missed_samples]+@data[:samples]) + f.printf " GC: #{@data[:gc_samples]} (%.2f%%)\n", 100.0*@data[:gc_samples]/@data[:samples] + f.puts "==================================" + f.printf "% 10s (pct) % 10s (pct) FRAME\n" % ["TOTAL", "SAMPLES"] + list = frames(sort_by_total) + list = list.first(limit) if limit + list.each do |frame, info| call, total = info.values_at(:samples, :total_samples) - printf "% 10d % 8s % 10d % 8s %s\n", total, "(%2.1f%%)" % (total*100.0/overall_samples), call, "(%2.1f%%)" % (call*100.0/overall_samples), info[:name] + f.printf "% 10d % 8s % 10d % 8s %s\n", total, "(%2.1f%%)" % (total*100.0/overall_samples), call, "(%2.1f%%)" % (call*100.0/overall_samples), info[:name] end end - def print_source(name) + def print_callgrind(f = STDOUT) + f.puts "version: 1" + f.puts "creator: stackprof" + f.puts "pid: 0" + f.puts "cmd: ruby" + f.puts "part: 1" + f.puts "desc: mode: #{modeline}" + f.puts "desc: missed: #{@data[:missed_samples]})" + f.puts "positions: line" + f.puts "events: Instructions" + f.puts "summary: #{@data[:samples]}" + + list = frames + list.each do |addr, frame| + f.puts "fl=#{frame[:file]}" + f.puts "fn=#{frame[:name]}" + frame[:lines].each do |line, weight| + f.puts "#{line} #{weight.is_a?(Array) ? weight[1] : weight}" + end if frame[:lines] + frame[:edges].each do |edge, weight| + oframe = list[edge.to_s] + f.puts "cfl=#{oframe[:file]}" unless oframe[:file] == frame[:file] + f.puts "cfn=#{oframe[:name]}" + f.puts "calls=#{weight} #{frame[:line] || 0}\n#{oframe[:line] || 0} #{weight}" + end if frame[:edges] + f.puts + end + + f.puts "totals: #{@data[:samples]}" + end + + def print_method(name, f = STDOUT) name = /#{Regexp.escape name}/ unless Regexp === name frames.each do |frame, info| next unless info[:name] =~ name file, line = info.values_at(:file, :line) + line ||= 1 - maxline = info[:lines] ? info[:lines].keys.max : line + 5 - printf "%s (%s:%d)\n", info[:name], file, line - lines = info[:lines] - source = File.readlines(file).each_with_index do |code, i| - next unless (line-1..maxline).include?(i) - if lines and samples = lines[i+1] - printf "% 5d % 7s / % 7s | % 5d | %s", samples, "(%2.1f%%" % (100.0*samples/overall_samples), "%2.1f%%)" % (100.0*samples/info[:samples]), i+1, code + maxline = lines ? lines.keys.max : line + 5 + f.printf "%s (%s:%d)\n", info[:name], file, line + f.printf " samples: % 5d self (%2.1f%%) / % 5d total (%2.1f%%)\n", info[:samples], 100.0*info[:samples]/overall_samples, info[:total_samples], 100.0*info[:total_samples]/overall_samples + + if (callers = data[:frames].map{ |id, other| [other[:name], other[:edges][frame.to_i]] if other[:edges] && other[:edges].include?(frame.to_i) }.compact).any? + f.puts " callers:" + callers = callers.sort_by(&:last).reverse + callers.each do |name, weight| + f.printf " % 5d (% 8s) %s\n", weight, "%3.1f%%" % (100.0*weight/info[:total_samples]), name + end + end + + if callees = info[:edges] + f.printf " callees (%d total):\n", info[:total_samples]-info[:samples] + callees = callees.map{ |k, weight| [data[:frames][k.to_s][:name], weight] } + callees.each do |name, weight| + f.printf " % 5d (% 8s) %s\n", weight, "%3.1f%%" % (100.0*weight/(info[:total_samples]-info[:samples])), name + end + end + + f.puts " code:" + source_display(f, file, lines, line-1..maxline) + end + end + + def print_files(sort_by_total=false, limit=nil, f = STDOUT) + list = files.map{ |file, vals| [file, vals.values.inject([0,0]){ |sum, n| add_lines(sum, n) }] } + list = list.sort_by{ |file, samples| -samples[1] } + list = list.first(limit) if limit + list.each do |file, vals| + total_samples, samples = *vals + f.printf "% 5d (%2.1f%%) / % 5d (%2.1f%%) %s\n", total_samples, (100.0*total_samples/overall_samples), samples, (100.0*samples/overall_samples), file + end + end + + def print_file(filter, f = STDOUT) + filter = /#{Regexp.escape filter}/ unless Regexp === filter + list = files + list.select!{ |name, lines| name =~ filter } + list.sort_by{ |file, vals| -vals.values.inject(0){ |sum, n| sum + (n.is_a?(Array) ? n[1] : n) } }.each do |file, lines| + source_display(f, file, lines) + end + end + + private + + def source_display(f, file, lines, range=nil) + File.readlines(file).each_with_index do |code, i| + next unless range.nil? || range.include?(i) + if lines and lineinfo = lines[i+1] + total_samples, samples = lineinfo + if version == 1.0 + samples = total_samples + f.printf "% 5d % 7s | % 5d | %s", samples, "(%2.1f%%)" % (100.0*samples/overall_samples), i+1, code + elsif samples > 0 + f.printf "% 5d % 8s / % 5d % 7s | % 5d | %s", total_samples, "(%2.1f%%)" % (100.0*total_samples/overall_samples), samples, "(%2.1f%%)" % (100.0*samples/overall_samples), i+1, code else - printf " | % 5d | %s", i+1, code + f.printf "% 5d % 8s | % 5d | %s", total_samples, "(%3.1f%%)" % (100.0*total_samples/overall_samples), i+1, code end + else + if version == 1.0 + f.printf " | % 5d | %s", i+1, code + else + f.printf " | % 5d | %s", i+1, code + end end end + end + + def +(other) + raise ArgumentError, "cannot combine #{other.class}" unless self.class == other.class + raise ArgumentError, "cannot combine #{modeline} with #{other.modeline}" unless modeline == other.modeline + raise ArgumentError, "cannot combine v#{version} with v#{other.version}" unless version == other.version + + f1, f2 = normalized_frames, other.normalized_frames + frames = (f1.keys + f2.keys).uniq.inject(Hash.new) do |hash, id| + if f1[id].nil? + hash[id] = f2[id] + elsif f2[id] + hash[id] = f1[id] + hash[id][:total_samples] += f2[id][:total_samples] + hash[id][:samples] += f2[id][:samples] + if f2[id][:edges] + edges = hash[id][:edges] ||= {} + f2[id][:edges].each do |edge, weight| + edges[edge] ||= 0 + edges[edge] += weight + end + end + if f2[id][:lines] + lines = hash[id][:lines] ||= {} + f2[id][:lines].each do |line, weight| + lines[line] = add_lines(lines[line], weight) + end + end + else + hash[id] = f1[id] + end + hash + end + + d1, d2 = data, other.data + data = { + version: version, + mode: d1[:mode], + interval: d1[:interval], + samples: d1[:samples] + d2[:samples], + gc_samples: d1[:gc_samples] + d2[:gc_samples], + missed_samples: d1[:missed_samples] + d2[:missed_samples], + frames: frames + } + + self.class.new(data) end end end