class Heapy::Analyzer

Used for inspecting contents of a heap dump

To glance all contents at a glance run:

Analyzer.new(file_name).analyze

To inspect contents of a specific generation run:

Analyzer.new(file_name).drill_down(generation, Float::INFINITY)

Public Class Methods

new(filename) click to toggle source
# File lib/heapy/analyzer.rb, line 13
def initialize(filename)
  @filename = filename
end

Public Instance Methods

analyze() click to toggle source
# File lib/heapy/analyzer.rb, line 126
def analyze
  puts ""
  puts "Analyzing Heap"
  puts "=============="
  default_key = "nil".freeze

  # generation number is key, value is count
  data = Hash.new {|h, k| h[k] = 0 }
  mem = Hash.new {|h, k| h[k] = 0 }
  total_count = 0
  total_mem = 0

  read do |parsed|
    data[parsed["generation"] || 0] += 1
    mem[parsed["generation"] || 0] += parsed["memsize"] || 0
  end

  data = data.sort {|(k1,v1), (k2,v2)| k1 <=> k2 }
  max_length = [data.last[0].to_s.length, default_key.length].max
  data.each do |generation, count|
    generation = default_key if generation == 0
    total_count += count
    total_mem += mem[generation]
    puts "Generation: #{ generation.to_s.rjust(max_length) } object count: #{ count }, mem: #{(mem[generation].to_f / 1024).round(1)} kb"
  end

  puts ""
  puts "Heap total"
  puts "=============="
  puts "Generations (active): #{data.length}"
  puts "Count: #{total_count}"
  puts "Memory: #{(total_mem.to_f / 1024).round(1)} kb"
end
drill_down(generation_to_inspect, max_items_to_display) click to toggle source
# File lib/heapy/analyzer.rb, line 30
def drill_down(generation_to_inspect, max_items_to_display)
  puts ""
  puts "Analyzing Heap (Generation: #{generation_to_inspect})"
  puts "-------------------------------"
  puts ""

  generation_to_inspect = Integer(generation_to_inspect) unless generation_to_inspect == "all"

  memsize_hash    = Hash.new { |h, k| h[k] = 0  }
  count_hash      = Hash.new { |h, k| h[k] = 0  }
  string_count    = Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = 0  } }

  reference_hash  = Hash.new { |h, k| h[k] = 0  }

  read do |parsed|
    generation = parsed["generation"] || 0
    if generation_to_inspect == "all".freeze || generation == generation_to_inspect
      next unless parsed["file"]

      key = "#{ parsed["file"] }:#{ parsed["line"] }"
      memsize_hash[key] += parsed["memsize"] || 0
      count_hash[key]   += 1

      if parsed["type"] == "STRING".freeze
        string_count[parsed["value"]][key] += 1 if parsed["value"]
      end

      if parsed["references"]
        reference_hash[key] += parsed["references"].length
      end
    end
  end

  raise "not a valid Generation: #{generation_to_inspect.inspect}" if memsize_hash.empty?

  total_memsize = memsize_hash.inject(0){|count, (k, v)| count += v}

  # /Users/richardschneeman/Documents/projects/codetriage/app/views/layouts/application.html.slim:1"=>[{"address"=>"0x7f8a4fbf2328", "type"=>"STRING", "class"=>"0x7f8a4d5dec68", "bytesize"=>223051, "capacity"=>376832, "encoding"=>"UTF-8", "file"=>"/Users/richardschneeman/Documents/projects/codetriage/app/views/layouts/application.html.slim", "line"=>1, "method"=>"new", "generation"=>36, "memsize"=>377065, "flags"=>{"wb_protected"=>true, "old"=>true, "long_lived"=>true, "marked"=>true}}]}
  puts "allocated by memory (#{total_memsize}) (in bytes)"
  puts "=============================="
  memsize_hash = memsize_hash.sort {|(k1, v1), (k2, v2)| v2 <=> v1 }.first(max_items_to_display)
  longest      = memsize_hash.first[1].to_s.length
  memsize_hash.each do |file_line, memsize|
    puts "  #{memsize.to_s.rjust(longest)}  #{file_line}"
  end

  total_count = count_hash.inject(0){|count, (k, v)| count += v}

  puts ""
  puts "object count (#{total_count})"
  puts "=============================="
  count_hash = count_hash.sort {|(k1, v1), (k2, v2)| v2 <=> v1 }.first(max_items_to_display)
  longest      = count_hash.first[1].to_s.length
  count_hash.each do |file_line, memsize|
    puts "  #{memsize.to_s.rjust(longest)}  #{file_line}"
  end

  puts ""
  puts "High Ref Counts"
  puts "=============================="
  puts ""

  reference_hash = reference_hash.sort {|(k1, v1), (k2, v2)| v2 <=> v1 }.first(max_items_to_display)
  longest      = count_hash.first[1].to_s.length

  reference_hash.each do |file_line, count|
    puts "  #{count.to_s.rjust(longest)}  #{file_line}"
  end

  if !string_count.empty?
    puts ""
    puts "Duplicate strings"
    puts "=============================="
    puts ""

    value_count = {}

    string_count.each do |string, location_count_hash|
      value_count[string] = location_count_hash.values.inject(&:+)
    end

    value_count = value_count.sort {|(k1, v1), (k2, v2)| v2 <=> v1 }.first(max_items_to_display)
    longest     = value_count.first[1].to_s.length

    value_count.each do |string, c1|

      puts " #{c1.to_s.rjust(longest)}  #{string.inspect}"
      string_count[string].sort {|(k1, v1), (k2, v2)| v2 <=> v1 }.each do |file_line, c2|
       puts " #{c2.to_s.rjust(longest)}  #{file_line}"
     end
     puts ""
    end
  end

end
read() { |parsed| ... } click to toggle source
# File lib/heapy/analyzer.rb, line 17
def read
  File.open(@filename) do |f|
    f.each_line do |line|
      begin
        parsed = JSON.parse(line)
        yield parsed
      rescue JSON::ParserError
        puts "Could not parse #{line}"
      end
    end
  end
end