Created
March 2, 2017 17:46
-
-
Save cfryanr/0aedaee933ab6ea1763f6f388c6b499f to your computer and use it in GitHub Desktop.
A very rough draft of a ruby heap analyzer
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
# This is just a sketch of some useful tools for analyzing ruby | |
# heap dump files while investigating memory leaks. Maybe I'll | |
# clean this up and make a gem out of it some day. | |
require 'json' | |
require 'pp' | |
require 'set' | |
def for_each_line(method) | |
File.open('/tmp/heap.json', 'r') do |f| | |
f.each_line do |line| | |
method.call(JSON.parse(line)) | |
end | |
end | |
end | |
def prepare_hash_for_output(o) | |
class_name = $map_address_to_class_name[o['class']] | |
o['class'] = "#{o['class']} (#{class_name})" if class_name | |
o['file'] = "#{o['file']}:#{o['line']}" if o['file'] | |
o.delete 'bfs_parent' | |
o | |
end | |
def find_shortest_path(heap, from_address, to_address) | |
visited_addresses = Set.new | |
current_level_addresses = [from_address] | |
next_level_addresses = [] | |
found = false | |
loop do | |
while (node_address = current_level_addresses.pop) do | |
# puts "searching at node #{node_address}" | |
if node_address == to_address | |
found = true | |
# puts "found!" | |
break | |
end | |
if visited_addresses.include? node_address | |
# puts "skipping visited node" | |
next | |
end | |
visited_addresses.add node_address | |
children = heap[node_address]['references'] || [] | |
children.each do |child_address| | |
next if visited_addresses.include? child_address | |
child_node = heap[child_address] | |
next unless child_node | |
# puts "adding child to next level, child=#{child_address}, parent=#{node_address}" | |
next_level_addresses << child_address | |
child_node['bfs_parent'] = node_address | |
end | |
end | |
# puts "current level exhausted, found=#{found}" | |
break if found || next_level_addresses.length == 0 | |
# puts "moving to next level" | |
current_level_addresses = next_level_addresses | |
next_level_addresses = [] | |
end | |
if found | |
# puts "building path to found object" | |
path = [] | |
node = heap[to_address] | |
while node | |
# puts "node #{node['address']} going into path" | |
path.unshift node | |
parent_address = node['bfs_parent'] | |
break unless parent_address | |
node = heap[parent_address] | |
end | |
return path | |
end | |
nil | |
end | |
####### | |
first_arg, *args = ARGV | |
$map_address_to_class_name = {} | |
build_map = lambda do |o| | |
if o['type'] == 'CLASS' || o['type'] == 'MODULE' | |
$map_address_to_class_name[o['address']] = o['name'] | |
end | |
end | |
for_each_line(build_map) | |
after_all = nil | |
if first_arg == 'class' | |
class_name = args[0] | |
each_line = lambda do |o| | |
if (o['type'] == 'CLASS' && o['name'] == class_name) | |
pp prepare_hash_for_output(o) | |
end | |
end | |
elsif first_arg == 'at_address' | |
obj_address = args[0] | |
each_line = lambda do |o| | |
if o['address'] == obj_address | |
pp prepare_hash_for_output(o) | |
end | |
end | |
elsif first_arg == 'instances_of' | |
class_address = args[0] | |
each_line = lambda do |o| | |
if o['class'] == class_address | |
pp prepare_hash_for_output(o) | |
end | |
end | |
elsif first_arg == 'count_instances_of' | |
class_address = args[0] | |
count = 0 | |
each_line = lambda do |o| | |
if o['class'] == class_address | |
count += 1 | |
end | |
end | |
after_all = lambda do | |
puts "Found #{count} instances of #{class_address}" | |
end | |
elsif first_arg == 'refers_to' | |
instance_address = args[0] | |
each_line = lambda do |o| | |
if o['references'] && o['references'].include?(instance_address) | |
pp prepare_hash_for_output(o) | |
end | |
end | |
elsif first_arg == 'shortest_path' | |
from_address = args[0] | |
to_address = args[1] | |
heap = {} | |
each_line = lambda do |o| | |
heap[o['address']] = o | |
end | |
after_all = lambda do | |
path = find_shortest_path(heap, from_address, to_address) | |
if path | |
path.each do |o| | |
pp prepare_hash_for_output(o) | |
end | |
else | |
puts "No path from #{from_address} to #{to_address} found" | |
end | |
end | |
else | |
$stderr.puts 'Usage:' | |
$stderr.puts ' heap.rb class <class_name>' | |
$stderr.puts ' heap.rb at_address <address>' | |
$stderr.puts ' heap.rb instances_of <class_address>' | |
$stderr.puts ' heap.rb count_instances_of <class_address>' | |
$stderr.puts ' heap.rb refers_to <address>' | |
$stderr.puts ' heap.rb shortest_path <from_address> <to_address>' | |
exit 1 | |
end | |
for_each_line(each_line) | |
after_all.call if after_all |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment