memory-leak 0.1.0 → 0.3.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: 8b50558351a2324cdddac0369f5b4f81d8787a1b9cee2d92206c997777637ae4
4
- data.tar.gz: 7091170b19d36840768f145dbba9bd8f805724af1a2332b60f8ac3df40bb5ed3
3
+ metadata.gz: 426ae960c62375c639420dcf7f498e9e5226b1519614b1fff452b2212f70d7fe
4
+ data.tar.gz: 6002732a1fb1e17fc105ca6528a510e3d0f564827ff7197682286e68f98a0da9
5
5
  SHA512:
6
- metadata.gz: 4edd0b28026f560e48717721f7f6afcb10798710654758637320ed8dabcd6af8306d68f69d825ab901c78eca027e1172e54e1c970204f2cc7a5e5ac8a0686d0b
7
- data.tar.gz: 959f65bc8f7e403cd482d0519d1768e5226b0ed95995dc52a0dca3c9fbc03893e9df7a54b4ad32b49273e1d3919911d93c8965d5e5927157b67c06b80c8da99d
6
+ metadata.gz: f36940083fb9907c227bff8242af9ec35a64951f9621b8581adce24cfbb8b2510c7810415d04125e201b36838a06775d9204a6dfc953c780134dec2115924f6d
7
+ data.tar.gz: 4e727229e8b12a4c73b88066c423f4683a3e9b21fe29c42214eab422c6e310463dc71152c539d5f6cd302e08b9f0ecc3e7166e7620d031c543d90483a3f7e134
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require "console"
7
+ require_relative "monitor"
8
+
9
+ module Memory
10
+ module Leak
11
+ # Detects memory leaks in a cluster of processes.
12
+ #
13
+ # 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
+ class Cluster
15
+ # Create a new cluster.
16
+ #
17
+ # @parameter limit [Numeric | Nil] The (total) memory limit for the cluster.
18
+ def initialize(limit: nil)
19
+ @limit = limit
20
+
21
+ @processes = {}
22
+ end
23
+
24
+ # @returns [Hash] A serializable representation of the cluster.
25
+ def as_json(...)
26
+ {
27
+ limit: @limit,
28
+ processes: @processes.transform_values(&:as_json),
29
+ }
30
+ end
31
+
32
+ # @returns [String] The JSON representation of the cluster.
33
+ def to_json(...)
34
+ as_json.to_json(...)
35
+ end
36
+
37
+ # @attribute [Numeric | Nil] The memory limit for the cluster.
38
+ attr_accessor :limit
39
+
40
+ # @attribute [Hash(Integer, Monitor)] The process IDs and monitors in the cluster.
41
+ attr :processes
42
+
43
+ # Add a new process ID to the cluster.
44
+ def add(process_id, **options)
45
+ @processes[process_id] = Monitor.new(process_id, **options)
46
+ end
47
+
48
+ # Remove a process ID from the cluster.
49
+ def remove(process_id)
50
+ @processes.delete(process_id)
51
+ end
52
+
53
+ # 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
+ #
55
+ # @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
58
+
59
+ if total > limit
60
+ Console.warn(self, "Total memory usage exceeded limit.", total: total, limit: limit)
61
+ end
62
+
63
+ sorted = @processes.sort_by do |process_id, monitor|
64
+ -monitor.current
65
+ end
66
+
67
+ sorted.each do |process_id, monitor|
68
+ if total > limit
69
+ if yield process_id, monitor, total
70
+ total -= monitor.current
71
+ end
72
+ else
73
+ break
74
+ end
75
+ end
76
+ end
77
+
78
+ # Check all processes in the cluster for memory leaks.
79
+ #
80
+ # @yields {|process_id, monitor| ...} each process ID and monitor that is leaking or exceeds the memory limit.
81
+ def check!(&block)
82
+ leaking = []
83
+
84
+ @processes.each do |process_id, monitor|
85
+ monitor.sample!
86
+
87
+ if monitor.leaking?
88
+ Console.debug(self, "Memory Leak Detected!", process_id: process_id, monitor: monitor)
89
+
90
+ leaking << [process_id, monitor]
91
+ end
92
+ end
93
+
94
+ leaking.each(&block)
95
+
96
+ # Finally, apply any per-cluster memory limits:
97
+ apply_limit!(@limit, &block) if @limit
98
+ end
99
+ end
100
+ end
101
+ end
@@ -12,32 +12,54 @@ module Memory
12
12
  # 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
13
  #
14
14
  # 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
- class Detector
16
- # We only track heap size changes greater than this threshold (KB), across the DEFAULT_INTERVAL.
15
+ class Monitor
16
+ # We only track heap size changes greater than this threshold, across the DEFAULT_INTERVAL.
17
17
  # True memory leaks will eventually hit this threshold, while small fluctuations will not.
18
- DEFAULT_THRESHOLD = 1024*10
18
+ DEFAULT_THRESHOLD = 1024*1024*10
19
19
 
20
20
  # We track the last N heap size increases.
21
21
  # If the heap size is not stabilizing within the specified limit, we can assume there is a leak.
22
22
  # With a default interval of 10 seconds, this will track the last ~3 minutes of heap size increases.
23
23
  DEFAULT_LIMIT = 20
24
24
 
25
- # Create a new detector.
25
+ # Create a new monitor.
26
26
  #
