lib/ruby-prof/printers/dot_printer.rb
# encoding: utf-8 require 'set' module RubyProf # Generates a graphviz graph in dot format. # # To use the dot printer: # # result = RubyProf.profile do # [code to profile] # end # # printer = RubyProf::DotPrinter.new(result) # printer.print(STDOUT) # # You can use either dot viewer such as GraphViz, or the dot command line tool # to reformat the output into a wide variety of outputs: # # dot -Tpng graph.dot > graph.png # class DotPrinter < RubyProf::AbstractPrinter CLUSTER_COLORS = %w[#1A35A6 #2E86C1 #1ABC9C #5B2C8E #2471A3 #148F77 #1F618D #7D3C98] # Creates the DotPrinter using a RubyProf::Proile. def initialize(result) super(result) @seen_methods = Set.new @class_color_map = {} @color_index = 0 end # Print a graph report to the provided output. # # output - Any IO object, including STDOUT or a file. The default value is # STDOUT. # # Keyword arguments - See AbstractPrinter#print for available options. # # When profiling results that cover a large number of method calls it # helps to use the min_percent: option, for example: # # DotPrinter.new(result).print(STDOUT, min_percent: 5) # def print(output = STDOUT, min_percent: 0, max_percent: 100, filter_by: :self_time, sort_method: nil, **) @output = output @min_percent = min_percent @max_percent = max_percent @filter_by = filter_by @sort_method = sort_method puts 'digraph "Profile" {' puts 'rankdir=TB;' puts 'bgcolor="#FAFAFA";' puts 'node [fontname="Helvetica" fontsize=11 style="filled,rounded" shape=box fillcolor="#FFFFFF" color="#CCCCCC" penwidth=1.2];' puts 'edge [fontname="Helvetica" fontsize=9 color="#5B7DB1" arrowsize=0.7];' puts 'labelloc=t;' puts 'labeljust=l;' print_threads puts '}' end private # Something of a hack, figure out which constant went with the # RubyProf.measure_mode so that we can display it. Otherwise it's easy to # forget what measurement was made. def mode_name RubyProf.constants.find{|c| RubyProf.const_get(c) == RubyProf.measure_mode} end def print_threads @result.threads.each do |thread| puts "subgraph \"Thread #{thread.id}\" {" print_thread(thread) puts "}" print_classes(thread) end end # Determines an ID to use to represent the subject in the Dot file. def dot_id(subject) subject.object_id end def color_for_class(klass_name) @class_color_map[klass_name] ||= begin color = CLUSTER_COLORS[@color_index % CLUSTER_COLORS.length] @color_index += 1 color end end def node_color(total_percentage) if total_percentage >= 50 '#0D2483' elsif total_percentage >= 25 '#1A35A6' elsif total_percentage >= 10 '#2E86C1' elsif total_percentage >= 5 '#D4E6F1' else '#FFFFFF' end end def node_fontcolor(total_percentage) total_percentage >= 10 ? '#FFFFFF' : '#333333' end def print_thread(thread) total_time = thread.total_time thread.methods.sort_by(&sort_method).reverse_each do |method| total_percentage = (method.total_time/total_time) * 100 next if total_percentage < min_percent name = method.full_name.split("#").last label = "#{name}\\n(#{total_percentage.round}%)" # Only emit fill/font attrs when they differ from the default (white/#333) fill = node_color(total_percentage) fontcolor = node_fontcolor(total_percentage) attrs = "label=\"#{label}\"" attrs += " fillcolor=\"#{fill}\" fontcolor=\"#{fontcolor}\"" unless fill == '#FFFFFF' puts "#{dot_id(method)} [#{attrs}];" @seen_methods << method print_edges(total_time, method) end end def print_classes(thread) grouped = {} thread.methods.each{|m| grouped[m.klass_name] ||= []; grouped[m.klass_name] << m} grouped.each do |cls, methods2| # Filter down to just seen methods big_methods = methods2.select{|m| @seen_methods.include? m} if !big_methods.empty? color = color_for_class(cls) puts "subgraph cluster_#{cls.object_id} {" puts "label = \"#{cls}\";" puts "fontname = \"Helvetica\";" puts "fontcolor = \"#{color}\";" puts "fontsize = 14;" puts "color = \"#{color}\";" puts "style = \"rounded,dashed\";" puts "penwidth = 1.5;" big_methods.each do |m| puts "#{m.object_id};" end puts "}" end end end def print_edges(total_time, method) method.call_trees.callees.sort_by(&:total_time).reverse.each do |call_tree| target_percentage = (call_tree.target.total_time / total_time) * 100.0 next if target_percentage < min_percent next unless @seen_methods.include?(call_tree.target) edge_width = [0.5 + (target_percentage / 20.0), 4.0].min puts "#{dot_id(method)} -> #{dot_id(call_tree.target)} [label=\"#{call_tree.called}/#{call_tree.target.called}\" penwidth=#{format('%.1f', edge_width)}];" end end # Silly little helper for printing to the @output def puts(str) @output.puts(str) end end end