memory-leak 0.7.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: 6ab8d8ab5e5ebff81f2847704ece1e34c20c71aa35a361caea8eafccc4b39f6f
4
- data.tar.gz: e0a1dce4599132dc7a5aff55d1d639efb50e4d5f2b7c7f0f81289100471c8682
3
+ metadata.gz: 37f012e5be6339c13ac1ed6be815e5f88d076b0c32809b65390f3666fa80511a
4
+ data.tar.gz: 7be83bdf7182cff46548332674a1cb1faa3aeedf3689d8d02ba741d6720a9f11
5
5
  SHA512:
6
- metadata.gz: cff70665edfcdf6d0e3d6ed7510586deb57fbd04f16476e5795acb73635688dec1f455a343b7bd3ba595a1e00a84dd65a34b658fecb11ad0b9fb2ff093e174e5
7
- data.tar.gz: 9a09bb3be13452c240289f6d5433fc11c46213d987ac2abea1db6f6c2484f8453670413d9a48555e57d9cbbbd0918d91049ebb4d05306f74fa683efd8e839829
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.7.0"
8
+ VERSION = "0.9.0"
9
9
  end
10
10
  end
data/license.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # MIT License
2
2
 
3
- Copyright, 2025, by Samuel Williams.
3
+ Copyright, 2025-2026, by Samuel Williams.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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`).
data/releases.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Releases
2
2
 
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.
11
+
3
12
  ## v0.7.0
4
13
 
5
14
  - Make both `increase_limit` and `maximum_size_limit` optional (if `nil`).
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.7.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
@@ -70,7 +83,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
70
83
  - !ruby/object:Gem::Version
71
84
  version: '0'
72
85
  requirements: []
73
- rubygems_version: 3.7.2
86
+ rubygems_version: 4.0.3
74
87
  specification_version: 4
75
88
  summary: A memory leak monitor.
76
89
  test_files: []
metadata.gz.sig CHANGED
Binary file
@@ -1,61 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2025, by Samuel Williams.
5
-
6
- require "console"
7
-
8
- module Memory
9
- module Leak
10
- # System-specific memory information.
11
- module System
12
- if File.exist?("/proc/meminfo")
13
- # @returns [Integer] The total memory size in bytes.
14
- def self.total_memory_size
15
- File.foreach("/proc/meminfo") do |line|
16
- if /MemTotal:\s*(?<total>\d+)\s*kB/ =~ line
17
- return total.to_i * 1024
18
- end
19
- end
20
- end
21
- elsif RUBY_PLATFORM =~ /darwin/
22
- # @returns [Integer] The total memory size in bytes.
23
- def self.total_memory_size
24
- IO.popen(["sysctl", "hw.memsize"], "r") do |io|
25
- io.each_line do |line|
26
- if /hw.memsize:\s*(?<total>\d+)/ =~ line
27
- return total.to_i
28
- end
29
- end
30
- end
31
- end
32
- end
33
-
34
- # Get the memory usage of the given process IDs.
35
- #
36
- # @parameter process_ids [Array(Integer)] The process IDs to monitor.
37
- # @returns [Array(Tuple(Integer, Integer))] The memory usage of the given process IDs.
38
- def self.memory_usages(process_ids)
39
- return to_enum(__method__, process_ids) unless block_given?
40
-
41
- if process_ids.any?
42
- IO.popen(["ps", "-o", "pid=,rss=", "-p", process_ids.join(",")]) do |io|
43
- io.each_line.map(&:split).each do |process_id, size|
44
- yield process_id.to_i, size.to_i * 1024
45
- end
46
- end
47
- end
48
- end
49
-
50
- # Get the memory usage of the given process IDs.
51
- #
52
- # @parameter process_ids [Array(Integer)] The process IDs to monitor.
53
- # @returns [Array(Tuple(Integer, Integer))] The memory usage of the given process IDs.
54
- def self.memory_usage(process_id)
55
- IO.popen(["ps", "-o", "rss=", "-p", process_id.to_s]) do |io|
56
- return io.read.to_i * 1024
57
- end
58
- end
59
- end
60
- end
61
- end