memory-leak 0.8.0 → 0.9.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: e90479921bd01bec68994b554ea988197ba78cbf3051eef9fd7a379aff819ae3
4
- data.tar.gz: 0d4e41b000ce59d1ce7ee614410e7a3a42c6133a6ad2ac537b79800b32fc6c8f
3
+ metadata.gz: 37f012e5be6339c13ac1ed6be815e5f88d076b0c32809b65390f3666fa80511a
4
+ data.tar.gz: 7be83bdf7182cff46548332674a1cb1faa3aeedf3689d8d02ba741d6720a9f11
5
5
  SHA512:
6
- metadata.gz: 54b369c45a58a2d52671df46f08e8e949499973392147c07060e4a1ea171fbd21db0631ce9f595c30c3ebce04626eec83c9724a3458d6041524905a6272e3410
7
- data.tar.gz: 6c1088a90a3581f1a91404d5d8c51480dd5029a5a65abfb76b47a5d6f9cc4265fc3c493e8f5b8617bb9e0e55db5f266c3c357021c6321ac0271917ae27fa9066
6
+ metadata.gz: 4dd8a56dd4dd2b812a42cb0537ed701f91f0ffe75b53020805d846dd89e009240551b05f918f5d2d251cc17edf7b92dea84651c1240698052976cb444fc35121
7
+ data.tar.gz: 35674c676112e87c1aefc0cb36078dc6da5f927787bd9529d09c27a35dba8141fccea77f410bf61807c817054913582af85b80f788c5e92b4e564c9fb6a4f431
checksums.yaml.gz.sig CHANGED
Binary file
@@ -57,26 +57,46 @@ module Memory
57
57
 
58
58
  # 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.
59
59
  #
60
- # @yields {|process_id, monitor| ...} each process ID and monitor in order of maximum memory usage, return true if it was terminated to adjust memory usage.
61
- def apply_limit!(total_size_limit = @total_size_limit)
62
- @total_size = @processes.values.map(&:current_size).sum
60
+ # Total cluster memory usage is calculated as: max(shared memory usages) + sum(private memory usages)
61
+ # This accounts for shared memory being counted only once across all processes.
62
+ #
63
+ # @parameter block [Proc] Required block to handle process termination.
64
+ # @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.
65
+ # @raises [ArgumentError] if no block is provided.
66
+ protected def apply_limit!(total_size_limit = @total_size_limit, &block)
67
+ # Only processes with known private size can be considered:
68
+ monitors = @processes.values.select do |monitor|
69
+ monitor.current_private_size != nil
70
+ end
71
+
72
+ maximum_shared_size = monitors.map(&:current_shared_size).max || 0
73
+ sum_private_size = monitors.map(&:current_private_size).sum
74
+
75
+ @total_size = maximum_shared_size + sum_private_size
63
76
 
64
77
  if @total_size > total_size_limit
65
- Console.warn(self, "Total memory usage exceeded limit.", total_size: @total_size, total_size_limit: total_size_limit)
78
+ Console.warn(self, "Total memory usage exceeded limit.", total_size: @total_size, total_size_limit: total_size_limit, maximum_shared_size: maximum_shared_size, sum_private_size: sum_private_size)
66
79
  else
67
80
  return false
68
81
  end
69
82
 
70
- sorted = @processes.sort_by do |process_id, monitor|
71
- -monitor.current_size
83
+ # Only process monitors where we can compute private size:
84
+ monitors.sort_by! do |monitor|
85
+ -monitor.current_private_size
72
86
  end
73
87
 
74
- sorted.each do |process_id, monitor|
88
+ monitors.each do |monitor|
75
89
  if @total_size > total_size_limit
76
- yield(process_id, monitor, @total_size)
90
+ # Capture values before yielding (process may be removed):
91
+ private_size = monitor.current_private_size
92
+
93
+ yield(monitor.process_id, monitor, @total_size)
77
94
 
