memory-leak 0.4.0 → 0.5.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: 831ea4087e21cc1b7a9ba7b7dadd3239b1b3ef77aa6a9eccb26392880b2b4068
4
- data.tar.gz: 680ec100fe3017a15a8fc9b5e75c68c70e8d28e46a439737f0e4e53e715da9a9
3
+ metadata.gz: 14b3c3712d827c78c74132d04b0488e5572ede9a4ff1451e5df6990c8931007f
4
+ data.tar.gz: f055ae13339a94c15fe4ee52ec264cde3e6ab764e84e3f358e94477c3f3e5753
5
5
  SHA512:
6
- metadata.gz: 86abd84862289e63d20123f222b35493c051f4daa0f15e386e988b1269702dae0f888a4113d8f97fbf0c9d4240cf0c061d478e4a709436e497c0736dfbca250c
7
- data.tar.gz: b8d92826ffd0aa9fb42dc3209e60d46fc685f739c3c1f4a5688b4a013f8e7c075fb39a47afb248e00c48c9f1e62c19ff6bb0bee992e127885a56a525dc2c3d4a
6
+ metadata.gz: cd92451ab8a82bc932279eff5d76c8a2b9e9b792bcc77e663b867a9ab8cbb9640dbe0b343841b1fdb74e770e1bf53abddbcd060bcfccecf47679c69cba5ddd57
7
+ data.tar.gz: de5bd472ca16f2819c991c9d5b542da3d5a62c161bd1d4b64098f14a956f980cbb5db247a749512f35a982eeae0a265f2ceffbbaf60485adfeb17a060530eb55
checksums.yaml.gz.sig CHANGED
Binary file
@@ -14,9 +14,10 @@ module Memory
14
14
  class Cluster
15
15
  # Create a new cluster.
16
16
  #
17
- # @parameter limit [Numeric | Nil] The (total) memory limit for the cluster.
18
- def initialize(limit: nil)
19
- @limit = limit
17
+ # @parameter total_size_limit [Numeric | Nil] The total memory limit for the cluster.
18
+ def initialize(total_size_limit: nil)
19
+ @total_size = nil
20
+ @total_size_limit = total_size_limit
20
21
 
21
22
  @processes = {}
22
23
  end
@@ -24,7 +25,8 @@ module Memory
24
25
  # @returns [Hash] A serializable representation of the cluster.
25
26
  def as_json(...)
26
27
  {
27
- limit: @limit,
28
+ total_size: @total_size,
29
+ total_size_limit: @total_size_limit,
28
30
  processes: @processes.transform_values(&:as_json),
29
31
  }
30
32
  end
@@ -34,8 +36,11 @@ module Memory
34
36
  as_json.to_json(...)
35
37
  end
36
38
 
37
- # @attribute [Numeric | Nil] The memory limit for the cluster.
38
- attr_accessor :limit
39
+ # @attribute [Numeric | Nil] The total size of the cluster.
40
+ attr :total_size
41
+
42
+ # @attribute [Numeric | Nil] The total size limit for the cluster, in bytes, if which is exceeded, the cluster will terminate processes.
43
+ attr_accessor :total_size_limit
39
44
 
40
45
  # @attribute [Hash(Integer, Monitor)] The process IDs and monitors in the cluster.
41
46
  attr :processes
@@ -53,37 +58,49 @@ module Memory
53
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.
54
59
  #
55
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.
56
- def apply_limit!(limit = @limit)
57
- total = @processes.values.map(&:current).sum
61
+ def apply_limit!(total_size_limit = @total_size_limit)
62
+ @total_size = @processes.values.map(&:current_size).sum
58
63
 
59
- if total > limit
60
- Console.warn(self, "Total memory usage exceeded limit.", total: total, limit: limit)
64
+ 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)
66
+ else
67
+ return false
61
68
  end
62
69
 
63
70
  sorted = @processes.sort_by do |process_id, monitor|
64
- -monitor.current
71
+ -monitor.current_size
65
72
  end
66
73
 
67
74
  sorted.each do |process_id, monitor|
68
- if total > limit
69
- if yield process_id, monitor, total
70
- total -= monitor.current
71
- end
75
+ if @total_size > total_size_limit
76
+ yield(process_id, monitor, @total_size)
77
+
78
+ # For the sake of the calculation, we assume that the process has been terminated:
79
+ @total_size -= monitor.current_size
72
80
  else
73
81
  break
74
82
  end
75
83
  end
76
84
  end
77
85
 
