memory-leak 0.9.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/lib/memory/leak/cluster.rb +117 -13
- data/lib/memory/leak/monitor.rb +1 -1
- data/lib/memory/leak/version.rb +2 -2
- data/readme.md +6 -0
- data/releases.md +4 -0
- data.tar.gz.sig +0 -0
- metadata +5 -5
- metadata.gz.sig +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4ef67abb16c59a1a7520e6d36db4bc8f86502efced42a96ec357f9700f1e7f39
|
|
4
|
+
data.tar.gz: 8d1f9ab5b22aeb8b4593bbc4266cdd783891a2ab904c39a55df62f954cc50f66
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8b2a09ce8b4fd83fb60e69af8fe324d86309efd67dde443d8e18ecf6743393719ad9155e980b1cc079091764ca0e3d393bf23c104d7680209b40c9022ba4245b
|
|
7
|
+
data.tar.gz: b175f60ddd2f541689226955bd4711779cade944a342023bea0ff97cdbcd37d1e767ec1036d895ff8279de95feb256d21badf2d357f952cd7a2430f7a00ae33b
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/lib/memory/leak/cluster.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2025, by Samuel Williams.
|
|
4
|
+
# Copyright, 2025-2026, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require "console"
|
|
7
7
|
require "process/metrics/host/memory"
|
|
@@ -11,14 +11,26 @@ module Memory
|
|
|
11
11
|
module Leak
|
|
12
12
|
# Detects memory leaks in a cluster of processes.
|
|
13
13
|
#
|
|
14
|
-
# This class is used to manage a cluster of processes and detect memory leaks in each process.
|
|
14
|
+
# This class is used to manage a cluster of processes and detect memory leaks in each process.
|
|
15
|
+
# It can also enforce cluster-wide memory limits in two ways:
|
|
16
|
+
#
|
|
17
|
+
# 1. **Total Size Limit** (`total_size_limit`): Limits the total memory used by all processes
|
|
18
|
+
# in the cluster, calculated as max(shared memory) + sum(private memory).
|
|
19
|
+
#
|
|
20
|
+
# 2. **Free Size Minimum** (`free_size_minimum`): Ensures the host system maintains a minimum
|
|
21
|
+
# amount of free memory by terminating processes when free memory drops too low.
|
|
22
|
+
#
|
|
23
|
+
# Both limits can be active simultaneously. Processes are terminated in order of largest
|
|
24
|
+
# private memory first to maximize the impact of each termination.
|
|
15
25
|
class Cluster
|
|
16
26
|
# Create a new cluster.
|
|
17
27
|
#
|
|
18
28
|
# @parameter total_size_limit [Numeric | Nil] The total memory limit for the cluster.
|
|
19
|
-
|
|
29
|
+
# @parameter free_size_minimum [Numeric | Nil] The minimum free memory required on the host, in bytes.
|
|
30
|
+
def initialize(total_size_limit: nil, free_size_minimum: nil)
|
|
20
31
|
@total_size = nil
|
|
21
32
|
@total_size_limit = total_size_limit
|
|
33
|
+
@free_size_minimum = free_size_minimum
|
|
22
34
|
|
|
23
35
|
@processes = {}
|
|
24
36
|
end
|
|
@@ -28,6 +40,7 @@ module Memory
|
|
|
28
40
|
{
|
|
29
41
|
total_size: @total_size,
|
|
30
42
|
total_size_limit: @total_size_limit,
|
|
43
|
+
free_size_minimum: @free_size_minimum,
|
|
31
44
|
processes: @processes.transform_values(&:as_json),
|
|
32
45
|
}
|
|
33
46
|
end
|
|
@@ -43,6 +56,9 @@ module Memory
|
|
|
43
56
|
# @attribute [Numeric | Nil] The total size limit for the cluster, in bytes, if which is exceeded, the cluster will terminate processes.
|
|
44
57
|
attr_accessor :total_size_limit
|
|
45
58
|
|
|
59
|
+
# @attribute [Numeric | Nil] The minimum free memory required on the host, in bytes. If free memory falls below this minimum, the cluster will terminate processes.
|
|
60
|
+
attr_accessor :free_size_minimum
|
|
61
|
+
|
|
46
62
|
# @attribute [Hash(Integer, Monitor)] The process IDs and monitors in the cluster.
|
|
47
63
|
attr :processes
|
|
48
64
|
|
|
@@ -56,15 +72,32 @@ module Memory
|
|
|
56
72
|
@processes.delete(process_id)
|
|
57
73
|
end
|
|
58
74
|
|
|
59
|
-
#
|
|
75
|
+
# Enforce the total size memory limit on the cluster. If the total memory usage exceeds the limit, yields each process ID and monitor in order of maximum memory usage, so that they could be terminated and/or removed.
|
|
76
|
+
#
|
|
77
|
+
# This method terminates processes (largest private memory first) until the total cluster memory
|
|
78
|
+
# usage drops below the limit. Total cluster memory usage is calculated as:
|
|
79
|
+
#
|
|
80
|
+
# total_size = max(shared memory usages) + sum(private memory usages)
|
|
81
|
+
#
|
|
82
|
+
# This accounts for shared memory being counted only once across all processes, as shared memory
|
|
83
|
+
# regions (e.g., loaded libraries) are mapped into multiple processes but only consume memory once.
|
|
84
|
+
#
|
|
85
|
+
# The calculation uses the initially computed maximum shared size throughout the termination loop
|
|
86
|
+
# for performance, even though terminating processes might change which process has the maximum
|
|
87
|
+
# shared size. This approximation is acceptable for enforcement purposes.
|
|
60
88
|
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
89
|
+
# Termination stops when `total_size <= total_size_limit`, where `total_size` is incrementally
|
|
90
|
+
# updated by subtracting each terminated process's private memory contribution.
|
|
63
91
|
#
|
|
92
|
+
# @parameter total_size_limit [Numeric] The maximum total memory allowed for the cluster, in bytes.
|
|
64
93
|
# @parameter block [Proc] Required block to handle process termination.
|
|
65
|
-
# @yields {|process_id, monitor, total_size| ...} each process ID and monitor in order of
|
|
94
|
+
# @yields {|process_id, monitor, total_size| ...} each process ID and monitor in order of decreasing private memory size.
|
|
95
|
+
# @parameter process_id [Integer] The process ID to terminate.
|
|
96
|
+
# @parameter monitor [Monitor] The monitor for the process.
|
|
97
|
+
# @parameter total_size [Integer] The current estimated total cluster memory usage, in bytes. Updated incrementally after each termination.
|
|
66
98
|
# @raises [ArgumentError] if no block is provided.
|
|
67
|
-
|
|
99
|
+
# @returns [Boolean] Returns true if processes were yielded for termination, false otherwise.
|
|
100
|
+
protected def enforce_total_size_limit!(total_size_limit = @total_size_limit, &block)
|
|
68
101
|
# Only processes with known private size can be considered:
|
|
69
102
|
monitors = @processes.values.select do |monitor|
|
|
70
103
|
monitor.current_private_size != nil
|
|
@@ -91,15 +124,16 @@ module Memory
|
|
|
91
124
|
|
|
92
125
|
monitors.each do |monitor|
|
|
93
126
|
if @total_size > total_size_limit
|
|
94
|
-
# Capture
|
|
127
|
+
# Capture private size before yielding (process may be removed):
|
|
95
128
|
private_size = monitor.current_private_size
|
|
96
129
|
|
|
97
130
|
yield(monitor.process_id, monitor, @total_size)
|
|
98
131
|
|
|
99
|
-
# Incrementally update
|
|
132
|
+
# Incrementally update total size by subtracting this process's private memory contribution.
|
|
133
|
+
# We keep the previously computed maximum_shared_size for performance,
|
|
134
|
+
# even though the maximum may shift to a different process after termination.
|
|
100
135
|
sum_private_size -= private_size
|
|
101
136
|
|
|
102
|
-
# We use the computed maximum shared size, even if it might not be correct, as it is good enough for enforcing the limits and recalculating it is non-trivial.
|
|
103
137
|
@total_size = maximum_shared_size + sum_private_size
|
|
104
138
|
else
|
|
105
139
|
break
|
|
@@ -107,6 +141,71 @@ module Memory
|
|
|
107
141
|
end
|
|
108
142
|
end
|
|
109
143
|
|
|
144
|
+
# Enforce the minimum free memory requirement. If free memory falls below the minimum, yields each process ID and monitor in order of maximum memory usage, so that they could be terminated and/or removed.
|
|
145
|
+
#
|
|
146
|
+
# This method terminates processes (largest private memory first) until the estimated free memory
|
|
147
|
+
# rises above the minimum threshold. The free memory calculation is approximate:
|
|
148
|
+
# - Free memory is captured once at the start
|
|
149
|
+
# - Expected freed memory is estimated by summing terminated process private memory sizes
|
|
150
|
+
# - The OS may not immediately release all private memory to the free pool
|
|
151
|
+
# - Other system processes may allocate memory concurrently
|
|
152
|
+
#
|
|
153
|
+
# Termination stops when `free_memory + freed_private_size >= free_size_minimum`, where
|
|
154
|
+
# `freed_private_size` is the sum of private memory from all terminated processes.
|
|
155
|
+
#
|
|
156
|
+
# @parameter free_size_minimum [Numeric] The minimum free memory required, in bytes.
|
|
157
|
+
# @parameter block [Proc] Required block to handle process termination.
|
|
158
|
+
# @yields {|process_id, monitor, free_memory| ...} each process ID and monitor in order of decreasing private memory size.
|
|
159
|
+
# @parameter process_id [Integer] The process ID to terminate.
|
|
160
|
+
# @parameter monitor [Monitor] The monitor for the process.
|
|
161
|
+
# @parameter free_memory [Integer] The estimated current free memory (initial free + sum of freed private memory so far), in bytes. This is an estimate and may not reflect actual system state.
|
|
162
|
+
# @raises [ArgumentError] if no block is provided.
|
|
163
|
+
# @returns [Boolean] Returns true if processes were yielded for termination, false otherwise.
|
|
164
|
+
protected def enforce_free_size_minimum!(free_size_minimum = @free_size_minimum, &block)
|
|
165
|
+
return false unless free_size_minimum
|
|
166
|
+
|
|
167
|
+
host_memory = Process::Metrics::Host::Memory.capture
|
|
168
|
+
return false unless host_memory
|
|
169
|
+
|
|
170
|
+
free_memory = host_memory.free_size
|
|
171
|
+
|
|
172
|
+
if free_memory < free_size_minimum
|
|
173
|
+
Console.warn(self, "Free memory below minimum.", free_memory: free_memory, free_size_minimum: free_size_minimum, host_memory: host_memory)
|
|
174
|
+
else
|
|
175
|
+
Console.info(self, "Free memory above minimum.", free_memory: free_memory, free_size_minimum: free_size_minimum, host_memory: host_memory)
|
|
176
|
+
return false
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Only processes with known private size can be considered:
|
|
180
|
+
monitors = @processes.values.select do |monitor|
|
|
181
|
+
monitor.current_private_size != nil
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Sort by private memory size (descending) to terminate largest processes first:
|
|
185
|
+
monitors.sort_by! do |monitor|
|
|
186
|
+
-monitor.current_private_size
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Track how much private memory we've freed by terminating processes.
|
|
190
|
+
# Note: This is an estimate based on process private memory sizes.
|
|
191
|
+
# Actual free memory may differ due to OS memory management and other processes.
|
|
192
|
+
freed_private_size = 0
|
|
193
|
+
|
|
194
|
+
monitors.each do |monitor|
|
|
195
|
+
if free_memory + freed_private_size < free_size_minimum
|
|
196
|
+
# Capture private size before yielding (process may be removed):
|
|
197
|
+
private_size = monitor.current_private_size
|
|
198
|
+
|
|
199
|
+
yield(monitor.process_id, monitor, free_memory + freed_private_size)
|
|
200
|
+
|
|
201
|
+
# Incrementally track freed memory:
|
|
202
|
+
freed_private_size += private_size
|
|
203
|
+
else
|
|
204
|
+
break
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
110
209
|
# Sample the memory usage of all processes in the cluster.
|
|
111
210
|
def sample!
|
|
112
211
|
@processes.each_value(&:sample!)
|
|
@@ -131,9 +230,14 @@ module Memory
|
|
|
131
230
|
if block_given?
|
|
132
231
|
leaking.each(&block)
|
|
133
232
|
|
|
134
|
-
# Finally,
|
|
233
|
+
# Finally, enforce any per-cluster memory limits:
|
|
135
234
|
if @total_size_limit
|
|
136
|
-
|
|
235
|
+
enforce_total_size_limit!(@total_size_limit, &block)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Enforce minimum free memory requirement:
|
|
239
|
+
if @free_size_minimum
|
|
240
|
+
enforce_free_size_minimum!(@free_size_minimum, &block)
|
|
137
241
|
end
|
|
138
242
|
end
|
|
139
243
|
|
data/lib/memory/leak/monitor.rb
CHANGED
data/lib/memory/leak/version.rb
CHANGED
data/readme.md
CHANGED
|
@@ -8,10 +8,16 @@ Detects memory leaks in Ruby applications.
|
|
|
8
8
|
|
|
9
9
|
Please see the [project documentation](https://socketry.github.io/memory-leak/) for more details.
|
|
10
10
|
|
|
11
|
+
- [Getting Started](https://socketry.github.io/memory-leak/guides/getting-started/index) - This guide explains how to use `memory-leak` to detect and prevent memory leaks in your Ruby applications.
|
|
12
|
+
|
|
11
13
|
## Releases
|
|
12
14
|
|
|
13
15
|
Please see the [project releases](https://socketry.github.io/memory-leak/releases/index) for all releases.
|
|
14
16
|
|
|
17
|
+
### v0.10.0
|
|
18
|
+
|
|
19
|
+
- Introduce `free_size_minimum` to monitor minimum free memory size, which can be used to trigger alerts or actions when available memory is critically low.
|
|
20
|
+
|
|
15
21
|
### v0.9.2
|
|
16
22
|
|
|
17
23
|
- Also log host memory in total memory usage logs.
|
data/releases.md
CHANGED
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: memory-leak
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -42,16 +42,16 @@ dependencies:
|
|
|
42
42
|
name: process-metrics
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
44
44
|
requirements:
|
|
45
|
-
- - "
|
|
45
|
+
- - ">="
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
|
-
version:
|
|
47
|
+
version: 0.10.1
|
|
48
48
|
type: :runtime
|
|
49
49
|
prerelease: false
|
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
51
|
requirements:
|
|
52
|
-
- - "
|
|
52
|
+
- - ">="
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
|
-
version:
|
|
54
|
+
version: 0.10.1
|
|
55
55
|
executables: []
|
|
56
56
|
extensions: []
|
|
57
57
|
extra_rdoc_files: []
|
metadata.gz.sig
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
[+|�1���҂�X|�͖�wx�t���}d�i��B���z���f�2�4tk�3��B��LR���hy��+%;^Mo}b�$`PG�ʸq���9�f���+l~7������ι�����6#���`�L;y�_��61vv��&`����M�Y?���E����7G�4��Ή4�r��?�L-���c��!��+=O��@�`%�7�w
|
|
2
|
+
��l����KM%5B,�Y��=P�>/�1��I�>l0��f��w���+%��W8
|