78
- # For the sake of the calculation, we assume that the process has been terminated:
79
- @total_size -= monitor.current_size
95
+ # Incrementally update: subtract this process's contribution
96
+ sum_private_size -= private_size
97
+
98
+ # 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.
99
+ @total_size = maximum_shared_size + sum_private_size
80
100
  else
81
101
  break
82
102
  end
@@ -85,11 +105,7 @@ module Memory
85
105
 
86
106
  # Sample the memory usage of all processes in the cluster.
87
107
  def sample!
88
- System.memory_usages(@processes.keys) do |process_id, memory_usage|
89
- if monitor = @processes[process_id]
90
- monitor.sample!(memory_usage)
91
- end
92
- end
108
+ @processes.each_value(&:sample!)
93
109
  end
94
110
 
95
111
  # Check all processes in the cluster for memory leaks.
@@ -110,11 +126,13 @@ module Memory
110
126
 
111
127
  if block_given?
112
128
  leaking.each(&block)
129
+
130
+ # Finally, apply any per-cluster memory limits:
131
+ if @total_size_limit
132
+ apply_limit!(@total_size_limit, &block)
133
+ end
113
134
  end
114
135
 
115
- # Finally, apply any per-cluster memory limits:
116
- apply_limit!(@total_size_limit, &block) if @total_size_limit
117
-
118
136
  return leaking
119
137
  end
120
138
  end
@@ -4,7 +4,7 @@
4
4
  # Copyright, 2025, by Samuel Williams.
5
5
 
6
6
  require "console"
7
- require_relative "system"
7
+ require "process/metrics/memory"
8
8
 
9
9
  module Memory
10
10
  module Leak
@@ -35,6 +35,8 @@ module Memory
35
35
 
36
36
  @sample_count = 0
37
37
  @current_size = nil
38
+ @current_shared_size = nil
39
+ @current_private_size = nil
38
40
  @maximum_size = maximum_size
39
41
  @maximum_size_limit = maximum_size_limit
40
42
  @maximum_observed_size = nil
@@ -50,6 +52,8 @@ module Memory
50
52
  process_id: @process_id,
51
53
  sample_count: @sample_count,
52
54
  current_size: @current_size,
55
+ current_shared_size: @current_shared_size,
56
+ current_private_size: @current_private_size,
53
57
  maximum_size: @maximum_size,
54
58
  maximum_size_limit: @maximum_size_limit,
55
59
  threshold_size: @threshold_size,
@@ -81,9 +85,19 @@ module Memory
81
85
  # @attribute [Numeric] The limit for the number of process size increases, before we assume a memory leak.
82
86
  attr_accessor :increase_limit
83
87
 
88
+ # @attribute [Integer] The last sampled shared memory size in bytes.
89
+ attr_accessor :current_shared_size
90
+
91
+ # @attribute [Integer] The last sampled private memory size in bytes.
92
+ attr_accessor :current_private_size
93
+
84
94
  # @returns [Integer] Ask the system for the current memory usage.
85
95
  def memory_usage
86
- System.memory_usage(@process_id)
96
+ if metrics = Process::Metrics::Memory.capture(@process_id, faults: false)
97
+ return metrics.resident_size
98
+ else
99
+ return 0
100
+ end
87
101
  end
88
102
 
89
103
  # @attribute [Integer] The number of samples taken.
@@ -129,10 +143,19 @@ module Memory
129
143
  # Capture a memory usage sample and yield if a memory leak is detected.
130
144
  #
131
145
  # @yields {|sample, monitor| ...} If a memory leak is detected.
132
- def sample!(memory_usage = self.memory_usage)
146
+ def sample!
133
147
  @sample_count += 1
134
148
 
