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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 127eb4efe2387664701cb1d56ba80dea1f0d833b92c645666b0bc8ad810851d7
4
- data.tar.gz: 72360bbde481a84283a54edfa5cfede10522c0610be43c2d12b8ed0c36c9f5b1
3
+ metadata.gz: 4ef67abb16c59a1a7520e6d36db4bc8f86502efced42a96ec357f9700f1e7f39
4
+ data.tar.gz: 8d1f9ab5b22aeb8b4593bbc4266cdd783891a2ab904c39a55df62f954cc50f66
5
5
  SHA512:
6
- metadata.gz: 56cf37bd97c28753a9e40898eeacee679617a175f38aa7f2be3dc850b2c669c463c4ce92e1027b865e274154cbecf5038c5f0cc916296eee9ed4b240cbf59c8b
7
- data.tar.gz: 8611335101e758f763c564536573d717ab2c3670ffe13fb1dbe39218f196bc3a49a3e468d7495a057d2c11948867e58fa53af54f15e1f096de0418801963b37c
6
+ metadata.gz: 8b2a09ce8b4fd83fb60e69af8fe324d86309efd67dde443d8e18ecf6743393719ad9155e980b1cc079091764ca0e3d393bf23c104d7680209b40c9022ba4245b
7
+ data.tar.gz: b175f60ddd2f541689226955bd4711779cade944a342023bea0ff97cdbcd37d1e767ec1036d895ff8279de95feb256d21badf2d357f952cd7a2430f7a00ae33b
checksums.yaml.gz.sig CHANGED
Binary file
@@ -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. It can also apply a memory limit to the cluster, and terminate processes if the memory limit is exceeded.
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
- def initialize(total_size_limit: nil)
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
- # Apply the memory limit to 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.
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
- # Total cluster memory usage is calculated as: max(shared memory usages) + sum(private memory usages)
62
- # This accounts for shared memory being counted only once across all processes.
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 maximum memory usage, return true if it was terminated to adjust memory usage.
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
- protected def apply_limit!(total_size_limit = @total_size_limit, &block)
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 values before yielding (process may be removed):
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: subtract this process's contribution
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, apply any per-cluster memory limits:
233
+ # Finally, enforce any per-cluster memory limits:
135
234
  if @total_size_limit
136
- apply_limit!(@total_size_limit, &block)
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
 
@@ -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/memory"
@@ -1,10 +1,10 @@
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
  module Memory
7
7
  module Leak
8
- VERSION = "0.9.2"
8
+ VERSION = "0.10.0"
9
9
  end
10
10
  end
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
@@ -1,5 +1,9 @@
1
1
  # Releases
2
2
 
3
+ ## v0.10.0
4
+
5
+ - 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.
6
+
3
7
  ## v0.9.2
4
8
 
5
9
  - Also log host memory in total memory usage logs.
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.9.2
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: '0.10'
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: '0.10'
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
- @ �<}f7X����[�H��]� 1�3-�jw�3Y���'�z��i�ي�H�/f��P�����9�|��x�����("�Y����7"5$w:S/����)d-���K�_��ߟ���Z�����wq�0�
2
- BJ
1
+ [+|�1���҂�X|�͖�wxt���}di��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