Skip to content

Instantly share code, notes, and snippets.

@outoftime
Last active December 14, 2015 09:58
Show Gist options
  • Save outoftime/5068278 to your computer and use it in GitHub Desktop.
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.
#!/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
#!/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
#!/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