This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
# frozen_string_literal: true
|
||||
require_relative 'ruby_object_leak_tracker'
|
||||
require_relative 'os_memory_leak_tracker'
|
||||
|
||||
module MemoryTestHelpers
|
||||
class << self
|
||||
attr_accessor :gc_proc, :iterations, :logger
|
||||
|
||||
def setup
|
||||
if RUBY_PLATFORM == "java"
|
||||
# for leak detection
|
||||
JRuby.objectspace = true if defined?(JRuby)
|
||||
# for gc
|
||||
require 'java'
|
||||
java_import 'java.lang.System'
|
||||
self.gc_proc = proc { System.gc }
|
||||
else
|
||||
self.gc_proc = proc { GC.start }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module TestMethods
|
||||
def memory_leak_test(description, &block)
|
||||
context(description) do
|
||||
it "doesn't leak ruby objects" do
|
||||
object_leak_tracker = RubyObjectLeakTracker.new
|
||||
track_memory_usage(object_leak_tracker, &block)
|
||||
object_leak_tracker.total_difference_between_runs.should be <= 10
|
||||
end
|
||||
|
||||
it "doesn't leak OS memory (C interop check)" do
|
||||
os_memory_leak_tracker = OSMemoryLeakTracker.new
|
||||
track_memory_usage(os_memory_leak_tracker, &block)
|
||||
os_memory_leak_tracker.total_difference_between_runs.should be <= 10
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def track_memory_usage(tracker)
|
||||
# Intentionally do all this setup before we do any testing
|
||||
logger = MemoryTestHelpers.logger
|
||||
iterations = MemoryTestHelpers.iterations
|
||||
|
||||
checkpoint_frequency = (iterations / 10.0).to_i
|
||||
gc_frequency = 20
|
||||
|
||||
warmup_iterations = [(iterations / 3.0).to_i, 500].min
|
||||
logger.info "Performing #{warmup_iterations} warmup iterations"
|
||||
warmup_iterations.times do
|
||||
yield
|
||||
MemoryTestHelpers.gc_proc.call
|
||||
end
|
||||
tracker.capture_initial_memory_usage
|
||||
|
||||
logger.info "Performing #{iterations} iterations (checkpoint every #{checkpoint_frequency})"
|
||||
|
||||
iterations.times do |i|
|
||||
yield
|
||||
|
||||
last_iteration = (i == iterations - 1)
|
||||
checkpoint = last_iteration || (i % checkpoint_frequency == 0)
|
||||
|
||||
if checkpoint || (i % gc_frequency == 0)
|
||||
MemoryTestHelpers.gc_proc.call
|
||||
end
|
||||
|
||||
if checkpoint
|
||||
logger.info "Iteration #{i} checkpoint"
|
||||
tracker.capture_memory_usage
|
||||
tracker.dump_status(logger)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,48 @@
|
||||
# frozen_string_literal: true
|
||||
class OSMemoryLeakTracker
|
||||
attr_reader :current_run
|
||||
|
||||
def initialize
|
||||
@previous_run = @current_run = 0
|
||||
end
|
||||
|
||||
def difference_between_runs(basis=@previous_run)
|
||||
@current_run - basis
|
||||
end
|
||||
|
||||
def total_difference_between_runs
|
||||
difference_between_runs(@initial_count_run)
|
||||
end
|
||||
|
||||
def capture_initial_memory_usage
|
||||
capture_memory_usage
|
||||
@initial_count_run = @current_run
|
||||
end
|
||||
|
||||
def capture_memory_usage
|
||||
@previous_run = @current_run
|
||||
@current_run = rss_bytes
|
||||
end
|
||||
|
||||
def dump_status(logger)
|
||||
delta = difference_between_runs
|
||||
logger.add(log_level(delta), sprintf("\tTotal memory usage (kb): %d (%+d)", current_run, delta))
|
||||
end
|
||||
|
||||
private
|
||||
# amount of memory the current process "is using", in RAM
|
||||
# (doesn't include any swap memory that it may be using, just that in actual RAM)
|
||||
# Code loosely based on https://github.com/rdp/os/blob/master/lib/os.rb
|
||||
# returns 0 on windows
|
||||
def rss_bytes
|
||||
if ENV['OS'] == 'Windows_NT'
|
||||
0
|
||||
else
|
||||
`ps -o rss= -p #{Process.pid}`.to_i # in kilobytes
|
||||
end
|
||||
end
|
||||
|
||||
def log_level(delta)
|
||||
delta > 0 ? Logger::WARN : Logger::DEBUG
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,49 @@
|
||||
# frozen_string_literal: true
|
||||
class RubyObjectLeakTracker
|
||||
attr_reader :previous_count_hash, :current_count_hash
|
||||
|
||||
def initialize
|
||||
@previous_count_hash = @current_count_hash = {}
|
||||
end
|
||||
|
||||
def difference_between_runs(basis=@previous_count_hash)
|
||||
@difference_between_runs ||= Hash[@current_count_hash.map do |object_class, count|
|
||||
[object_class, count - (basis[object_class] || 0)]
|
||||
end]
|
||||
end
|
||||
|
||||
def total_difference_between_runs
|
||||
difference_between_runs(@initial_count_hash).values.inject(0) { |sum, count| sum + count }
|
||||
end
|
||||
|
||||
def capture_initial_memory_usage
|
||||
capture_memory_usage
|
||||
@initial_count_hash = @current_count_hash
|
||||
end
|
||||
|
||||
def capture_memory_usage
|
||||
@difference_between_runs = nil
|
||||
@previous_count_hash = @current_count_hash
|
||||
|
||||
class_to_count = Hash.new { |hash, key| hash[key] = 0 }
|
||||
ObjectSpace.each_object { |obj| class_to_count[obj.class] += 1 }
|
||||
|
||||
sorted_class_to_count = class_to_count.sort_by { |k, v| -v }
|
||||
@current_count_hash = Hash[sorted_class_to_count]
|
||||
end
|
||||
|
||||
def dump_status(logger)
|
||||
diff = difference_between_runs
|
||||
most_used_objects = current_count_hash.to_a.sort_by(&:last).reverse[0, 20]
|
||||
|
||||
most_used_objects.each do |object_class, count|
|
||||
delta = diff[object_class]
|
||||
logger.add(log_level(delta), sprintf("\t%s: %d (%+d)", object_class, count, delta))
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def log_level(delta)
|
||||
delta > 0 ? Logger::WARN : Logger::DEBUG
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user