memory-profiler 1.1.15 → 1.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: cce184966c5eb2b104059f419fb7f220b2bea168b5359adacc90923079332241
4
- data.tar.gz: 436690f9e75f5ba72546604fd58d71efccb56d4dc8b8880feaa3b4ecab03c918
3
+ metadata.gz: f059f309b7c1fd6fcffd119a0f6d40e8d4480783e2955ad3fc054fa280015f9d
4
+ data.tar.gz: e3c1ddd9afd7240b3bf727f6b6219b2bcb2e164c8e9c1c245ac4163684b180b8
5
5
  SHA512:
6
- metadata.gz: 54d4d1e2f8d19da788e53ffa0c5e69dcaf5db708d5d1de0336bccc79c6311330a56b0c8cb605b7bb7dbc116b91b91a752cacf317d4403e5188517ddcdded12a2
7
- data.tar.gz: 4994f81f0b6dd2b0c46bc3aa3e415dbc16ffb633d9fefdeafd67d10df522aaccc5003dd632a664b8eb69e476053bac5f51d0979c69c4daa97965356b1bb2ca9c
6
+ metadata.gz: 2bd209fd9a3c9f3116bf0c76b405c1fed1580f5412292646749539f73eb5e2b7334e2267a5bbd43fbc4c13fa7c8385422e32f369779e62f022966e56108a46c3
7
+ data.tar.gz: 6e38c7368db453b2476a0084bbcdca8c60df5d0c51d2395df5e23c18a2b4f49f3bbbd64f5e562af2a02e91a0cd73b6cc129000eb80f75e485afbfe3b9d769ff2
checksums.yaml.gz.sig CHANGED
Binary file
@@ -143,13 +143,24 @@ void Memory_Profiler_Allocations_clear(VALUE allocations) {
143
143
  }
144
144
  }
145
145
 
146
+ // Allocate a new Allocations object (for testing)
147
+ static VALUE Memory_Profiler_Allocations_allocate(VALUE klass) {
148
+ struct Memory_Profiler_Capture_Allocations *record = ALLOC(struct Memory_Profiler_Capture_Allocations);
149
+ record->callback = Qnil;
150
+ record->new_count = 0;
151
+ record->free_count = 0;
152
+ record->states = st_init_numtable();
153
+
154
+ return Memory_Profiler_Allocations_wrap(record);
155
+ }
156
+
146
157
  void Init_Memory_Profiler_Allocations(VALUE Memory_Profiler)
