Created
December 20, 2022 21:16
-
-
Save nilbus/6499ab32b3e53f839f6426f2cc35be26 to your computer and use it in GitHub Desktop.
Ruby code for dumping and diffing heap dumps to find memory leaks
This file contains hidden or 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
require 'set' | |
require 'json' | |
require 'objspace' | |
DIFF_FILENAMES = ['/tmp/1.json', '/tmp/2.json', '/tmp/3.json'] | |
def dump_memory(n) | |
BatchLoader::Executor.clear_current | |
GC.start | |
file = File.open("/tmp/#{n}.json", 'w') | |
ObjectSpace.dump_all(output: file) | |
end | |
def diff_dumps | |
first_addrs = Set.new | |
third_addrs = Set.new | |
# Get a list of memory addresses from the first dump | |
File.open(DIFF_FILENAMES[0], "r").each_line do |line| | |
begin | |
parsed = JSON.parse(line) | |
first_addrs << parsed["address"] if parsed && parsed["address"] | |
rescue JSON::ParserError | |
puts "Couldn't parse: #{line}.inspect" | |
end | |
end | |
# Get a list of memory addresses from the last dump | |
File.open(DIFF_FILENAMES[2], "r").each_line do |line| | |
begin | |
parsed = JSON.parse(line) | |
third_addrs << parsed["address"] if parsed && parsed["address"] | |
rescue JSON::ParserError | |
puts "Couldn't parse: #{line}.inspect" | |
end | |
end | |
diff = [] | |
# Get a list of all items present in both the second and | |
# third dumps but not in the first. | |
File.open(DIFF_FILENAMES[1], "r").each_line do |line| | |
begin | |
parsed = JSON.parse(line) | |
if parsed && parsed["address"] | |
if !first_addrs.include?(parsed["address"]) && third_addrs.include?(parsed["address"]) | |
diff << parsed | |
end | |
end | |
rescue JSON::ParserError | |
puts "Couldn't parse: #{line}.inspect" | |
end | |
end | |
# Group items | |
leaks = diff.group_by do |x| | |
[x["type"], x["file"], x["line"]] | |
end.map do |x,y| | |
# Collect memory size | |
[x, y.count, y.inject(0){|sum,i| sum + (i['bytesize'] || 0) }, y.inject(0){|sum,i| sum + (i['memsize'] || 0) }] | |
end.sort do |a,b| | |
b[1] <=> a[1] | |
end | |
leaks.each do |x,y,bytesize,memsize| | |
# Output information about each potential leak | |
puts "Leaked #{y} #{x[0]} objects of size #{bytesize}/#{memsize} at: #{x[1]}:#{x[2]}" | |
end | |
# Also output total memory usage, because why not? | |
memsize = diff.inject(0){|sum,i| sum + (i['memsize'] || 0) } | |
bytesize = diff.inject(0){|sum,i| sum + (i['bytesize'] || 0) } | |
puts "\n\nTotal Size: #{bytesize}/#{memsize}" | |
leaks.size | |
end | |
def detect_leaks | |
ObjectSpace.trace_object_allocations_start | |
dump_memory(1) | |
yield | |
dump_memory(2) | |
yield | |
dump_memory(3) | |
diff_dumps | |
end |
This file contains hidden or 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 'set' | |
require 'json' | |
if ARGV.length != 3 | |
puts "Usage: detect_leaks [FIRST.json] [SECOND.json] [THIRD.json]" | |
exit 1 | |
end | |
first_addrs = Set.new | |
third_addrs = Set.new | |
# Get a list of memory addresses from the first dump | |
File.open(ARGV[0], "r").each_line do |line| | |
begin | |
parsed = JSON.parse(line) | |
first_addrs << parsed["address"] if parsed && parsed["address"] | |
rescue JSON::ParserError | |
puts "Couldn't parse: #{line}.inspect" | |
end | |
end | |
# Get a list of memory addresses from the last dump | |
File.open(ARGV[2], "r").each_line do |line| | |
begin | |
parsed = JSON.parse(line) | |
third_addrs << parsed["address"] if parsed && parsed["address"] | |
rescue JSON::ParserError | |
puts "Couldn't parse: #{line}.inspect" | |
end | |
end | |
diff = [] | |
# Get a list of all items present in both the second and | |
# third dumps but not in the first. | |
File.open(ARGV[1], "r").each_line do |line| | |
begin | |
parsed = JSON.parse(line) | |
if parsed && parsed["address"] | |
if !first_addrs.include?(parsed["address"]) && third_addrs.include?(parsed["address"]) | |
diff << parsed | |
end | |
end | |
rescue JSON::ParserError | |
puts "Couldn't parse: #{line}.inspect" | |
end | |
end | |
# Group items | |
diff.group_by do |x| | |
[x["type"], x["file"], x["line"]] | |
end.map do |x,y| | |
# Collect memory size | |
[x, y.count, y.inject(0){|sum,i| sum + (i['bytesize'] || 0) }, y.inject(0){|sum,i| sum + (i['memsize'] || 0) }] | |
end.sort do |a,b| | |
b[1] <=> a[1] | |
end.each do |x,y,bytesize,memsize| | |
# Output information about each potential leak | |
puts "Leaked #{y} #{x[0]} objects of size #{bytesize}/#{memsize} at: #{x[1]}:#{x[2]}" | |
end | |
# Also output total memory usage, because why not? | |
memsize = diff.inject(0){|sum,i| sum + (i['memsize'] || 0) } | |
bytesize = diff.inject(0){|sum,i| sum + (i['bytesize'] || 0) } | |
puts "\n\nTotal Size: #{bytesize}/#{memsize}" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment