class Memory::Leak::Monitor
We should be careful not to filter historical data, as some memory leaks may only become apparent after a long period of time. Any kind of filtering may prevent us from detecting such a leak.
A memory leak is characterised by the memory usage of the application continuing to rise over time. We can detect this by sampling memory usage and comparing it to the previous sample. If the memory usage is higher than the previous sample, we can say that the application has allocated more memory. Eventually we expect to see this stabilize, but if it continues to rise, we can say that the application has a memory leak.
Detects memory leaks by tracking process size increases.
def as_json(...)
def as_json(...) { process_id: @process_id, current_size: @current_size, maximum_size: @maximum_size, maximum_size_limit: @maximum_size_limit, threshold_size: @threshold_size, increase_count: @increase_count, increase_limit: @increase_limit, } end
def current_size
def current_size @current_size ||= memory_usage end
def current_size=(value)
def current_size=(value) @current_size = value end
def increase_limit_exceeded?
If the number of increasing heap size samples is greater than or equal to the increase_limit, a memory leak is assumed.
Indicates whether a memory leak has been detected.
def increase_limit_exceeded? @increase_count >= @increase_limit end
def initialize(process_id = Process.pid, maximum_size: nil, maximum_size_limit: nil, threshold_size: DEFAULT_THRESHOLD_SIZE, increase_limit: DEFAULT_INCREASE_LIMIT)
@parameter threshold_size [Numeric] The threshold for process size increases, in bytes.
@parameter maximum_size_limit [Numeric | Nil] The maximum process size allowed, in bytes, before we assume a memory leak.
@parameter maximum_size [Numeric] The initial process size, from which we willl track increases, in bytes.
@parameter process_id [Integer] The process ID to monitor.
Create a new monitor.
def initialize(process_id = Process.pid, maximum_size: nil, maximum_size_limit: nil, threshold_size: DEFAULT_THRESHOLD_SIZE, increase_limit: DEFAULT_INCREASE_LIMIT) @process_id = process_id @current_size = nil @maximum_size = maximum_size @maximum_size_limit = maximum_size_limit @threshold_size = threshold_size @increase_count = 0 @increase_limit = increase_limit end
def leaking?
Indicates whether a memory leak has been detected.
def leaking? increase_limit_exceeded? || maximum_size_limit_exceeded? end
def maximum_size_limit_exceeded?
Indicates that the current memory usage has grown beyond the maximum size limit.
def maximum_size_limit_exceeded? @maximum_size_limit && self.current_size > @maximum_size_limit end
def memory_usage
def memory_usage System.memory_usage(@process_id) end
def sample!
Capture a memory usage sample and yield if a memory leak is detected.
def sample! self.current_size = memory_usage if @maximum_observed_size delta = @current_size - @maximum_observed_size Console.debug(self, "Heap size captured.", current_size: @current_size, delta: delta, threshold_size: @threshold_size, maximum_observed_size: @maximum_observed_size) if delta > @threshold_size @maximum_observed_size = @current_size @increase_count += 1 Console.debug(self, "Heap size increased.", maximum_observed_size: @maximum_observed_size, count: @count) end else Console.debug(self, "Initial heap size captured.", current_size: @current_size) @maximum_observed_size = @current_size end return @current_size end
def to_json(...)
def to_json(...) as_json.to_json(...) end