86
+ # Sample the memory usage of all processes in the cluster.
87
+ def sample!
88
+ System.memory_usages(@processes.keys).each do |process_id, memory_usage|
89
+ @processes[process_id].current_size = memory_usage
90
+ end
91
+ end
92
+
78
93
  # Check all processes in the cluster for memory leaks.
79
94
  #
80
95
  # @yields {|process_id, monitor| ...} each process ID and monitor that is leaking or exceeds the memory limit.
81
96
  def check!(&block)
97
+ return to_enum(__method__) unless block_given?
98
+
99
+ self.sample!
100
+
82
101
  leaking = []
83
102
 
84
103
  @processes.each do |process_id, monitor|
85
- monitor.sample!
86
-
87
104
  if monitor.leaking?
88
105
  Console.debug(self, "Memory Leak Detected!", process_id: process_id, monitor: monitor)
89
106
 
@@ -94,7 +111,7 @@ module Memory
94
111
  leaking.each(&block)
95
112
 
96
113
  # Finally, apply any per-cluster memory limits:
97
- apply_limit!(@limit, &block) if @limit
114
+ apply_limit!(@total_size_limit, &block) if @total_size_limit
98
115
  end
99
116
  end
100
117
  end
@@ -4,51 +4,54 @@
4
4
  # Copyright, 2025, by Samuel Williams.
5
5
 
6
6
  require "console"
7
+ require_relative "system"
7
8
 
8
9
  module Memory
9
10
  module Leak
10
- # Detects memory leaks by tracking heap size increases.
11
+ # Detects memory leaks by tracking process size increases.
11
12
  #
12
13
  # A memory leak is characterised by the memory usage of the application continuing to rise over time. We can detect this by sampling memory usage and comparing it to the previous sample. If the memory usage is higher than the previous sample, we can say that the application has allocated more memory. Eventually we expect to see this stabilize, but if it continues to rise, we can say that the application has a memory leak.
13
14
  #
14
15
  # We should be careful not to filter historical data, as some memory leaks may only become apparent after a long period of time. Any kind of filtering may prevent us from detecting such a leak.
15
16
  class Monitor
16
- # We only track heap size changes greater than this threshold, across the DEFAULT_INTERVAL.
17
- # True memory leaks will eventually hit this threshold, while small fluctuations will not.
18
- DEFAULT_THRESHOLD = 1024*1024*10
17
+ # We only track process size changes greater than this threshold_size, across the DEFAULT_INTERVAL.
18
+ # True memory leaks will eventually hit this threshold_size, while small fluctuations will not.
19
+ DEFAULT_THRESHOLD_SIZE = 1024*1024*10
19
20
 
20
- # We track the last N heap size increases.
21
- # If the heap size is not stabilizing within the specified limit, we can assume there is a leak.
22
- # With a default interval of 10 seconds, this will track the last ~3 minutes of heap size increases.
23
- DEFAULT_LIMIT = 20
21
+ # We track the last N process size increases.
22
+ # If the process size is not stabilizing within the specified increase_limit, we can assume there is a leak.
23
+ # With a default interval of 10 seconds, this will track the last ~3 minutes of process size increases.
24
+ DEFAULT_INCREASE_LIMIT = 20
24
25
 
25
26
  # Create a new monitor.
26
27
  #
27
- # @parameter maximum [Numeric] The initial maximum heap size, from which we willl track increases, in bytes.
28
- # @parameter threshold [Numeric] The threshold for heap size increases, in bytes.
29
- # @parameter limit [Numeric] The limit for the number of heap size increases, before we assume a memory leak.
30
- # @parameter [Integer] The process ID to monitor.
31
- def initialize(process_id = Process.pid, maximum: nil, threshold: DEFAULT_THRESHOLD, limit: DEFAULT_LIMIT)
28
+ # @parameter process_id [Integer] The process ID to monitor.
29
+ # @parameter maximum_size [Numeric] The initial process size, from which we willl track increases, in bytes.
30
+ # @parameter maximum_size_limit [Numeric | Nil] The maximum process size allowed, in bytes, before we assume a memory leak.
31
+ # @parameter threshold_size [Numeric] The threshold for process size increases, in bytes.
32
+ # @parameter increase_limit [Numeric] The limit for the number of process size increases, before we assume a memory leak.
33
+ def initialize(process_id = Process.pid, maximum_size: nil, maximum_size_limit: nil, threshold_size: DEFAULT_THRESHOLD_SIZE, increase_limit: DEFAULT_INCREASE_LIMIT)
32
34
  @process_id = process_id
33
35
 
34
- @maximum = maximum
35
- @threshold = threshold
36
- @limit = limit
36
+ @current_size = nil
37
+ @maximum_size = maximum_size
38
+ @maximum_size_limit = maximum_size_limit
37
39
 
38
- # The number of increasing heap size samples.
39
- @count = 0
40
- @current = nil
40
+ @threshold_size = threshold_size
41
+ @increase_count = 0
42
+ @increase_limit = increase_limit
41
43
  end
42
44
 
43
45
  # @returns [Hash] A serializable representation of the cluster.
44
46
  def as_json(...)
45
47
  {
46
48
  process_id: @process_id,
47
- current: @current,
48
- maximum: @maximum,
49
- threshold: @threshold,
50
- limit: @limit,
51
- count: @count,
49
+ current_size: @current_size,
50
+ maximum_size: @maximum_size,
51
+ maximum_size_limit: @maximum_size_limit,
52
+ threshold_size: @threshold_size,
53
+ increase_count: @increase_count,
54
+ increase_limit: @increase_limit,
52
55
  }
53
56
  end
54
57
 
@@ -60,65 +63,81 @@ module Memory
60
63
  # @attribute [Integer] The process ID to monitor.
61
64
  attr :process_id
62
65
 
63
- # @attribute [Numeric] The current maximum heap size.
64
- attr :maximum
66
+ # @attribute [Numeric] The maximum process size observed.
67
+ attr_accessor :maximum_size
65
68
 
66
- # @attribute [Numeric] The threshold for heap size increases.
67
- attr :threshold
69
+ # @attribute [Numeric | Nil] The maximum process size allowed, before we assume a memory leak.
70
+ attr_accessor :maximum_size_limit
68
71
 
69
- # @attribute [Numeric] The limit for the number of heap size increases, before we assume a memory leak.
70
- attr :limit
72
+ # @attribute [Numeric] The threshold_size for process size increases.
73
+ attr_accessor :threshold_size
71
74
 
72
- # @attribute [Integer] The number of increasing heap size samples.
73
- attr :count
75
+ # @attribute [Integer] The number of increasing process size samples.
76
+ attr_accessor :increase_count
74
77
 
75
- # The current resident set size (RSS) of the process.
76
- #
77
- # Even thought the absolute value of this number may not very useful, the relative change is useful for detecting memory leaks, and it works on most platforms.
78
- #
79
- # @returns [Numeric] Memory usage size in bytes.
80
- private def memory_usage
81
- IO.popen(["ps", "-o", "rss=", @process_id.to_s]) do |io|
82
- return Integer(io.readlines.last) * 1024
83
- end
78
+ # @attribute [Numeric] The limit for the number of process size increases, before we assume a memory leak.
79
+ attr_accessor :increase_limit
80
+
81
+ # @returns [Integer] Ask the system for the current memory usage.
82
+ def memory_usage
83
+ System.memory_usage(@process_id)
84
84
  end
85
85
 
86
86
  # @returns [Integer] The last sampled memory usage.
87
- def current
88
- @current ||= memory_usage
87
+ def current_size
88
+ @current_size ||= memory_usage
89
+ end
90
+
91
+ # Set the current memory usage, rather than sampling it.
92
+ def current_size=(value)
93
+ @current_size = value
89
94
  end
90
95
 
91
96
  # Indicates whether a memory leak has been detected.
92
97
  #
93
- # If the number of increasing heap size samples is greater than or equal to the limit, a memory leak is assumed.
98
+ # If the number of increasing heap size samples is greater than or equal to the increase_limit, a memory leak is assumed.
99
+ #
100
+ # @returns [Boolean] True if a memory leak has been detected.
101
+ def increase_limit_exceeded?
102
+ @increase_count >= @increase_limit
103
+ end
104
+
105
+ # Indicates that the current memory usage has grown beyond the maximum size limit.
106
+ #
107
+ # @returns [Boolean] True if the current memory usage has grown beyond the maximum size limit.
108
+ def maximum_size_limit_exceeded?
109
+ @maximum_size_limit && self.current_size > @maximum_size_limit
110
+ end
111
+
112
+ # Indicates whether a memory leak has been detected.
94
113
  #
95
114
  # @returns [Boolean] True if a memory leak has been detected.
96
115
  def leaking?
97
- @count >= @limit
116
+ increase_limit_exceeded? || maximum_size_limit_exceeded?
98
117
  end
99
118
 
100
119
  # Capture a memory usage sample and yield if a memory leak is detected.
101
120
  #
102
121
  # @yields {|sample, monitor| ...} If a memory leak is detected.
103
122
  def sample!
104
- @current = memory_usage
123
+ self.current_size = memory_usage
105
124
 
106
- if @maximum
107
- delta = @current - @maximum
108
- Console.debug(self, "Heap size captured.", current: @current, delta: delta, threshold: @threshold, maximum: @maximum)
125
+ if @maximum_observed_size
126
+ delta = @current_size - @maximum_observed_size
127
+ Console.debug(self, "Heap size captured.", current_size: @current_size, delta: delta, threshold_size: @threshold_size, maximum_observed_size: @maximum_observed_size)
109
128
 
110
- if delta > @threshold
111
- @maximum = @current
112
- @count += 1
129
+ if delta > @threshold_size
130
+ @maximum_observed_size = @current_size
131
+ @increase_count += 1
113
132
 
114
- Console.debug(self, "Heap size increased.", maximum: @maximum, count: @count)
133
+ Console.debug(self, "Heap size increased.", maximum_observed_size: @maximum_observed_size, count: @count)
115
134
  end
116
135
  else
117
- Console.debug(self, "Initial heap size captured.", current: @current)
118
- @maximum = @current
136
+ Console.debug(self, "Initial heap size captured.", current_size: @current_size)
137
+ @maximum_observed_size = @current_size
119
138
  end
120
139
 
121
- return @current
140
+ return @current_size
122
141
  end
123
142
  end
124
143
  end
@@ -7,18 +7,19 @@ require "console"
7
7
 
8
8
  module Memory
9
9
  module Leak
10
+ # System-specific memory information.
10
11
  module System
11
12
  if File.exist?("/proc/meminfo")
13
+ # @returns [Integer] The total memory size in bytes.
12
14
  def self.total_memory_size
13
15
  File.foreach("/proc/meminfo") do |line|
14
16
  if /MemTotal:\s*(?<total>\d+)\s*kB/ =~ line
15
17
  return total.to_i * 1024
16
18
  end
17
19
  end
18
-
19
- return nil
20
20
  end
21
21
  elsif RUBY_PLATFORM =~ /darwin/
22
+ # @returns [Integer] The total memory size in bytes.
22
23
  def self.total_memory_size
23
24
  IO.popen(["sysctl", "hw.memsize"], "r") do |io|
24
25
  io.each_line do |line|
@@ -29,6 +30,26 @@ module Memory
29
30
  end
30
31
  end
31
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
+ IO.popen(["ps", "-o", "pid=,rss=", *process_ids.map(&:to_s)]) do |io|
40
+ io.each_line.map(&:split).map{|process_id, size| [process_id.to_i, size.to_i * 1024]}
41
+ end
42
+ end
43
+
44
+ # Get the memory usage of the given process IDs.
45
+ #
46
+ # @parameter process_ids [Array(Integer)] The process IDs to monitor.
47
+ # @returns [Array(Tuple(Integer, Integer))] The memory usage of the given process IDs.
48
+ def self.memory_usage(process_id)
49
+ IO.popen(["ps", "-o", "rss=", process_id.to_s]) do |io|
50
+ return io.read.to_i * 1024
51
+ end
52
+ end
32
53
  end
33
54
  end
34
55
  end
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Memory
7
7
  module Leak
8
- VERSION = "0.4.0"
8
+ VERSION = "0.5.0"
9
9
  end
10
10
  end
data/readme.md CHANGED
@@ -12,6 +12,11 @@ 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.5.0
16
+
17
+ - Improved variable names.
18
+ - Added `maximum_size_limit` to process monitor.
19
+
15
20
  ### v0.1.0
16
21
 
17
22
  - Initial implementation.
data/releases.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Releases
2
2
 
3
+ ## v0.5.0
4
+
5
+ - Improved variable names.
6
+ - Added `maximum_size_limit` to process monitor.
7
+
3
8
  ## v0.1.0
4
9
 
5
10
  - Initial implementation.
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.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -36,7 +36,7 @@ cert_chain:
36
36
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
37
37
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
38
38
  -----END CERTIFICATE-----
39
- date: 2025-02-26 00:00:00.000000000 Z
39
+ date: 2025-02-28 00:00:00.000000000 Z
40
40
  dependencies: []
41
41
  executables: []
42
42
  extensions: []
metadata.gz.sig CHANGED
Binary file