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 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260
|
# encoding: utf-8
require 'erb'
require 'fileutils'
require 'base64'
module RubyProf
# prints a HTML visualization of the call tree
class CallStackPrinter < AbstractPrinter
include ERB::Util
# Specify print options.
#
# options - Hash table
# :min_percent - Number 0 to 100 that specifes the minimum
# %self (the methods self time divided by the
# overall total time) that a method must take
# for it to be printed out in the report.
# Default value is 0.
#
# :print_file - True or false. Specifies if a method's source
# file should be printed. Default value if false.
#
# :threshold - a float from 0 to 100 that sets the threshold of
# results displayed.
# Default value is 1.0
#
# :title - a String to overide the default "ruby-prof call tree"
# title of the report.
#
# :expansion - a float from 0 to 100 that sets the threshold of
# results that are expanded, if the percent_total
# exceeds it.
# Default value is 10.0
#
# :application - a String to overide the name of the application,
# as it appears on the report.
#
def print(output = STDOUT, options = {})
@output = output
setup_options(options)
if @graph_html = options.delete(:graph)
@graph_html = "file://" + @graph_html if @graph_html[0]=="/"
end
print_header
@overall_threads_time = @result.threads.inject(0) do |val, thread|
val += thread.total_time
end
@result.threads.each do |thread|
@current_thread_id = thread.fiber_id
@overall_time = thread.total_time
thread_info = "Thread: #{thread.id}"
thread_info << ", Fiber: #{thread.fiber_id}" unless thread.id == thread.fiber_id
thread_info << " (#{"%4.2f%%" % ((@overall_time/@overall_threads_time)*100)} ~ #{@overall_time})"
@output.print "<div class=\"thread\">#{thread_info}</div>"
@output.print "<ul name=\"thread\">"
thread.methods.each do |m|
# $stderr.print m.dump
next unless m.root?
m.call_infos.each do |ci|
next unless ci.root?
print_stack ci, thread.total_time
end
end
@output.print "</ul>"
end
print_footer
end
def print_stack(call_info, parent_time)
total_time = call_info.total_time
percent_parent = (total_time/parent_time)*100
percent_total = (total_time/@overall_time)*100
return unless percent_total > min_percent
color = self.color(percent_total)
kids = call_info.children
visible = percent_total >= threshold
expanded = percent_total >= expansion
display = visible ? "block" : "none"
@output.print "<li class=\"color#{color}\" style=\"display:#{display}\">"
if kids.empty?
@output.print "<a href=\"#\" class=\"toggle empty\" ></a>"
else
visible_children = kids.any?{|ci| (ci.total_time/@overall_time)*100 >= threshold}
image = visible_children ? (expanded ? "minus" : "plus") : "empty"
@output.print "<a href=\"#\" class=\"toggle #{image}\" ></a>"
end
@output.printf "<span> %4.2f%% (%4.2f%%) %s %s</span>\n", percent_total, percent_parent, link(call_info), graph_link(call_info)
unless kids.empty?
if expanded
@output.print "<ul>"
else
@output.print '<ul style="display:none">'
end
kids.sort_by{|c| -c.total_time}.each do |callinfo|
print_stack callinfo, total_time
end
@output.print "</ul>"
end
@output.print "</li>"
end
def name(call_info)
method = call_info.target
method.full_name
end
def link(call_info)
method = call_info.target
file = File.expand_path(method.source_file)
if file =~ /\/ruby_runtime$/
h(name(call_info))
else
if RUBY_PLATFORM =~ /darwin/
"<a href=\"txmt://open?url=file://#{file}&line=#{method.line}\">#{h(name(call_info))}</a>"
else
"<a href=\"file://#{file}##{method.line}\">#{h(name(call_info))}</a>"
end
end
end
def graph_link(call_info)
total_calls = call_info.target.call_infos.inject(0){|t, ci| t += ci.called}
href = "#{@graph_html}##{method_href(call_info.target)}"
totals = @graph_html ? "<a href='#{href}'>#{total_calls}</a>" : total_calls.to_s
"[#{call_info.called} calls, #{totals} total]"
end
def method_href(method)
h(method.full_name.gsub(/[><#\.\?=:]/,"_") + "_" + @current_thread_id.to_s)
end
def total_time(call_infos)
sum(call_infos.map{|ci| ci.total_time})
end
def sum(a)
a.inject(0.0){|s,t| s+=t}
end
def dump(ci)
$stderr.printf "%s/%d t:%f s:%f w:%f \n", ci, ci.object_id, ci.total_time, ci.self_time, ci.wait_time
end
def color(p)
case i = p.to_i
when 0..5
"01"
when 5..10
"05"
when 100
"9"
else
"#{i/10}"
end
end
def application
@options[:application] || $PROGRAM_NAME
end
def arguments
ARGV.join(' ')
end
def title
@title ||= @options.delete(:title) || "ruby-prof call tree"
end
def threshold
@options[:threshold] || 1.0
end
def expansion
@options[:expansion] || 10.0
end
def print_header
@output.puts "<html><head>"
@output.puts '<meta http-equiv="content-type" content="text/html; charset=utf-8">'
@output.puts "<title>#{h title}</title>"
print_css
print_java_script
@output.puts '</head><body><div style="display: inline-block;">'
print_title_bar
print_commands
print_help
end
def print_footer
@output.puts '<div id="sentinel"></div></div></body></html>'
end
def open_asset(file)
path = File.join(File.expand_path('../../assets', __FILE__), file)
File.open(path, 'rb').read
end
def print_css
html = open_asset('call_stack_printer.css.html')
@output.puts html.gsub('%s', base64_image)
end
def base64_image
file = open_asset('call_stack_printer.png')
Base64.encode64(file).gsub(/\n/, '')
end
def print_java_script
html = open_asset('call_stack_printer.js.html')
@output.puts html
end
def print_title_bar
@output.puts <<-"end_title_bar"
<div id="titlebar">
Call tree for application <b>#{h application} #{h arguments}</b><br/>
Generated on #{Time.now} with options #{h @options.inspect}<br/>
</div>
end_title_bar
end
def print_commands
@output.puts <<-"end_commands"
<div id=\"commands\">
<span style=\"font-size: 11pt; font-weight: bold;\">Threshold:</span>
<input value=\"#{h threshold}\" size=\"3\" id=\"threshold\" type=\"text\">
<input value=\"Apply\" onclick=\"setThreshold();\" type=\"submit\">
<input value=\"Expand All\" onclick=\"expandAll(event);\" type=\"submit\">
<input value=\"Collapse All\" onclick=\"collapseAll(event);\" type=\"submit\">
<input value=\"Show Help\" onclick=\"toggleHelp(this);\" type=\"submit\">
</div>
end_commands
end
def print_help
@output.puts <<-'end_help'
<div style="display: none;" id="help">
• Enter a decimal value <i>d</i> into the threshold field and click "Apply"
to hide all nodes marked with time values lower than <i>d</i>.<br>
• Click on "Expand All" for full tree expansion.<br>
• Click on "Collapse All" to show only top level nodes.<br>
• Use a, s, d, w as in Quake or Urban Terror to navigate the tree.<br>
• Use f and b to navigate the tree in preorder forward and backwards.<br>
• Use x to toggle visibility of a subtree.<br>
• Use * to expand/collapse a whole subtree.<br>
• Use h to navigate to thread root.<br>
• Use n and p to navigate between threads.<br>
• Click on background to move focus to a subtree.<br>
</div>
end_help
end
end
end
|