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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/ext/memory/profiler/allocations.c +13 -2
- data/ext/memory/profiler/capture.c +2 -2
- data/lib/memory/profiler/allocations.rb +33 -0
- data/lib/memory/profiler/call_tree.rb +25 -2
- data/lib/memory/profiler/sampler.rb +33 -29
- data/lib/memory/profiler/version.rb +1 -1
- data/lib/memory/profiler.rb +1 -0
- data/readme.md +19 -10
- data/releases.md +19 -0
- data.tar.gz.sig +0 -0
- metadata +2 -1
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f059f309b7c1fd6fcffd119a0f6d40e8d4480783e2955ad3fc054fa280015f9d
|
|
4
|
+
data.tar.gz: e3c1ddd9afd7240b3bf727f6b6219b2bcb2e164c8e9c1c245ac4163684b180b8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
//
|
|
152
|
-
|
|
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
|
|
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, "
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
176
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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.
|
|
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
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
254
|
-
|
|
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
|
-
#
|
|
266
|
-
|
|
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) {
|
|
301
|
+
->(location) {true}
|
|
298
302
|
end
|
|
299
303
|
|
|
300
304
|
def prune_call_trees!
|
data/lib/memory/profiler.rb
CHANGED
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.
|
|
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
|