class Monitor
Detects memory leaks by tracking process size increases.
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.
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.
Definitions
DEFAULT_THRESHOLD_SIZE = 1024*1024*10
We only track process size changes greater than this threshold_size, across the DEFAULT_INTERVAL. True memory leaks will eventually hit this threshold_size, while small fluctuations will not.
DEFAULT_INCREASE_LIMIT = 20
We track the last N process size increases. If the process size is not stabilizing within the specified increase_limit, we can assume there is a leak. With a default interval of 10 seconds, this will track the last ~3 minutes of process size increases.
def initialize(process_id = Process.pid, maximum_size: nil, maximum_size_limit: nil, threshold_size: DEFAULT_THRESHOLD_SIZE, increase_limit: DEFAULT_INCREASE_LIMIT)
Create a new monitor.
Signature
-
parameter
process_id
Integer
The process ID to monitor.
-
parameter
maximum_size
Numeric
The initial process size, from which we willl track increases, in bytes.
-
parameter
maximum_size_limit
Numeric | Nil
The maximum process size allowed, in bytes, before we assume a memory leak.
-
parameter
threshold_size
Numeric
The threshold for process size increases, in bytes.
-
parameter
increase_limit
Numeric
The limit for the number of process size increases, before we assume a memory leak.
Implementation
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 as_json(...)
Signature
-
returns
Hash
A serializable representation of the cluster.
Implementation
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 to_json(...)
Signature
-
returns
String
The JSON representation of the cluster.
Implementation
def to_json(...)
as_json.to_json(...)
end
attr :process_id
Signature
-
attribute
Integer
The process ID to monitor.
attr_accessor :maximum_size
Signature
-
attribute
Numeric
The maximum process size observed.
attr_accessor :maximum_size_limit
Signature
-
attribute
Numeric | Nil
The maximum process size allowed, before we assume a memory leak.
attr_accessor :threshold_size
Signature
-
attribute
Numeric
The threshold_size for process size increases.
attr_accessor :increase_count
Signature
-
attribute
Integer
The number of increasing process size samples.
attr_accessor :increase_limit
Signature
-
attribute
Numeric
The limit for the number of process size increases, before we assume a memory leak.
def memory_usage
Signature
-
returns
Integer
Ask the system for the current memory usage.
Implementation
def memory_usage
System.memory_usage(@process_id)
end
def current_size
Signature
-
returns
Integer
The last sampled memory usage.
Implementation
def current_size
@current_size ||= memory_usage
end
def current_size=(value)
Set the current memory usage, rather than sampling it.
Implementation
def current_size=(value)
@current_size = value
end
def increase_limit_exceeded?
Indicates whether a memory leak has been detected.
If the number of increasing heap size samples is greater than or equal to the increase_limit, a memory leak is assumed.
Signature
-
returns
Boolean
True if a memory leak has been detected.
Implementation
def increase_limit_exceeded?
@increase_count >= @increase_limit
end
def maximum_size_limit_exceeded?
Indicates that the current memory usage has grown beyond the maximum size limit.
Signature
-
returns
Boolean
True if the current memory usage has grown beyond the maximum size limit.
Implementation
def maximum_size_limit_exceeded?
@maximum_size_limit && self.current_size > @maximum_size_limit
end
def leaking?
Indicates whether a memory leak has been detected.
Signature
-
returns
Boolean
True if a memory leak has been detected.
Implementation
def leaking?
increase_limit_exceeded? || maximum_size_limit_exceeded?
end
def sample!
Capture a memory usage sample and yield if a memory leak is detected.
Signature
-
yields
{|sample, monitor| ...}
If a memory leak is detected.
Implementation
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