Skip to content

Instantly share code, notes, and snippets.

@nilbus
Last active July 22, 2021 02:24
Show Gist options
  • Save nilbus/e79dd10344ae5091e077fe340b6d1d02 to your computer and use it in GitHub Desktop.
Save nilbus/e79dd10344ae5091e077fe340b6d1d02 to your computer and use it in GitHub Desktop.
Find objects that aren't getting garbage collected. Usage: detect_leaks { some_code }
# Adapted from https://blog.skylight.io/hunting-for-leaks-in-ruby/
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment