|
| 1 | +require 'json' |
| 2 | +require 'readline' |
| 3 | +require 'set' |
| 4 | + |
| 5 | +module Heapy |
| 6 | + |
| 7 | + # Follows references to given object addresses and prints |
| 8 | + # them as a reference stack. |
| 9 | + # Since multiple reference stacks are possible, it will preferably |
| 10 | + # try to print a stack that leads to a root node, since reference chains |
| 11 | + # leading to a root node will make an object non-collectible by GC. |
| 12 | + # |
| 13 | + # In case no chain to a root node can be found one possible stack is printed |
| 14 | + # as a fallback. |
| 15 | + class ReferenceExplorer |
| 16 | + def initialize(filename) |
| 17 | + @objects = {} |
| 18 | + @reverse_references = {} |
| 19 | + @virtual_root_address = 0 |
| 20 | + File.open(filename) do |f| |
| 21 | + f.each.with_index do |line, i| |
| 22 | + o = JSON.parse(line) |
| 23 | + addr = add_object(o) |
| 24 | + add_reverse_references(o, addr) |
| 25 | + add_class_references(o, addr) |
| 26 | + end |
| 27 | + end |
| 28 | + end |
| 29 | + |
| 30 | + def drill_down_list(addresses) |
| 31 | + addresses.each { |addr| drill_down(addr) } |
| 32 | + end |
| 33 | + |
| 34 | + def drill_down_interactive |
| 35 | + while buf = Readline.readline("Enter address > ", true) |
| 36 | + drill_down(buf) |
| 37 | + end |
| 38 | + end |
| 39 | + |
| 40 | + def drill_down(addr_string) |
| 41 | + addr = addr_string.to_i(16) |
| 42 | + puts |
| 43 | + |
| 44 | + chain = find_root_chain(addr) |
| 45 | + unless chain |
| 46 | + puts 'Could not find a reference chain leading to a root node. Searching for a non-specific chain now.' |
| 47 | + puts |
| 48 | + chain = find_any_chain(addr) |
| 49 | + end |
| 50 | + |
| 51 | + puts '## Reference chain' |
| 52 | + chain.each do |ref| |
| 53 | + puts format_object(ref) |
| 54 | + end |
| 55 | + |
| 56 | + puts |
| 57 | + puts "## All references to #{addr_string}" |
| 58 | + refs = @reverse_references[addr] || [] |
| 59 | + refs.each do |ref| |
| 60 | + puts " * #{format_object(ref)}" |
| 61 | + end |
| 62 | + |
| 63 | + puts |
| 64 | + end |
| 65 | + |
| 66 | + def inspect |
| 67 | + "<ReferenceExplorer #{@objects.size} objects; #{@reverse_references.size} back-refs>" |
| 68 | + end |
| 69 | + |
| 70 | + private |
| 71 | + |
| 72 | + def add_object(o) |
| 73 | + addr = o['address']&.to_i(16) |
| 74 | + if !addr && o['type'] == 'ROOT' |
| 75 | + addr = @virtual_root_address |
| 76 | + o['name'] ||= o['root'] |
| 77 | + @virtual_root_address += 1 |
| 78 | + end |
| 79 | + |
| 80 | + return unless addr |
| 81 | + |
| 82 | + simple_object = o.slice('type', 'file', 'name', 'class', 'length', 'imemo_type') |
| 83 | + simple_object['class'] = simple_object['class'].to_i(16) if simple_object.key?('class') |
| 84 | + simple_object['file'] = o['file'] + ":#{o['line']}" if o.key?('file') && o.key?('line') |
| 85 | + |
| 86 | + @objects[addr] = simple_object |
| 87 | + |
| 88 | + addr |
| 89 | + end |
| 90 | + |
| 91 | + def add_reverse_references(o, addr) |
| 92 | + return unless o.key?('references') |
| 93 | + o.fetch('references').map { |r| r.to_i(16) }.each do |ref| |
| 94 | + (@reverse_references[ref] ||= []) << addr |
| 95 | + end |
| 96 | + end |
| 97 | + |
| 98 | + # An instance of a class keeps that class marked by the GC. |
| 99 | + # This is not directly indicated as a reference in a heap dump, |
| 100 | + # so we manually introduce the back-reference. |
| 101 | + def add_class_references(o, addr) |
| 102 | + return unless o.key?('class') |
| 103 | + return if o['type'] == 'IMEMO' |
| 104 | + |
| 105 | + class_addr = o.fetch('class').to_i(16) |
| 106 | + (@reverse_references[class_addr] ||= []) << addr |
| 107 | + end |
| 108 | + |
| 109 | + def find_root_chain(addr, known_addresses = Set.new) |
| 110 | + known_addresses << addr |
| 111 | + |
| 112 | + return [addr] if addr < @virtual_root_address # assumption: only root objects have smallest possible addresses |
| 113 | + |
| 114 | + references = @reverse_references[addr] || [] |
| 115 | + |
| 116 | + references.reject { |a| known_addresses.include?(a) }.each do |ref| |
| 117 | + path = find_root_chain(ref, known_addresses) |
| 118 | + return [addr] + path if path |
| 119 | + end |
| 120 | + |
| 121 | + nil |
| 122 | + end |
| 123 | + |
| 124 | + def find_any_chain(addr, known_addresses = Set.new) |
| 125 | + known_addresses << addr |
| 126 | + |
| 127 | + references = @reverse_references[addr] || [] |
| 128 | + |
| 129 | + next_ref = references.reject { |a| known_addresses.include?(a) }.first |
| 130 | + if next_ref |
| 131 | + [addr] + find_any_chain(next_ref, known_addresses) |
| 132 | + else |
| 133 | + [] |
| 134 | + end |
| 135 | + end |
| 136 | + |
| 137 | + def format_path(path) |
| 138 | + return '' unless path |
| 139 | + |
| 140 | + path.split('/').reverse.take(4).reverse.join('/') |
| 141 | + end |
| 142 | + |
| 143 | + def format_object(addr) |
| 144 | + obj = @objects[addr] |
| 145 | + return "<Unknown 0x#{addr.to_s(16)}>" unless obj |
| 146 | + |
| 147 | + desc = if obj['name'] |
| 148 | + obj['name'] |
| 149 | + elsif obj['type'] == 'OBJECT' |
| 150 | + @objects.dig(obj['class'], 'name') |
| 151 | + elsif obj['type'] == 'ARRAY' |
| 152 | + "#{obj['length']} items" |
| 153 | + elsif obj['type'] == 'IMEMO' |
| 154 | + obj['imemo_type'] |
| 155 | + end |
| 156 | + desc = desc ? " #{desc}" : '' |
| 157 | + addr = addr ? " 0x#{addr.to_s(16).upcase}" : '' |
| 158 | + "<#{obj['type']}#{desc}#{addr}> (allocated at #{format_path obj['file']})" |
| 159 | + end |
| 160 | + end |
| 161 | +end |
0 commit comments