147
158
  {
148
159
  // Allocations class - wraps allocation data for a specific class
149
160
  Memory_Profiler_Allocations = rb_define_class_under(Memory_Profiler, "Allocations", rb_cObject);
150
161
 
151
- // Allocations objects are only created internally via wrap, never from Ruby:
152
- rb_undef_alloc_func(Memory_Profiler_Allocations);
162
+ // Allow allocation for testing
163
+ rb_define_alloc_func(Memory_Profiler_Allocations, Memory_Profiler_Allocations_allocate);
153
164
 
154
165
  rb_define_method(Memory_Profiler_Allocations, "new_count", Memory_Profiler_Allocations_new_count, 0);
155
166
  rb_define_method(Memory_Profiler_Allocations, "free_count", Memory_Profiler_Allocations_free_count, 0);
@@ -469,7 +469,7 @@ static VALUE Memory_Profiler_Capture_tracking_p(VALUE self, VALUE klass) {
469
469
  }
470
470
 
471
471
  // Get count of live objects for a specific class (O(1) lookup!)
472
- static VALUE Memory_Profiler_Capture_count_for(VALUE self, VALUE klass) {
472
+ static VALUE Memory_Profiler_Capture_retained_count_of(VALUE self, VALUE klass) {
473
473
  struct Memory_Profiler_Capture *capture;
474
474
  TypedData_Get_Struct(self, struct Memory_Profiler_Capture, &Memory_Profiler_Capture_type, capture);
475
475
 
@@ -641,7 +641,7 @@ void Init_Memory_Profiler_Capture(VALUE Memory_Profiler)
641
641
  rb_define_method(Memory_Profiler_Capture, "track", Memory_Profiler_Capture_track, -1); // -1 to accept block
642
642
  rb_define_method(Memory_Profiler_Capture, "untrack", Memory_Profiler_Capture_untrack, 1);
643
643
  rb_define_method(Memory_Profiler_Capture, "tracking?", Memory_Profiler_Capture_tracking_p, 1);
644
- rb_define_method(Memory_Profiler_Capture, "count_for", Memory_Profiler_Capture_count_for, 1);
644
+ rb_define_method(Memory_Profiler_Capture, "retained_count_of", Memory_Profiler_Capture_retained_count_of, 1);
645
645
  rb_define_method(Memory_Profiler_Capture, "each", Memory_Profiler_Capture_each, 0);
646
646
  rb_define_method(Memory_Profiler_Capture, "[]", Memory_Profiler_Capture_aref, 1);
647
647
  rb_define_method(Memory_Profiler_Capture, "clear", Memory_Profiler_Capture_clear, 0);
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require_relative "capture"
7
+
8
+ module Memory
9
+ module Profiler
10
+ # Ruby extensions to the C-defined Allocations class.
11
+ # The base Allocations class is defined in the C extension.
12
+ class Allocations
13
+ # Convert allocation statistics to JSON-compatible hash.
14
+ #
15
+ # @returns [Hash] Allocation statistics as a hash.
16
+ def as_json(...)
17
+ {
18
+ new_count: self.new_count,
19
+ free_count: self.free_count,
20
+ retained_count: self.retained_count,
21
+ }
22
+ end
23
+
24
+ # Convert allocation statistics to JSON string.
25
+ #
26
+ # @returns [String] Allocation statistics as JSON.
27
+ def to_json(...)
28
+ as_json.to_json(...)
29
+ end
30
+ end
31
+ end
32
+ end
33
+
@@ -187,7 +187,7 @@ module Memory
187
187
  # @parameter limit [Integer] Maximum number of paths to return.
188
188
  # @parameter by [Symbol] Sort by :total or :retained count.
189
189
  # @returns [Array(Array)] Array of [locations, total_count, retained_count].
190
- def top_paths(limit = 10, by: :retained)
190
+ def top_paths(limit: 10, by: :retained)
191
191
  paths = []
192
192
 
193
193
  @root.each_path do |path, total_count, retained_count|
@@ -206,7 +206,7 @@ module Memory
206
206
  # @parameter limit [Integer] Maximum number of hotspots to return.
207
207
  # @parameter by [Symbol] Sort by :total or :retained count.
208
208
  # @returns [Hash] Map of location => [total_count, retained_count].
209
- def hotspots(limit = 20, by: :retained)
209
+ def hotspots(limit: 20, by: :retained)
210
210
  frames = Hash.new{|h, k| h[k] = [0, 0]}
211
211
 
212
212
  collect_frames(@root, frames)
@@ -245,6 +245,29 @@ module Memory
245
245
  @root.prune!(limit)
246
246
  end
247
247
 
248
+ # Convert call tree data to JSON-compatible hash.
249
+ #
250
+ # @returns [Hash] Call tree data as a hash.
251
+ def as_json(top_paths: {limit: 10}, hotspots: {limit: 20})
252
+ {
253
+ total_allocations: total_allocations,
254
+ retained_allocations: retained_allocations,
255
+ top_paths: top_paths(**top_paths).map{|path, total, retained|
256
+ {path: path, total_count: total, retained_count: retained}
257
+ },
258
+ hotspots: hotspots(**hotspots).transform_values{|total, retained|
259
+ {total_count: total, retained_count: retained}
260
+ }
261
+ }
262
+ end
263
+
264
+ # Convert call tree data to JSON string.
265
+ #
266
+ # @returns [String] Call tree data as JSON.
267
+ def to_json(...)
268
+ as_json.to_json(...)
269
+ end
270
+
248
271
  private
249
272
 
250
273
  def collect_frames(node, frames)
@@ -7,6 +7,7 @@ require "console"
7
7
  require "objspace"
8
8
 
9
9
  require_relative "capture"
10
+ require_relative "allocations"
10
11
  require_relative "call_tree"
11
12
 
12
13
  module Memory
@@ -93,12 +94,15 @@ module Memory
93
94
  # @parameter increases_threshold [Integer] Number of increases before enabling detailed tracking.
94
95
  # @parameter prune_limit [Integer] Keep only top N children per node during pruning (default: 5).
95
96
  # @parameter prune_threshold [Integer] Number of insertions before auto-pruning (nil = no auto-pruning).
96
- def initialize(depth: 4, filter: nil, increases_threshold: 10, prune_limit: 5, prune_threshold: nil)
97
+ # @parameter gc [Hash | Nil] Run GC with these options before each sample (nil = don't run GC).
98
+ def initialize(depth: 4, filter: nil, increases_threshold: 10, prune_limit: 5, prune_threshold: nil, gc: nil)
97
99
  @depth = depth
98
100
  @filter = filter || default_filter
99
101
  @increases_threshold = increases_threshold
100
102
  @prune_limit = prune_limit
101
103
  @prune_threshold = prune_threshold
104
+ @gc = gc
105
+
102
106
  @capture = Capture.new
103
107
  @call_trees = {}
104
108
  @samples = {}
@@ -150,6 +154,9 @@ module Memory
150
154
  while true
151
155
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
152
156
 
157
+ # Optional garbage collection before sampling can help reduce noise:
158
+ GC.start(**@gc) if @gc
159
+
153
160
  sample!(&block)
154
161
 
155
162
  # Log capture statistics to detect issues like missing FREEOBJ events:
@@ -169,15 +176,22 @@ module Memory
169
176
  @capture.each do |klass, allocations|
170
177
  count = allocations.retained_count
171
178
  sample = @samples[klass] ||= Sample.new(klass, count)
179
+ increased = false
172
180
 
173
181
  if sample.sample!(count)
182
+ increased = true
183
+
174
184
  # Check if we should enable detailed tracking
175
- if sample.increases >= @increases_threshold && !@call_trees.key?(klass)
176
- track(klass, allocations)
185
+ if sample.increases >= @increases_threshold
186
+ # Start tracking with call path analysis if not already doing so:
187
+ unless tracking?(klass)
188
+ track(klass, allocations)
189
+ end
177
190
  end
178
-
179
- # Notify about growth if block given
180
- yield sample if block_given?
191
+ end
192
+
193
+ if block_given?
194
+ yield sample, increased
181
195
  end
182
196
  end
183
197
 
@@ -188,14 +202,12 @@ module Memory
188
202
  # Start tracking with call path analysis.
189
203
  #
190
204
  # @parameter klass [Class] The class to track with detailed analysis.
191
- def track(klass, allocations = nil)
205
+ def track(klass, allocations = nil, filter: @filter, depth: @depth)
192
206
  # Track the class and get the allocations object
193
207
  allocations ||= @capture.track(klass)
194
208
 
195
209
  # Set up call tree for this class
196
210
  tree = @call_trees[klass] = CallTree.new
197
- depth = @depth
198
- filter = @filter
199
211
 
200
212
  # Register callback on allocations object:
201
213
  # - On :newobj - returns state (leaf node) which C extension stores
@@ -233,7 +245,7 @@ module Memory
233
245
 
234
246
  # Get live object count for a class.
235
247
  def count(klass)
236
- @capture.count_for(klass)
248
+ @capture.retained_count_of(klass)
237
249
  end
238
250
 
239
251
  # Get the call tree for a specific class.
@@ -245,29 +257,20 @@ module Memory
245
257
  #
246
258
  # @parameter klass [Class] The class to get statistics for.
247
259
  # @returns [Hash] Statistics including total, retained, paths, and hotspots.
248
- def statistics(klass)
249
- tree = @call_trees[klass]
250
- return nil unless tree
260
+ def analyze(klass)
261
+ call_tree = @call_trees[klass]
262
+ allocations = @capture[klass]
263
+
264
+ return nil unless call_tree or allocations
251
265
 
252
266
  {
253
- live_count: @capture.count_for(klass),
254
- total_allocations: tree.total_allocations,
255
- retained_allocations: tree.retained_allocations,
256
- top_paths: tree.top_paths(10).map{|path, total, retained|
257
- {path: path, total_count: total, retained_count: retained}
258
- },
259
- hotspots: tree.hotspots(20).transform_values{|total, retained|
260
- {total_count: total, retained_count: retained}
261
- }
267
+ allocations: allocations&.as_json,
268
+ call_tree: call_tree&.as_json
262
269
  }
263
270
  end
264
271
 
265
- # Get statistics for all tracked classes.
266
- def all_statistics
267
- @call_trees.keys.each_with_object({}) do |klass, result|
268
- result[klass] = statistics(klass) if tracking?(klass)
269
- end
270
- end
272
+ # @deprecated Use {analyze} instead.
273
+ alias statistics analyze
271
274
 
272
275
  # Clear tracking data for a class.
273
276
  def clear(klass)
@@ -293,8 +296,9 @@ module Memory
293
296
 
294
297
  private
295
298
 
299
+ # Default filter to include all locations.
296
300
  def default_filter
297
- ->(location) {!location.path.match?(%r{/(gems|ruby)/|\A\(eval\)})}
301
+ ->(location) {true}
298
302
  end
299
303
 
300
304
  def prune_call_trees!
@@ -7,7 +7,7 @@
7
7
  module Memory
8
8
  # @namespace
9
9
  module Profiler
10
- VERSION = "1.1.15"
10
+ VERSION = "1.3.0"
11
11
  end
12
12
  end
13
13
 
@@ -6,4 +6,5 @@
6
6
  require_relative "profiler/version"
7
7
  require_relative "profiler/call_tree"
8
8
  require_relative "profiler/capture"
9
+ require_relative "profiler/allocations"
9
10
  require_relative "profiler/sampler"
data/readme.md CHANGED
@@ -22,6 +22,25 @@ Please see the [project documentation](https://socketry.github.io/memory-profile
22
22
 
23
23
  Please see the [project releases](https://socketry.github.io/memory-profiler/releases/index) for all releases.
24
24
 
25
+ ### v1.3.0
26
+
27
+ - **Breaking**: Renamed `Capture#count_for` to `Capture#retained_count_of` for better clarity and consistency.
28
+ - **Breaking**: Changed `CallTree#top_paths(limit)` to `CallTree#top_paths(limit:)` - now uses keyword argument.
29
+ - **Breaking**: Changed `CallTree#hotspots(limit)` to `CallTree#hotspots(limit:)` - now uses keyword argument.
30
+ - Simplified `Sampler#analyze` return structure to `{allocations: {...}, call_tree: {...}}` format.
31
+ - Added `Allocations#as_json` and `Allocations#to_json` methods for JSON serialization.
32
+ - Added `CallTree#as_json` and `CallTree#to_json` methods for JSON serialization with configurable options.
33
+ - `Memory::Profiler::Allocations.new` can now be instantiated directly (primarily for testing).
34
+ - `Sampler#statistics` is now a deprecated alias for `Sampler#analyze`.
35
+ - **Breaking**: Removed `Sampler#all_statistics` method.
36
+
37
+ ### v1.2.0
38
+
39
+ - Enable custom `depth:` and `filter:` options to `Sampler#track`.
40
+ - Change default filter to no-op.
41
+ - Add option to run GC with custom options before each sample to reduce noise.
42
+ - Always report sampler statistics after each sample.
43
+
25
44
  ### v1.1.15
26
45
 
27
46
  - Ignore `freeobj` for classes that are not being tracked.
@@ -60,16 +79,6 @@ Please see the [project releases](https://socketry.github.io/memory-profiler/rel
60
79
 
61
80
  - Use single global queue for event handling to avoid incorrect ordering.
62
81
 
63
- ### v1.1.7
64
-
65
- - Expose `Capture#statistics` for debugging internal memory tracking state.
66
-
67
- ### v1.1.6
68
-
69
- - Write barriers all the things.
70
- - Better state handling and object increment/decrement counting.
71
- - Better call tree handling - including support for `prune!`.
72
-
73
82
  ## Contributing
74
83
 
75
84
  We welcome contributions to this project.
data/releases.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Releases
2
2
 
3
+ ## v1.3.0
4
+
5
+ - **Breaking**: Renamed `Capture#count_for` to `Capture#retained_count_of` for better clarity and consistency.
6
+ - **Breaking**: Changed `CallTree#top_paths(limit)` to `CallTree#top_paths(limit:)` - now uses keyword argument.
7
+ - **Breaking**: Changed `CallTree#hotspots(limit)` to `CallTree#hotspots(limit:)` - now uses keyword argument.
8
+ - Simplified `Sampler#analyze` return structure to `{allocations: {...}, call_tree: {...}}` format.
9
+ - Added `Allocations#as_json` and `Allocations#to_json` methods for JSON serialization.
10
+ - Added `CallTree#as_json` and `CallTree#to_json` methods for JSON serialization with configurable options.
11
+ - `Memory::Profiler::Allocations.new` can now be instantiated directly (primarily for testing).
12
+ - `Sampler#statistics` is now a deprecated alias for `Sampler#analyze`.
13
+ - **Breaking**: Removed `Sampler#all_statistics` method.
14
+
15
+ ## v1.2.0
16
+
17
+ - Enable custom `depth:` and `filter:` options to `Sampler#track`.
18
+ - Change default filter to no-op.
19
+ - Add option to run GC with custom options before each sample to reduce noise.
20
+ - Always report sampler statistics after each sample.
21
+
3
22
  ## v1.1.15
4
23
 
5
24
  - Ignore `freeobj` for classes that are not being tracked.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: memory-profiler
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.15
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -56,6 +56,7 @@ files:
56
56
  - ext/memory/profiler/profiler.c
57
57
  - ext/memory/profiler/queue.h
58
58
  - lib/memory/profiler.rb
59
+ - lib/memory/profiler/allocations.rb
59
60
  - lib/memory/profiler/call_tree.rb
60
61
  - lib/memory/profiler/capture.rb
61
62
  - lib/memory/profiler/sampler.rb
metadata.gz.sig CHANGED
Binary file