memory-tracker-x 0.1.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.
@@ -0,0 +1,270 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require_relative "capture"
7
+ require_relative "call_tree"
8
+
9
+ module Memory
10
+ module Tracker
11
+ # Periodic sampler for monitoring memory growth over time.
12
+ #
13
+ # Samples class allocation counts at regular intervals and detects potential memory leaks
14
+ # by tracking when counts increase beyond a threshold. When a class exceeds the increases
15
+ # threshold, automatically enables detailed call path tracking for diagnosis.
16
+ class Sampler
17
+ # Tracks memory growth for a specific class.
18
+ #
19
+ # Records allocation counts over time and detects sustained growth patterns
20
+ # that indicate potential memory leaks.
21
+ class Sample
22
+ # Create a new sample tracker for a class.
23
+ #
24
+ # @parameter target [Class] The class being sampled.
25
+ # @parameter size [Integer] Initial object count.
26
+ # @parameter threshold [Integer] Minimum increase to consider significant.
27
+ def initialize(target, size = 0, threshold: 1000)
28
+ @target = target
29
+ @current_size = size
30
+ @maximum_observed_size = size
31
+ @threshold = threshold
32
+
33
+ @sample_count = 0
34
+ @increases = 0
35
+ end
36
+
37
+ attr_reader :target, :current_size, :maximum_observed_size, :threshold, :sample_count, :increases
38
+
39
+ # Record a new sample measurement.
40
+ #
41
+ # @parameter size [Integer] Current object count for this class.
42
+ # @returns [Boolean] True if count increased significantly.
43
+ def sample!(size)
44
+ @sample_count += 1
45
+ @current_size = size
46
+
47
+ # @maximum_observed_count ratchets up in units of at least @threshold counts.
48
+ # When it does, we bump @increases to track a potential memory leak.
49
+ if @maximum_observed_size
50
+ delta = @current_size - @maximum_observed_size
51
+ if delta > @threshold
52
+ @maximum_observed_size = size
53
+ @increases += 1
54
+
55
+ return true
56
+ end
57
+ else
58
+ @maximum_observed_size = size
59
+ end
60
+
61
+ return false
62
+ end
63
+
64
+ # Convert sample data to JSON-compatible hash.
65
+ #
66
+ # @returns [Hash] Sample data as a hash.
67
+ def as_json(...)
68
+ {
69
+ target: @target.name || "(anonymous class)",
70
+ current_size: @current_size,
71
+ maximum_observed_size: @maximum_observed_size,
72
+ increases: @increases,
73
+ sample_count: @sample_count,
74
+ threshold: @threshold,
75
+ }
76
+ end
77
+
78
+ # Convert sample data to JSON string.
79
+ #
80
+ # @returns [String] Sample data as JSON.
81
+ def to_json(...)
82
+ as_json.to_json(...)
83
+ end
84
+ end
85
+
86
+ attr_reader :depth
87
+
88
+ # Create a new memory sampler.
89
+ #
90
+ # @parameter depth [Integer] Number of stack frames to capture for call path analysis.
91
+ # @parameter filter [Proc] Optional filter to exclude frames from call paths.
92
+ # @parameter increases_threshold [Integer] Number of increases before enabling detailed tracking.
93
+ def initialize(depth: 10, filter: nil, increases_threshold: 10)
94
+ @depth = depth
95
+ @filter = filter || default_filter
96
+ @increases_threshold = increases_threshold
97
+ @capture = Capture.new
98
+ @call_trees = {}
99
+ @samples = {}
100
+ end
101
+
102
+ # Start capturing allocations.
103
+ def start
104
+ @capture.start
105
+ end
106
+
107
+ # Stop capturing allocations.
108
+ def stop
109
+ @capture.stop
110
+ end
111
+
112
+ # Run periodic sampling in a loop.
113
+ #
114
+ # Samples allocation counts at the specified interval and reports when
115
+ # classes show sustained memory growth. Automatically tracks ALL classes
116
+ # that allocate objects - no need to specify them upfront.
117
+ #
118
+ # @parameter interval [Numeric] Seconds between samples.
119
+ # @yields {|sample| ...} Called when a class shows significant growth.
120
+ def run(interval: 60, &block)
121
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
122
+
123
+ while true
124
+ sample!(&block)
125
+
126
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
127
+ delta = interval - (now - start_time)
128
+ sleep(delta) if delta > 0
129
+ start_time = now
130
+ end
131
+ end
132
+
133
+ # Take a single sample of memory usage for all tracked classes.
134
+ #
135
+ # @yields {|sample| ...} Called when a class shows significant growth.
136
+ def sample!
137
+ @capture.each do |klass, allocations|
138
+ count = allocations.retained_count
139
+ sample = @samples[klass] ||= Sample.new(klass, count)
140
+
141
+ if sample.sample!(count)
142
+ # Check if we should enable detailed tracking
143
+ if sample.increases >= @increases_threshold && !@call_trees.key?(klass)
144
+ track_with_analysis_internal(klass, allocations)
145
+ end
146
+
147
+ # Notify about growth if block given
148
+ yield sample if block_given?
149
+ end
150
+ end
151
+ end
152
+
153
+ # Internal: Enable tracking with analysis using allocations object
154
+ private def track_with_analysis_internal(klass, allocations)
155
+ tree = @call_trees[klass] = CallTree.new
156
+ depth = @depth
157
+ filter = @filter
158
+
159
+ # Register callback on allocations object
160
+ allocations.track do |obj_klass|
161
+ # Callback captures caller_locations with desired depth
162
+ locations = caller_locations(1, depth)
163
+ filtered = locations.select(&filter)
164
+ tree.record(filtered) unless filtered.empty?
165
+ end
166
+ end
167
+
168
+ # Start tracking allocations for a class (count only).
169
+ def track(klass)
170
+ return if @capture.tracking?(klass)
171
+
172
+ @capture.track(klass)
173
+ end
174
+
175
+ # Start tracking with call path analysis.
176
+ #
177
+ # @parameter klass [Class] The class to track with detailed analysis.
178
+ def track_with_analysis(klass)
179
+ # Track the class if not already tracked
180
+ unless @capture.tracking?(klass)
181
+ @capture.track(klass)
182
+ end
183
+
184
+ # Enable analysis by setting callback on the allocations object
185
+ @capture.each do |tracked_klass, allocations|
186
+ if tracked_klass == klass
187
+ track_with_analysis_internal(klass, allocations)
188
+ break
189
+ end
190
+ end
191
+ end
192
+
193
+ # Stop tracking a specific class.
194
+ def untrack(klass)
195
+ @capture.untrack(klass)
196
+ @call_trees.delete(klass)
197
+ end
198
+
199
+ # Check if a class is being tracked.
200
+ def tracking?(klass)
201
+ @capture.tracking?(klass)
202
+ end
203
+
204
+ # Get live object count for a class.
205
+ def count(klass)
206
+ @capture.count_for(klass)
207
+ end
208
+
209
+ # Get the call tree for a specific class.
210
+ def call_tree(klass)
211
+ @call_trees[klass]
212
+ end
213
+
214
+ # Get allocation statistics for a tracked class.
215
+ def statistics(klass)
216
+ tree = @call_trees[klass]
217
+ return nil unless tree
218
+
219
+ {
220
+ live_count: @capture.count_for(klass),
221
+ total_allocations: tree.total_allocations,
222
+ top_paths: tree.top_paths(10).map {|path, count|
223
+ { path: path, count: count }
224
+ },
225
+ hotspots: tree.hotspots(20)
226
+ }
227
+ end
228
+
229
+ # Get statistics for all tracked classes.
230
+ def all_statistics
231
+ @call_trees.keys.each_with_object({}) do |klass, result|
232
+ result[klass] = statistics(klass) if tracking?(klass)
233
+ end
234
+ end
235
+
236
+ # Clear tracking data for a class.
237
+ def clear(klass)
238
+ tree = @call_trees[klass]
239
+ tree&.clear!
240
+ end
241
+
242
+ # Clear all tracking data.
243
+ def clear_all!
244
+ @call_trees.each_value(&:clear!)
245
+ @capture.clear
246
+ end
247
+
248
+ # Stop all tracking and clean up.
249
+ def stop!
250
+ @capture.stop
251
+ @call_trees.each_key do |klass|
252
+ @capture.untrack(klass)
253
+ end
254
+ @capture.clear
255
+ @call_trees.clear
256
+ end
257
+
258
+ private
259
+
260
+ def default_filter
261
+ ->(location) {path = location.path
262
+ !path.include?("/gems/") &&
263
+ !path.include?("/ruby/") &&
264
+ !path.start_with?("(eval)")
265
+ }
266
+ end
267
+ end
268
+ end
269
+ end
270
+
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ # @namespace
7
+ module Memory
8
+ # @namespace
9
+ module Tracker
10
+ VERSION = "0.1.0"
11
+ end
12
+ end
13
+
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require_relative "tracker/version"
7
+ require_relative "tracker/call_tree"
8
+ require_relative "tracker/capture"
9
+ require_relative "tracker/sampler"
data/license.md ADDED
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright, 2025, by Samuel Williams.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/readme.md ADDED
@@ -0,0 +1,45 @@
1
+ # Memory::Tracker
2
+
3
+ Efficient memory allocation tracking focused on **retained objects only**. Automatically tracks allocations and cleans up when objects are freed, giving you precise data on memory leaks.
4
+
5
+ [![Development Status](https://github.com/socketry/memory-tracker/workflows/Test/badge.svg)](https://github.com/socketry/memory-tracker/actions?workflow=Test)
6
+
7
+ ## Features
8
+
9
+ - **Retained Objects Only**: Uses `RUBY_INTERNAL_EVENT_NEWOBJ` and `RUBY_INTERNAL_EVENT_FREEOBJ` to automatically track only objects that survive GC.
10
+ - **O(1) Live Counts**: Maintains per-class counters updated on alloc/free - no heap enumeration needed\!
11
+ - **Tree-Based Analysis**: Deduplicates common call paths using an efficient tree structure.
12
+ - **Native C Extension**: **Required** - uses Ruby internal events not available in pure Ruby.
13
+ - **Configurable Depth**: Control how deep to capture call stacks.
14
+
15
+ ## Usage
16
+
17
+ Please see the [project documentation](https://socketry.github.io/memory-tracker/) for more details.
18
+
19
+ - [Getting Started](https://socketry.github.io/memory-tracker/guides/getting-started/index) - This guide explains how to use `memory-tracker` to detect and diagnose memory leaks in Ruby applications.
20
+
21
+ ## Releases
22
+
23
+ Please see the [project releases](https://socketry.github.io/memory-tracker/releases/index) for all releases.
24
+
25
+ ### v0.1.0
26
+
27
+ - Initial implementation.
28
+
29
+ ## Contributing
30
+
31
+ We welcome contributions to this project.
32
+
33
+ 1. Fork it.
34
+ 2. Create your feature branch (`git checkout -b my-new-feature`).
35
+ 3. Commit your changes (`git commit -am 'Add some feature'`).
36
+ 4. Push to the branch (`git push origin my-new-feature`).
37
+ 5. Create new Pull Request.
38
+
39
+ ### Developer Certificate of Origin
40
+
41
+ In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
42
+
43
+ ### Community Guidelines
44
+
45
+ This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
data/releases.md ADDED
@@ -0,0 +1,5 @@
1
+ # Releases
2
+
3
+ ## v0.1.0
4
+
5
+ - Initial implementation.
data.tar.gz.sig ADDED
Binary file
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: memory-tracker-x
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Samuel Williams
8
+ bindir: bin
9
+ cert_chain:
10
+ - |
11
+ -----BEGIN CERTIFICATE-----
12
+ MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11
13
+ ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK
14
+ CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz
15
+ MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd
16
+ MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj
17
+ bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB
18
+ igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2
19
+ 9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW
20
+ sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE
21
+ e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN
22
+ XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss
23
+ RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn
24
+ tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM
25
+ zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW
26
+ xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O
27
+ BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs
28
+ aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs
29
+ aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE
30
+ cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl
31
+ xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/
32
+ c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp
33
+ 8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws
34
+ JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP
35
+ eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt
36
+ Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
37
+ voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
38
+ -----END CERTIFICATE-----
39
+ date: 1980-01-02 00:00:00.000000000 Z
40
+ dependencies: []
41
+ executables: []
42
+ extensions:
43
+ - ext/extconf.rb
44
+ extra_rdoc_files: []
45
+ files:
46
+ - context/getting-started.md
47
+ - context/index.yaml
48
+ - ext/extconf.rb
49
+ - ext/memory/tracker/capture.c
50
+ - ext/memory/tracker/capture.h
51
+ - ext/memory/tracker/tracker.c
52
+ - lib/memory/tracker.rb
53
+ - lib/memory/tracker/call_tree.rb
54
+ - lib/memory/tracker/capture.rb
55
+ - lib/memory/tracker/sampler.rb
56
+ - lib/memory/tracker/version.rb
57
+ - license.md
58
+ - readme.md
59
+ - releases.md
60
+ homepage: https://github.com/socketry/memory-tracker
61
+ licenses:
62
+ - MIT
63
+ metadata:
64
+ documentation_uri: https://socketry.github.io/memory-tracker/
65
+ source_code_uri: https://github.com/socketry/memory-tracker
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '3.2'
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubygems_version: 3.6.9
81
+ specification_version: 4
82
+ summary: Efficient memory allocation tracking with call path analysis.
83
+ test_files: []
metadata.gz.sig ADDED
Binary file