135
- self.current_size = memory_usage
149
+ if metrics = Process::Metrics::Memory.capture(@process_id, faults: false)
150
+ self.current_size = metrics.resident_size
151
+ self.current_shared_size = metrics.shared_clean_size
152
+ self.current_private_size = metrics.private_clean_size + metrics.private_dirty_size
153
+ else
154
+ # Process doesn't exist or we can't access it - use fallback values
155
+ self.current_size = memory_usage
156
+ self.current_shared_size = 0
157
+ self.current_private_size = 0
158
+ end
136
159
 
137
160
  if @maximum_observed_size
138
161
  delta = @current_size - @maximum_observed_size
@@ -142,7 +165,7 @@ module Memory
142
165
  @maximum_observed_size = @current_size
143
166
  @increase_count += 1
144
167
 
145
- Console.debug(self, "Heap size increased.", maximum_observed_size: @maximum_observed_size, count: @count)
168
+ Console.debug(self, "Heap size increased.", maximum_observed_size: @maximum_observed_size, count: @increase_count)
146
169
  end
147
170
  else
148
171
  Console.debug(self, "Initial heap size captured.", current_size: @current_size)
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Memory
7
7
  module Leak
8
- VERSION = "0.8.0"
8
+ VERSION = "0.9.0"
9
9
  end
10
10
  end
