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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/lib/memory/leak/cluster.rb +36 -18
- data/lib/memory/leak/monitor.rb +28 -5
- data/lib/memory/leak/version.rb +1 -1
- data/readme.md +9 -7
- data/releases.md +8 -8
- data.tar.gz.sig +0 -0
- metadata +16 -3
- metadata.gz.sig +0 -0
- data/lib/memory/leak/system.rb +0 -82
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 37f012e5be6339c13ac1ed6be815e5f88d076b0c32809b65390f3666fa80511a
|
|
4
|
+
data.tar.gz: 7be83bdf7182cff46548332674a1cb1faa3aeedf3689d8d02ba741d6720a9f11
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4dd8a56dd4dd2b812a42cb0537ed701f91f0ffe75b53020805d846dd89e009240551b05f918f5d2d251cc17edf7b92dea84651c1240698052976cb444fc35121
|
|
7
|
+
data.tar.gz: 35674c676112e87c1aefc0cb36078dc6da5f927787bd9529d09c27a35dba8141fccea77f410bf61807c817054913582af85b80f788c5e92b4e564c9fb6a4f431
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/lib/memory/leak/cluster.rb
CHANGED
|
@@ -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
|
-
#
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
88
|
+
monitors.each do |monitor|
|
|
75
89
|
if @total_size > total_size_limit
|
|
76
|
-
|
|
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
|
-
#
|
|
79
|
-
|
|
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
|
-
|
|
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
|
data/lib/memory/leak/monitor.rb
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# Copyright, 2025, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require "console"
|
|
7
|
-
|
|
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
|
-
|
|
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!
|
|
146
|
+
def sample!
|
|
133
147
|
@sample_count += 1
|
|
134
148
|
|
|
135
|
-
|
|
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: @
|
|
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)
|
data/lib/memory/leak/version.rb
CHANGED
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
|
-
|
|
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.
|
|
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
|
data/lib/memory/leak/system.rb
DELETED
|
@@ -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
|