27
- # @parameter maximum [Numeric] The initial maximum heap size, from which we willl track increases, in KiB.
28
- # @parameter threshold [Numeric] The threshold for heap size increases, in KiB.
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
29
  # @parameter limit [Numeric] The limit for the number of heap size increases, before we assume a memory leak.
30
- # @pid [Integer] The process ID to monitor.
31
- def initialize(maximum: nil, threshold: DEFAULT_THRESHOLD, limit: DEFAULT_LIMIT, pid: Process.pid)
30
+ # @parameter [Integer] The process ID to monitor.
31
+ def initialize(process_id = Process.pid, maximum: nil, threshold: DEFAULT_THRESHOLD, limit: DEFAULT_LIMIT)
32
+ @process_id = process_id
33
+
32
34
  @maximum = maximum
33
35
  @threshold = threshold
34
36
  @limit = limit
35
- @pid = pid
36
37
 
37
38
  # The number of increasing heap size samples.
38
39
  @count = 0
40
+ @current = nil
41
+ end
42
+
43
+ # @returns [Hash] A serializable representation of the cluster.
44
+ def as_json(...)
45
+ {
46
+ process_id: @process_id,
47
+ current: @current,
48
+ maximum: @maximum,
49
+ threshold: @threshold,
50
+ limit: @limit,
51
+ count: @count,
52
+ }
53
+ end
54
+
55
+ # @returns [String] The JSON representation of the cluster.
56
+ def to_json(...)
57
+ as_json.to_json(...)
39
58
  end
40
59
 
60
+ # @attribute [Integer] The process ID to monitor.
61
+ attr :process_id
62
+
41
63
  # @attribute [Numeric] The current maximum heap size.
42
64
  attr :maximum
43
65
 
@@ -54,44 +76,49 @@ module Memory
54
76
  #
55
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.
56
78
  #
57
- # @returns [Numeric] Memory usage size in KiB.
58
- def memory_usage(pid = @pid)
59
- IO.popen(["ps", "-o", "rss=", pid.to_s]) do |io|
60
- return Integer(io.readlines.last)
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
61
83
  end
62
84
  end
63
85
 
86
+ # @returns [Integer] The last sampled memory usage.
87
+ def current
88
+ @current ||= memory_usage
89
+ end
90
+
64
91
  # Indicates whether a memory leak has been detected.
65
92
  #
66
93
  # If the number of increasing heap size samples is greater than or equal to the limit, a memory leak is assumed.
67
94
  #
68
95
  # @returns [Boolean] True if a memory leak has been detected.
69
- def memory_leak_detected?
96
+ def leaking?
70
97
  @count >= @limit
71
98
  end
72
99
 
73
100
  # Capture a memory usage sample and yield if a memory leak is detected.
74
101
  #
75
- # @yields {|sample, detector| ...} If a memory leak is detected.
76
- def capture_sample
77
- sample = memory_usage
102
+ # @yields {|sample, monitor| ...} If a memory leak is detected.
103
+ def sample!
104
+ @current = memory_usage
78
105
 
79
106
  if @maximum
80
- delta = sample - @maximum
81
- Console.debug(self, "Heap size captured.", sample: sample, delta: delta, threshold: @threshold, maximum: @maximum)
107
+ delta = @current - @maximum
108
+ Console.debug(self, "Heap size captured.", current: @current, delta: delta, threshold: @threshold, maximum: @maximum)
82
109
 
83
110
  if delta > @threshold
84
- @maximum = sample
111
+ @maximum = @current
85
112
  @count += 1
86
113
 
87
114
  Console.debug(self, "Heap size increased.", maximum: @maximum, count: @count)
88
115
  end
89
116
  else
90
- Console.debug(self, "Initial heap size captured.", sample: sample)
91
- @maximum = sample
117
+ Console.debug(self, "Initial heap size captured.", current: @current)
118
+ @maximum = @current
92
119
  end
93
120
 
94
- return sample
121
+ return @current
95
122
  end
96
123
  end
97
124
  end
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Memory
7
7
  module Leak
8
- VERSION = "0.1.0"
8
+ VERSION = "0.3.0"
9
9
  end
10
10
  end
data/lib/memory/leak.rb CHANGED
@@ -4,7 +4,7 @@
4
4
  # Copyright, 2025, by Samuel Williams.
5
5
 
6
6
  require_relative "leak/version"
7
- require_relative "leak/detector"
7
+ require_relative "leak/monitor"
8
8
 
9
9
  # @namespace
10
10
  module Memory
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.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -36,14 +36,15 @@ cert_chain:
36
36
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
37
37
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
38
38
  -----END CERTIFICATE-----
39
- date: 2025-02-24 00:00:00.000000000 Z
39
+ date: 2025-02-26 00:00:00.000000000 Z
40
40
  dependencies: []
41
41
  executables: []
42
42
  extensions: []
43
43
  extra_rdoc_files: []
44
44
  files:
45
45
  - lib/memory/leak.rb
46
- - lib/memory/leak/detector.rb
46
+ - lib/memory/leak/cluster.rb
47
+ - lib/memory/leak/monitor.rb
47
48
  - lib/memory/leak/version.rb
48
49
  - license.md
49
50
  - readme.md
@@ -70,5 +71,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
70
71
  requirements: []
71
72
  rubygems_version: 3.6.2
72
73
  specification_version: 4
73
- summary: A memory leak detector.
74
+ summary: A memory leak monitor.
74
75
  test_files: []
metadata.gz.sig CHANGED
Binary file