data/readme.md CHANGED
@@ -12,6 +12,15 @@ Please see the [project documentation](https://socketry.github.io/memory-leak/)
12
12
 
13
13
  Please see the [project releases](https://socketry.github.io/memory-leak/releases/index) for all releases.
14
14
 
15
+ ### v0.9.0
16
+
17
+ - Use `process-metrics` gem for accessing both private and shared memory where possible.
18
+ - Better implementation of cluster `total_size_limit` that takes into account shared and private memory.
19
+
20
+ ### v0.8.0
21
+
22
+ - `Memory::Leak::System.total_memory_size` now considers `cgroup` memory limits.
23
+
15
24
  ### v0.7.0
16
25
 
17
26
  - Make both `increase_limit` and `maximum_size_limit` optional (if `nil`).
@@ -21,13 +30,6 @@ Please see the [project releases](https://socketry.github.io/memory-leak/release
21
30
  - Added `sample_count` attribute to monitor to track number of samples taken.
22
31
  - `check!` method in cluster now returns an array of leaking monitors if no block is given.
23
32
  - `Cluster#check!` now invokes `Monitor#sample!` to ensure memory usage is updated before checking for leaks.
24
- \=======
25
-
26
- ### v0.8.0
27
-
28
- - `Memory::Leak::System.total_memory_size` now considers `cgroup` memory limits.
29
-
30
- > > > > > > > Stashed changes
31
33
 
32
34
  ### v0.5.0
33
35
 
data/releases.md CHANGED
@@ -1,6 +1,13 @@
1
1
  # Releases
2
2
 
3
- \<\<\<\<\<\<\< Updated upstream
3
+ ## v0.9.0
4
+
5
+ - Use `process-metrics` gem for accessing both private and shared memory where possible.
6
+ - Better implementation of cluster `total_size_limit` that takes into account shared and private memory.
7
+
8
+ ## v0.8.0
9
+
10
+ - `Memory::Leak::System.total_memory_size` now considers `cgroup` memory limits.
4
11
 
5
12
  ## v0.7.0
6
13
 
@@ -11,13 +18,6 @@
11
18
  - Added `sample_count` attribute to monitor to track number of samples taken.
12
19
  - `check!` method in cluster now returns an array of leaking monitors if no block is given.
13
20
  - `Cluster#check!` now invokes `Monitor#sample!` to ensure memory usage is updated before checking for leaks.
14
- \=======
15
-
16
- ## v0.8.0
17
-
18
- - `Memory::Leak::System.total_memory_size` now considers `cgroup` memory limits.
19
-
20
- > > > > > > > Stashed changes
21
21
 
22
22
  ## v0.5.0
23
23
 
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.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -37,7 +37,21 @@ cert_chain:
37
37
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
38
38
  -----END CERTIFICATE-----
39
39
  date: 1980-01-02 00:00:00.000000000 Z
40
- dependencies: []
40
+ dependencies:
41
+ - !ruby/object:Gem::Dependency
42
+ name: process-metrics
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.9'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.9'
41
55
  executables: []
42
56
  extensions: []
43
57
  extra_rdoc_files: []
@@ -45,7 +59,6 @@ files:
45
59
  - lib/memory/leak.rb
46
60
  - lib/memory/leak/cluster.rb
47
61
  - lib/memory/leak/monitor.rb
48
- - lib/memory/leak/system.rb
49
62
  - lib/memory/leak/version.rb
50
63
  - license.md
51
64
  - readme.md
metadata.gz.sig CHANGED
Binary file
@@ -1,82 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2025-2026, by Samuel Williams.
5
-
6
- require "console"
7
-
8
- module Memory
9
- module Leak
10
- # System-specific memory information.
11
- module System
12
- # Determine the total memory size in bytes. This is the maximum amount of memory that can be used by the current process. If running in a container, this may be limited by the container runtime (e.g. cgroups).
13
- #
14
- # @returns [Integer] The total memory size in bytes.
15
- def self.total_memory_size
16
- # Check for Kubernetes/cgroup memory limit first (cgroups v2):
17
- if File.exist?("/sys/fs/cgroup/memory.max")
18
- limit = File.read("/sys/fs/cgroup/memory.max").strip
19
- # "max" means unlimited, fall through to other methods
20
- if limit != "max"
21
- return limit.to_i
22
- end
23
- end
24
-
25
- # Check for Kubernetes/cgroup memory limit (cgroups v1):
26
- if File.exist?("/sys/fs/cgroup/memory/memory.limit_in_bytes")
27
- limit = File.read("/sys/fs/cgroup/memory/memory.limit_in_bytes").strip
28
- # Very large number (like 9223372036854771712) means unlimited, fall through
29
- if limit.to_i < 2**50 # Reasonable upper bound for actual limits
30
- return limit.to_i
31
- end
32
- end
33
-
34
- # Fall back to Linux system memory detection:
35
- if File.exist?("/proc/meminfo")
36
- File.foreach("/proc/meminfo") do |line|
37
- if /MemTotal:\s*(?<total>\d+)\s*kB/ =~ line
38
- return total.to_i * 1024
39
- end
40
- end
41
- end
42
-
43
- # Fall back to macOS memory detection:
44
- if RUBY_PLATFORM =~ /darwin/
45
- IO.popen(["sysctl", "hw.memsize"], "r") do |io|
46
- io.each_line do |line|
47
- if /hw.memsize:\s*(?<total>\d+)/ =~ line
48
- return total.to_i
49
- end
50
- end
51
- end
52
- end
53
- end
54
-
55
- # Get the memory usage of the given process IDs.
56
- #
57
- # @parameter process_ids [Array(Integer)] The process IDs to monitor.
58
- # @returns [Array(Tuple(Integer, Integer))] The memory usage of the given process IDs.
59
- def self.memory_usages(process_ids)
60
- return to_enum(__method__, process_ids) unless block_given?
61
-
62
- if process_ids.any?
63
- IO.popen(["ps", "-o", "pid=,rss=", "-p", process_ids.join(",")]) do |io|
64
- io.each_line.map(&:split).each do |process_id, size|
65
- yield process_id.to_i, size.to_i * 1024
66
- end
67
- end
68
- end
69
- end
70
-
71
- # Get the memory usage of the given process IDs.
72
- #
73
- # @parameter process_ids [Array(Integer)] The process IDs to monitor.
74
- # @returns [Array(Tuple(Integer, Integer))] The memory usage of the given process IDs.
75
- def self.memory_usage(process_id)
76
- IO.popen(["ps", "-o", "rss=", "-p", process_id.to_s]) do |io|
77
- return io.read.to_i * 1024
78
- end
79
- end
80
- end
81
- end
82
- end