Last active
December 14, 2015 09:58
-
-
Save outoftime/5068278 to your computer and use it in GitHub Desktop.
Script to convert the output of perftools's `pprof --raw` output into a call tree in JSON or HTML. The code is messy and undocumented, and your mileage may vary, but it works for me. Also I know more about String#unpack than I did when I wrote this. Sorry, world.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
require 'rubygems' | |
require 'readline' | |
require 'yajl' | |
require 'term/ansicolor' | |
Color = Object.new | |
Color.extend(Term::ANSIColor) | |
module Pdb | |
InvalidCommand = Class.new(StandardError) | |
class Browser | |
attr_reader :current | |
def initialize(data) | |
@nodes = [] | |
@current = Node.new(data, @nodes, nil, 0) | |
@methods = [] | |
@method_map = Hash.new do |h, label| | |
method = Method.new(label, @methods.length) | |
@methods << method | |
h[label] = method | |
end | |
@current.search { |node| @method_map[node.label] << node } | |
@sorted_methods = @methods.sort { |m1, m2| m2.total_percentage - m1.total_percentage } | |
end | |
def top | |
@current = @nodes.first | |
end | |
def up(times = 1) | |
times.times do | |
if @current.parent.nil? | |
raise InvalidCommand, "Can't go up from root" | |
end | |
@current = @current.parent | |
end | |
end | |
def down(times = 1) | |
times.times { @current = current.children.first if current.children.any? } | |
end | |
def query(parts) | |
current.query(parts.reverse) | |
end | |
def parents(number) | |
number ||= current.depth | |
current_parent = current | |
parents = number.times. | |
map { |number| current_parent = current_parent.parent }. | |
reverse | |
end | |
def method(number = nil) | |
number ? @methods[number] : @method_map[current.label] | |
end | |
def methods | |
@sorted_methods | |
end | |
def goto(number) | |
if @nodes[number].nil? | |
raise InvalidCommand, "No node with number #{number}" | |
end | |
@current = @nodes[number] | |
end | |
def next_branch | |
until @current.branch? || @current.leaf? | |
@current = @current.children.first | |
end | |
end | |
def each_node(&block) | |
@current.search(&block) | |
end | |
end | |
class Node | |
attr_reader :label, :percentage, :number, :parent, :children, :depth | |
def initialize(data, nodes, parent, depth) | |
@label = data[:label] | |
@percentage = data[:percentage] | |
@parent = parent | |
@depth = depth | |
@number = nodes.length | |
nodes << self | |
if data[:children] | |
@children = data[:children].map do |child_data| | |
Node.new(child_data, nodes, self, depth + 1) | |
end | |
else | |
@children = [] | |
end | |
end | |
def branch? | |
children.length > 1 | |
end | |
def leaf? | |
children.empty? | |
end | |
def self_percentage | |
percentage - children.map(&:percentage).inject(0.0, &:+) | |
end | |
def percentage_of_parent | |
if parent.nil? then 100.0 | |
elsif parent.percentage == 0 then 0.0 | |
else (percentage.to_f / parent.percentage) * 100 | |
end | |
end | |
def non_recursive_percentage | |
return @non_recursive_percentage if @non_recursive_percentage | |
@non_recursive_percentage = percentage | |
closest_descendants { |node| node.label == label }.each do |node| | |
@non_recursive_percentage -= node.percentage | |
end | |
@non_recursive_percentage | |
end | |
def recursive_depth | |
return @recursive_depth if @recursive_depth | |
@recursive_depth = 0 | |
current = self | |
while current.parent | |
current = current.parent | |
@recursive_depth += 1 if current.label == label | |
end | |
@recursive_depth | |
end | |
def closest_descendants(&block) | |
children.map do |child| | |
if yield(child) then child | |
else child.closest_descendants(&block) | |
end | |
end.flatten | |
end | |
def search(&block) | |
return enum_for(:search) if block.nil? | |
yield self | |
children.each { |child| child.search(&block) } | |
end | |
def query(parts) | |
return self if parts.empty? | |
part = parts.pop | |
case part | |
when '.' then self | |
when '..' then parent | |
else children.find { |child| child.label == part } | |
end | |
end | |
def to_s | |
sprintf("[%5i] %4.1f%% %4.1f%% %4.1f%% %3i%% %2i %s", | |
number, percentage, non_recursive_percentage, | |
self_percentage, percentage_of_parent, recursive_depth, label) | |
end | |
end | |
class Method | |
attr_reader :label, :nodes, :number | |
def initialize(label, number) | |
@label, @number = label, number | |
@nodes = [] | |
end | |
def <<(node) | |
nodes << node | |
end | |
def to_s | |
sprintf('[%5i] %3i%% %3i %s', number, total_percentage, count, label) | |
end | |
def count | |
nodes.count | |
end | |
def total_percentage | |
nodes.map(&:non_recursive_percentage).inject(0.0, &:+) | |
end | |
def report | |
sprintf( | |
"%s\n%-15s %i\n%-15s %i%%\n\n%s", | |
label, | |
'Occurrences:', nodes.length, | |
'Total:', total_percentage, | |
nodes.sort_by(&:non_recursive_percentage).reverse.join("\n") | |
) | |
end | |
end | |
end | |
data = Yajl::Parser.parse(File.read(ARGV[0]), :symbolize_keys => true) | |
browser = Pdb::Browser.new(data) | |
def print_nodes(nodes) | |
string = nodes.map do |node| | |
if node.children.any? | |
"#{Color.bold}#{node}#{Color.clear}" | |
else | |
node | |
end | |
end.join("\n") | |
output(string) | |
end | |
def output(string) | |
string = string.to_s | |
if string.each_line.count > `tput lines`.to_i | |
IO.popen("less -R", "w") { |f| f.puts string } | |
else | |
puts string | |
end | |
end | |
def error(string) | |
STDERR.puts "#{Color.red}#{Color.bold}#{string}#{Color.clear}" | |
end | |
Readline.completion_append_character = ' ' | |
Readline.completer_word_break_characters = ' ' | |
loop do | |
current_node = browser.current | |
child_labels = current_node.children.map(&:label) | |
Readline.completion_proc = proc { |s| child_labels.grep(/(^|#)#{Regexp.escape(s)}/) } | |
command = Readline.readline("#{current_node.label}> ", true) | |
command.strip! unless command.nil? | |
begin | |
case command | |
when nil, 'quit', 'exit' | |
puts '' | |
exit | |
when /ls(?: (.+))?/ | |
if $1 | |
node = current_node.children.find { |child| child.label == $1 } | |
else | |
node = current_node | |
end | |
print_nodes(node.children) | |
when /cd #(\d+)/ | |
browser.goto($1.to_i) | |
when /cd (.+)/ | |
label = $1 | |
node = browser.query(label.split('/')) | |
if node | |
browser.goto(node.number) | |
else | |
error("No child #{label}") | |
end | |
when 'leaves' | |
nodes = [] | |
browser.each_node { |node| nodes << node if node.leaf? } | |
print_nodes(nodes) | |
when 'branch' | |
browser.next_branch | |
when 'up' | |
browser.up | |
when 'down' | |
browser.down | |
when 'db' | |
browser.down | |
browser.next_branch | |
when /pwd(?: (\d+))?/ | |
number = $1.to_i if $1 | |
print_nodes(browser.parents(number) + [current_node]) | |
when 'method' | |
output browser.method.report | |
when /method (\d+)/ | |
output browser.method($1.to_i).report | |
when 'top', 'cd' | |
browser.top | |
when 'methods' | |
output browser.methods.join("\n") | |
else error "Unrecognized command #{command}" | |
end | |
rescue Pdb::InvalidCommand => e | |
STDERR.puts "#{Color.red}#{Color.bold}#{e.message}#{Color.clear}" | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
require 'rubygems' | |
require 'stringio' | |
require 'json' | |
if ARGV[1] | |
INPUT = File.open(ARGV[1]) | |
else | |
INPUT = STDIN | |
end | |
INPUT.set_encoding('ASCII-8BIT') if INPUT.respond_to?(:set_encoding) | |
line = INPUT.gets while line != "--- symbol\n" | |
def normalize(symbol) | |
if symbol =~ /^Object#_run_erb_(.+)_locals/ | |
$1.gsub(/\d+/) { |i| i.to_i.chr } | |
else | |
symbol | |
end | |
end | |
symbol_table = {} | |
while (line = INPUT.gets) != "---\n" | |
address, symbol = *line.strip.split(' ', 2) | |
symbol_table[address] = normalize(symbol) | |
end | |
line = INPUT.gets while line != "--- profile\n" | |
class CallNode | |
attr_reader :label, :children, :count | |
def initialize(label) | |
@label = label | |
@count = 0 | |
@children = Hash.new do |h, label| | |
h[label] = CallNode.new(label) | |
end | |
end | |
def incr(by = 1) | |
@count += by | |
end | |
def to_s | |
io = StringIO.new | |
pretty_print(0, io, @count) | |
io.string | |
end | |
protected | |
def pretty_print(indent, io, total) | |
io.puts "#{' ' * indent}#{label} (#{sprintf('%0.1f', count / total.to_f * 100)}%)" | |
children.values.sort_by { |child| -child.count }.each do |child| | |
child.pretty_print(indent + 1, io, total) | |
end | |
end | |
end | |
class JsonFormatter | |
def initialize(root) | |
@root = root | |
@total = root.count.to_f | |
end | |
def print(io=STDOUT) | |
io.puts JSON.generate(node_as_json(@root), :max_nesting => false) | |
end | |
private | |
def node_as_json(node) | |
{ | |
:label => node.label, | |
:percentage => (node.count / @total * 100), | |
:children => node.children.values.sort_by { |child| -child.count }. | |
map { |child| node_as_json(child) } | |
} | |
end | |
end | |
class HtmlFormatter | |
def initialize(root) | |
@root = root | |
@total = @root.count.to_f | |
@counter = 0 | |
end | |
def print(io=STDOUT) | |
write_head(io) | |
io.puts(node_as_html(@root)) | |
write_tail(io) | |
end | |
private | |
def write_head(io) | |
io.puts <<-HTML | |
<html> | |
<head> | |
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script> | |
<style> | |
body { | |
font-family: Helvetica; | |
font-size: 14px; | |
padding-bottom: 300px; | |
padding-right: 200px; | |
} | |
li { line-height: 26px; } | |
ul { | |
list-style-type: none; | |
padding-left: 25px; | |
white-space: nowrap; | |
} | |
.buttons span { margin-right: 25px; } | |
.tree input + label + ul { display: none; } | |
</style> | |
<script type="text/javascript"> | |
$(function() { | |
$('input.node').change(function() { | |
if ($(this).prop('checked')) { | |
$(this).parent().children('ul').slideDown('fast'); | |
} else { | |
$(this).parent().children('ul').slideUp('fast'); | |
} | |
}); | |
$('#open-all-button').click(function() { | |
$('.tree').children('ul').find('ul').show(); | |
$('input').prop('checked', true); | |
}); | |
$('#close-all-button').click(function() { | |
$('.tree').children('ul').find('ul').hide(); | |
$('input').prop('checked', false); | |
}); | |
$('#close-percent').change(function() { | |
console.log('CHANGE'); | |
percent = parseFloat($(this).val()); | |
uls = $('ul'); | |
for (var idx = 0; idx < uls.length; ++idx) { | |
$ul = $(uls[idx]); | |
val = parseFloat($ul.data('percent')); | |
if (val < percent) { | |
$ul.hide(); | |
} else { | |
$ul.show(); | |
} | |
} | |
}); | |
}); | |
</script> | |
</head> | |
<body> | |
<div class="buttons"> | |
<span><a href="#" id="open-all-button">Open All</a></span> | |
<span><a href="#" id="close-all-button">Close All</a></span> | |
<span> | |
Close less than: | |
<input type="text" id="close-percent"/>% | |
</span> | |
</div> | |
<h4>Total: #{@root.count.to_f}</h4> | |
<div class="tree"> | |
HTML | |
end | |
def write_tail(io) | |
io.puts <<-HTML | |
</div> | |
</body> | |
</html> | |
HTML | |
end | |
def node_as_html(node) | |
percent = sprintf('%0.1f', node.count / @total * 100) | |
result = "<ul data-percent='#{percent}'>" | |
id = @counter+=1 | |
result += '<li>' | |
if node.children.values.any? | |
result += "<input type='checkbox' class='node' id='#{id}' \/>" | |
end | |
result += "<label for='#{id}'>" | |
result += "#{node.label} (#{percent}%, #{node.count})" | |
result += "</label>" | |
sorted_children = node.children.values.sort_by { |child| -child.count } | |
if sorted_children.any? | |
sorted_children.each_with_index do |child, idx| | |
result += node_as_html(child) | |
end | |
end | |
result += '</li>' | |
result += '</ul>' | |
end | |
end | |
if String.method_defined?(:ord) | |
def ord(char) | |
char.ord | |
end | |
else | |
def ord(char) | |
char[0] | |
end | |
end | |
def read_to_hex | |
word = INPUT.read(8) | |
sprintf( | |
"0x%016x", | |
word.unpack('Q>').first | |
) | |
end | |
root = CallNode.new('(root)') | |
INPUT.read(40) # not sure what these first few bytes are about | |
require 'ruby-debug' | |
profile_roots = Set[] | |
stacks = [] | |
until INPUT.eof? | |
count = read_to_hex.to_i(16) | |
size = read_to_hex.to_i(16) | |
stack = size.times.map do |i| | |
symbol = read_to_hex | |
symbol_table[symbol] | |
end.reverse | |
if size < 90 | |
profile_roots << stack.first | |
end | |
stacks << stack | |
end | |
stacks.each_with_index do |stack, i| | |
current = root | |
root.incr(count) | |
stack.each do |frame| | |
current = current.children[frame] | |
current.incr(count) | |
end | |
end | |
if ARGV[0].nil? || ARGV[0].downcase == 'json' | |
JsonFormatter.new(root).print | |
else | |
HtmlFormatter.new(root).print | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# Use with rack-perftools-profiler | |
require 'rubygems' | |
require 'rack' | |
require 'active_support/core_ext' | |
require 'uri' | |
require 'net/http' | |
filename = File.join(ENV['TMPDIR'], "profile.#{Time.now.to_i}.json") | |
uri = URI.parse(ARGV.first) | |
puts "Making dry-run request to #{uri} ..." | |
Net::HTTP.get(uri) | |
query = {'profile' => 'true', 'times' => '10'}. | |
merge(Rack::Utils.parse_query(uri.query)) | |
puts "Profiling #{query['times']} requests to #{uri} ..." | |
uri.query = query.to_query | |
raw = Net::HTTP.get(uri) | |
puts "Processing raw data into call tree in #{filename} ..." | |
IO.popen('script/perf-call-tree json', 'r+') do |io| | |
io << raw | |
io.close_write | |
File.open(filename, 'w') { |file| file << io.read until io.eof? } | |
end | |
system 'script/pdb', filename | |
puts "To reopen profile, run script/pdb #{filename}" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment