memory-profiler 1.3.0 → 1.4.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 +9 -70
- data/ext/memory/profiler/allocations.h +3 -7
- data/ext/memory/profiler/capture.c +279 -102
- data/ext/memory/profiler/events.c +5 -6
- data/ext/memory/profiler/events.h +4 -2
- data/lib/memory/profiler/capture.rb +1 -1
- data/lib/memory/profiler/graph.rb +369 -0
- data/lib/memory/profiler/native.rb +9 -0
- data/lib/memory/profiler/sampler.rb +62 -35
- data/lib/memory/profiler/version.rb +1 -1
- data/lib/memory/profiler.rb +1 -0
- data/readme.md +5 -4
- data/releases.md +5 -0
- data.tar.gz.sig +0 -0
- metadata +3 -1
- metadata.gz.sig +0 -0
|
@@ -23,7 +23,8 @@ struct Memory_Profiler_Event {
|
|
|
23
23
|
// The class of the object:
|
|
24
24
|
VALUE klass;
|
|
25
25
|
|
|
26
|
-
// The
|
|
26
|
+
// The object_id (Integer VALUE) - NOT the raw object.
|
|
27
|
+
// We store object_id to avoid referencing objects that should be freed.
|
|
27
28
|
VALUE object;
|
|
28
29
|
};
|
|
29
30
|
|
|
@@ -32,12 +33,13 @@ struct Memory_Profiler_Events;
|
|
|
32
33
|
struct Memory_Profiler_Events* Memory_Profiler_Events_instance(void);
|
|
33
34
|
|
|
34
35
|
// Enqueue an event to the global queue.
|
|
36
|
+
// object parameter should be the object_id (Integer), not the raw object.
|
|
35
37
|
// Returns non-zero on success, zero on failure.
|
|
36
38
|
int Memory_Profiler_Events_enqueue(
|
|
37
39
|
enum Memory_Profiler_Event_Type type,
|
|
38
40
|
VALUE capture,
|
|
39
41
|
VALUE klass,
|
|
40
|
-
VALUE
|
|
42
|
+
VALUE object_id
|
|
41
43
|
);
|
|
42
44
|
|
|
43
45
|
// Process all queued events immediately (flush the queue)
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require "objspace"
|
|
7
|
+
|
|
8
|
+
module Memory
|
|
9
|
+
module Profiler
|
|
10
|
+
class Graph
|
|
11
|
+
def initialize
|
|
12
|
+
@objects = Set.new.compare_by_identity
|
|
13
|
+
@parents = Hash.new{|hash, key| hash[key] = Set.new.compare_by_identity}.compare_by_identity
|
|
14
|
+
@internals = Set.new
|
|
15
|
+
@root = nil # Track the root of traversal for idom
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr :parents
|
|
19
|
+
|
|
20
|
+
def include?(object)
|
|
21
|
+
@parents.include?(object)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def inspect
|
|
25
|
+
"#<#{self.class.name} @objects=#{@objects.size} @parents=#{@parents.size} @names=#{@names.size}>"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def add(object)
|
|
29
|
+
@objects.add(object)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def update!(from = Object)
|
|
33
|
+
# Store the root for idom algorithm
|
|
34
|
+
@root = from
|
|
35
|
+
@parents.clear
|
|
36
|
+
@internals.clear
|
|
37
|
+
|
|
38
|
+
# If the user has provided a specific object, try to avoid traversing the root Object.
|
|
39
|
+
if from != Object
|
|
40
|
+
@parents[Object] = nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Don't traverse the graph itself:
|
|
44
|
+
@parents[self] = nil
|
|
45
|
+
|
|
46
|
+
traverse!(from)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def name_for(object, limit = 8)
|
|
50
|
+
if object.is_a?(Module)
|
|
51
|
+
object.name.to_s
|
|
52
|
+
elsif object.is_a?(Object)
|
|
53
|
+
if limit > 0
|
|
54
|
+
parents = @parents[object]
|
|
55
|
+
if parent = parents&.first
|
|
56
|
+
return name_for(parent, limit - 1) + compute_edge_label(parent, object)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
return object.class.name.to_s
|
|
61
|
+
else
|
|
62
|
+
object.inspect
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Return roots using immediate dominator algorithm
|
|
67
|
+
def roots
|
|
68
|
+
return [] if @objects.empty?
|
|
69
|
+
|
|
70
|
+
# Compute immediate dominators
|
|
71
|
+
idom = compute_idom
|
|
72
|
+
|
|
73
|
+
# Count how many tracked objects each node dominates
|
|
74
|
+
dominated_counts = Hash.new(0).compare_by_identity
|
|
75
|
+
retained_by_counts = Hash.new(0).compare_by_identity
|
|
76
|
+
|
|
77
|
+
@objects.each do |object|
|
|
78
|
+
# Credit the immediate dominator
|
|
79
|
+
if dominator = idom[object]
|
|
80
|
+
dominated_counts[dominator] += 1
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Credit all immediate parents for retained_by
|
|
84
|
+
if parent_set = @parents[object]
|
|
85
|
+
parent_set.each do |parent|
|
|
86
|
+
retained_by_counts[parent] += 1
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Build results
|
|
92
|
+
total = @objects.size
|
|
93
|
+
results = []
|
|
94
|
+
|
|
95
|
+
dominated_counts.each do |object, count|
|
|
96
|
+
result = {
|
|
97
|
+
name: name_for(object),
|
|
98
|
+
count: count,
|
|
99
|
+
percentage: (count * 100.0) / total
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if retained_count = retained_by_counts[object]
|
|
103
|
+
result[:retained_by] = retained_count
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
results << result
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Add entries that appear ONLY in retained_by (intermediate nodes)
|
|
110
|
+
retained_by_counts.each do |object, count|
|
|
111
|
+
next if dominated_counts[object] # Already included
|
|
112
|
+
|
|
113
|
+
results << {
|
|
114
|
+
name: name_for(object),
|
|
115
|
+
count: 0,
|
|
116
|
+
percentage: 0.0,
|
|
117
|
+
retained_by: count
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Sort by count descending
|
|
122
|
+
results.sort_by!{|r| -r[:count]}
|
|
123
|
+
|
|
124
|
+
results
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
IS_A = Kernel.method(:is_a?).unbind
|
|
130
|
+
RESPOND_TO = Kernel.method(:respond_to?).unbind
|
|
131
|
+
EQUAL = Kernel.method(:equal?).unbind
|
|
132
|
+
|
|
133
|
+
def traverse!(object, parent = nil)
|
|
134
|
+
queue = Array.new
|
|
135
|
+
queue << [object, parent]
|
|
136
|
+
|
|
137
|
+
while queue.any?
|
|
138
|
+
object, parent = queue.shift
|
|
139
|
+
|
|
140
|
+
# We shouldn't use internal objects as parents.
|
|
141
|
+
unless IS_A.bind_call(object, ObjectSpace::InternalObjectWrapper)
|
|
142
|
+
parent = object
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
ObjectSpace.reachable_objects_from(object).each do |child|
|
|
146
|
+
if IS_A.bind_call(child, ObjectSpace::InternalObjectWrapper)
|
|
147
|
+
# There is no value in scanning internal objects.
|
|
148
|
+
next if child.type == :T_IMEMO
|
|
149
|
+
|
|
150
|
+
# We need to handle internal objects differently, because they don't follow the same equality/identity rules. Since we can reach the same internal object from multiple parents, we need to use an appropriate key to track it. Otherwise, the objects will not be counted on subsequent parent traversals.
|
|
151
|
+
key = [parent.object_id, child.internal_object_id]
|
|
152
|
+
if @internals.add?(key)
|
|
153
|
+
queue << [child, parent]
|
|
154
|
+
end
|
|
155
|
+
elsif parents = @parents[child] # Skip traversal if we are explicitly set to nil.
|
|
156
|
+
# If we haven't seen the object yet, add it to the queue:
|
|
157
|
+
if parents.add?(parent)
|
|
158
|
+
queue << [child, parent]
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Limit the cost of computing the edge label.
|
|
166
|
+
SEARCH_LIMIT = 1000
|
|
167
|
+
|
|
168
|
+
# Lazily compute the edge label (how parent references child)
|
|
169
|
+
# This is called on-demand for objects that appear in roots
|
|
170
|
+
def compute_edge_label(parent, object)
|
|
171
|
+
case parent
|
|
172
|
+
when Hash
|
|
173
|
+
if parent.size < SEARCH_LIMIT
|
|
174
|
+
# Use Ruby's built-in key method to find the key
|
|
175
|
+
if key = parent.key(object)
|
|
176
|
+
return "[#{key.inspect}]"
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
return "[?/#{parent.size}]"
|
|
180
|
+
when Array
|
|
181
|
+
if parent.size < SEARCH_LIMIT
|
|
182
|
+
# Use Ruby's built-in index method to find the position
|
|
183
|
+
if index = parent.index(object)
|
|
184
|
+
return "[#{index}]"
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
return "[?/#{parent.size}]"
|
|
188
|
+
else
|
|
189
|
+
return extract_name(parent, object)
|
|
190
|
+
end
|
|
191
|
+
rescue => error
|
|
192
|
+
# If inspection fails, fall back to class name
|
|
193
|
+
return "(#{error.class.name}: #{error.message})"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Compute immediate dominators for all nodes
|
|
197
|
+
# Returns hash mapping node => immediate dominator
|
|
198
|
+
def compute_idom
|
|
199
|
+
idom = Hash.new.compare_by_identity
|
|
200
|
+
|
|
201
|
+
# Root dominates itself
|
|
202
|
+
idom[@root] = @root if @root
|
|
203
|
+
|
|
204
|
+
# Find all roots (nodes with no parents)
|
|
205
|
+
roots = Set.new.compare_by_identity
|
|
206
|
+
roots.add(@root) if @root
|
|
207
|
+
|
|
208
|
+
# Convert to array to avoid "can't add key during iteration" errors
|
|
209
|
+
@parents.each do |node, parents|
|
|
210
|
+
if parents.nil? || parents.empty?
|
|
211
|
+
roots.add(node)
|
|
212
|
+
idom[node] = node
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Iterative dataflow analysis
|
|
217
|
+
changed = true
|
|
218
|
+
iterations = 0
|
|
219
|
+
|
|
220
|
+
while changed && iterations < 1000
|
|
221
|
+
changed = false
|
|
222
|
+
iterations += 1
|
|
223
|
+
|
|
224
|
+
# Convert to array to avoid "can't add key during iteration" errors
|
|
225
|
+
@parents.each do |node, parents|
|
|
226
|
+
# Skip roots:
|
|
227
|
+
next if roots.include?(node)
|
|
228
|
+
next if parents.nil? || parents.empty?
|
|
229
|
+
|
|
230
|
+
# Find first processed predecessor
|
|
231
|
+
new_idom = parents.find{|parent| idom[parent]}
|
|
232
|
+
next unless new_idom
|
|
233
|
+
|
|
234
|
+
# Intersect with other processed predecessors
|
|
235
|
+
parents.each do |parent|
|
|
236
|
+
next if parent.equal?(new_idom)
|
|
237
|
+
next unless idom[parent]
|
|
238
|
+
|
|
239
|
+
new_idom = intersect(new_idom, parent, idom)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Update if changed
|
|
243
|
+
if !idom[node].equal?(new_idom)
|
|
244
|
+
idom[node] = new_idom
|
|
245
|
+
changed = true
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
idom
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Find lowest common ancestor in dominator tree
|
|
254
|
+
def intersect(node1, node2, idom)
|
|
255
|
+
finger1 = node1
|
|
256
|
+
finger2 = node2
|
|
257
|
+
seen = Set.new.compare_by_identity
|
|
258
|
+
iterations = 0
|
|
259
|
+
|
|
260
|
+
until finger1.equal?(finger2)
|
|
261
|
+
return finger1 if iterations > SEARCH_LIMIT
|
|
262
|
+
iterations += 1
|
|
263
|
+
|
|
264
|
+
# Prevent infinite loops
|
|
265
|
+
return finger1 if seen.include?(finger1)
|
|
266
|
+
seen.add(finger1)
|
|
267
|
+
|
|
268
|
+
# Walk up both paths until they meet
|
|
269
|
+
depth1 = depth(finger1)
|
|
270
|
+
depth2 = depth(finger2)
|
|
271
|
+
|
|
272
|
+
if depth1 > depth2
|
|
273
|
+
break unless idom[finger1]
|
|
274
|
+
finger1 = idom[finger1]
|
|
275
|
+
elsif depth2 > depth1
|
|
276
|
+
break unless idom[finger2]
|
|
277
|
+
finger2 = idom[finger2]
|
|
278
|
+
else
|
|
279
|
+
# Same depth, walk both up
|
|
280
|
+
break unless idom[finger1] && idom[finger2]
|
|
281
|
+
finger1 = idom[finger1]
|
|
282
|
+
finger2 = idom[finger2]
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
finger1
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Compute depth of node in parent tree
|
|
290
|
+
def depth(node)
|
|
291
|
+
depth = 0
|
|
292
|
+
current = node
|
|
293
|
+
seen = Set.new.compare_by_identity
|
|
294
|
+
|
|
295
|
+
while current
|
|
296
|
+
return depth if seen.include?(current)
|
|
297
|
+
seen.add(current)
|
|
298
|
+
|
|
299
|
+
break unless @parents.key?(current)
|
|
300
|
+
parents = @parents[current]
|
|
301
|
+
break if parents.empty?
|
|
302
|
+
|
|
303
|
+
current = parents.first # TODO: Should we use the first parent?
|
|
304
|
+
depth += 1
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
depth
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def extract_name(parent, object)
|
|
311
|
+
if RESPOND_TO.bind_call(parent, :constants)
|
|
312
|
+
parent.constants.each do |constant|
|
|
313
|
+
if !parent.autoload?(constant) && parent.const_defined?(constant)
|
|
314
|
+
if EQUAL.bind_call(parent.const_get(constant), object)
|
|
315
|
+
return "::#{constant}"
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
if RESPOND_TO.bind_call(parent, :instance_variables)
|
|
322
|
+
parent.instance_variables.each do |variable|
|
|
323
|
+
if EQUAL.bind_call(parent.instance_variable_get(variable), object)
|
|
324
|
+
return ".#{variable}"
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
if IS_A.bind_call(parent, Struct)
|
|
330
|
+
parent.members.each do |member|
|
|
331
|
+
if EQUAL.bind_call(parent[member], object)
|
|
332
|
+
return ".#{member}"
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
if IS_A.bind_call(parent, Fiber)
|
|
338
|
+
return "(fiber)"
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
if IS_A.bind_call(parent, Thread)
|
|
342
|
+
parent.thread_variables.each do |variable|
|
|
343
|
+
if EQUAL.bind_call(parent.thread_variable_get(variable), object)
|
|
344
|
+
return "(TLS #{variable})"
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
parent.keys.each do |key|
|
|
349
|
+
if EQUAL.bind_call(parent[key], object)
|
|
350
|
+
return "[#{key}]"
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
return "(thread)"
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
if IS_A.bind_call(parent, Ractor)
|
|
358
|
+
return "(ractor)"
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
return "(#{parent.inspect}::???)"
|
|
362
|
+
rescue => error
|
|
363
|
+
# If inspection fails, fall back to class name
|
|
364
|
+
return "(#{error.class.name}: #{error.message})"
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
@@ -9,6 +9,7 @@ require "objspace"
|
|
|
9
9
|
require_relative "capture"
|
|
10
10
|
require_relative "allocations"
|
|
11
11
|
require_relative "call_tree"
|
|
12
|
+
require_relative "graph"
|
|
12
13
|
|
|
13
14
|
module Memory
|
|
14
15
|
module Profiler
|
|
@@ -141,6 +142,28 @@ module Memory
|
|
|
141
142
|
def stop
|
|
142
143
|
@capture.stop
|
|
143
144
|
end
|
|
145
|
+
|
|
146
|
+
# Clear tracking data for a class.
|
|
147
|
+
def clear(klass)
|
|
148
|
+
tree = @call_trees[klass]
|
|
149
|
+
tree&.clear!
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Clear all tracking data.
|
|
153
|
+
def clear_all!
|
|
154
|
+
@call_trees.each_value(&:clear!)
|
|
155
|
+
@capture.clear
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Stop all tracking and clean up.
|
|
159
|
+
def stop!
|
|
160
|
+
@capture.stop
|
|
161
|
+
@call_trees.each_key do |klass|
|
|
162
|
+
@capture.untrack(klass)
|
|
163
|
+
end
|
|
164
|
+
@capture.clear
|
|
165
|
+
@call_trees.clear
|
|
166
|
+
end
|
|
144
167
|
|
|
145
168
|
# Run periodic sampling in a loop.
|
|
146
169
|
#
|
|
@@ -210,9 +233,9 @@ module Memory
|
|
|
210
233
|
tree = @call_trees[klass] = CallTree.new
|
|
211
234
|
|
|
212
235
|
# Register callback on allocations object:
|
|
213
|
-
# - On :newobj - returns
|
|
214
|
-
# - On :freeobj - receives
|
|
215
|
-
allocations.track do |klass, event,
|
|
236
|
+
# - On :newobj - returns data (leaf node) which C extension stores
|
|
237
|
+
# - On :freeobj - receives data back from C extension
|
|
238
|
+
allocations.track do |klass, event, data|
|
|
216
239
|
case event
|
|
217
240
|
when :newobj
|
|
218
241
|
# Capture call stack and record in tree
|
|
@@ -224,8 +247,8 @@ module Memory
|
|
|
224
247
|
end
|
|
225
248
|
# Return nil or the node - C will store whatever we return.
|
|
226
249
|
when :freeobj
|
|
227
|
-
# Decrement using the
|
|
228
|
-
|
|
250
|
+
# Decrement using the data (leaf node) passed back from then native extension:
|
|
251
|
+
data&.decrement_path!
|
|
229
252
|
end
|
|
230
253
|
rescue Exception => error
|
|
231
254
|
warn "Error in allocation tracking: #{error.message}\n#{error.backtrace.join("\n")}"
|
|
@@ -256,45 +279,49 @@ module Memory
|
|
|
256
279
|
# Get allocation statistics for a tracked class.
|
|
257
280
|
#
|
|
258
281
|
# @parameter klass [Class] The class to get statistics for.
|
|
259
|
-
# @
|
|
260
|
-
|
|
261
|
-
|
|
282
|
+
# @parameter allocation_roots [Boolean] Include call tree showing where allocations occurred (default: true if available).
|
|
283
|
+
# @parameter retained_roots [Boolean] Compute object graph showing what's retaining allocations (default: true, can be slow for large graphs).
|
|
284
|
+
# @parameter roots_from [Object] Starting point for retained roots analysis (default: Object).
|
|
285
|
+
# @returns [Hash] Statistics including allocations, allocation_roots (call tree), and retained_roots (object graph).
|
|
286
|
+
def analyze(klass, allocation_roots: true, retained_roots: false, retained_objects: true)
|
|
287
|
+
call_tree_data = @call_trees[klass] if allocation_roots
|
|
262
288
|
allocations = @capture[klass]
|
|
263
289
|
|
|
264
|
-
return nil unless
|
|
290
|
+
return nil unless call_tree_data or allocations
|
|
265
291
|
|
|
266
|
-
{
|
|
292
|
+
result = {
|
|
267
293
|
allocations: allocations&.as_json,
|
|
268
|
-
call_tree: call_tree&.as_json
|
|
269
294
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
alias statistics analyze
|
|
274
|
-
|
|
275
|
-
# Clear tracking data for a class.
|
|
276
|
-
def clear(klass)
|
|
277
|
-
tree = @call_trees[klass]
|
|
278
|
-
tree&.clear!
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
# Clear all tracking data.
|
|
282
|
-
def clear_all!
|
|
283
|
-
@call_trees.each_value(&:clear!)
|
|
284
|
-
@capture.clear
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
# Stop all tracking and clean up.
|
|
288
|
-
def stop!
|
|
289
|
-
@capture.stop
|
|
290
|
-
@call_trees.each_key do |klass|
|
|
291
|
-
@capture.untrack(klass)
|
|
295
|
+
|
|
296
|
+
if allocation_roots && call_tree_data
|
|
297
|
+
result[:allocation_roots] = call_tree_data.as_json
|
|
292
298
|
end
|
|
293
|
-
|
|
294
|
-
|
|
299
|
+
|
|
300
|
+
if retained_roots
|
|
301
|
+
result[:retained_roots] = compute_roots(klass)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
result
|
|
295
305
|
end
|
|
296
306
|
|
|
297
307
|
private
|
|
308
|
+
|
|
309
|
+
# Compute retaining roots for a class's allocations
|
|
310
|
+
def compute_roots(klass)
|
|
311
|
+
graph = Graph.new
|
|
312
|
+
|
|
313
|
+
# Add all tracked objects to the graph
|
|
314
|
+
# NOTE: States table is now at Capture level, so we use capture.each_object
|
|
315
|
+
@capture.each_object(klass) do |object, state|
|
|
316
|
+
graph.add(object)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Build parent relationships
|
|
320
|
+
graph.update!
|
|
321
|
+
|
|
322
|
+
# Return roots analysis
|
|
323
|
+
graph.roots
|
|
324
|
+
end
|
|
298
325
|
|
|
299
326
|
# Default filter to include all locations.
|
|
300
327
|
def default_filter
|
data/lib/memory/profiler.rb
CHANGED
data/readme.md
CHANGED
|
@@ -22,6 +22,11 @@ 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.4.0
|
|
26
|
+
|
|
27
|
+
- Implement [Cooper-Harvey-Kennedy](https://www.cs.tufts.edu/~nr/cs257/archive/keith-cooper/dom14.pdf) algorithm for finding root objects in memory leaks.
|
|
28
|
+
- Rework capture to track objects by `object_id` exclusively.
|
|
29
|
+
|
|
25
30
|
### v1.3.0
|
|
26
31
|
|
|
27
32
|
- **Breaking**: Renamed `Capture#count_for` to `Capture#retained_count_of` for better clarity and consistency.
|
|
@@ -75,10 +80,6 @@ Please see the [project releases](https://socketry.github.io/memory-profiler/rel
|
|
|
75
80
|
|
|
76
81
|
- More write barriers...
|
|
77
82
|
|
|
78
|
-
### v1.1.8
|
|
79
|
-
|
|
80
|
-
- Use single global queue for event handling to avoid incorrect ordering.
|
|
81
|
-
|
|
82
83
|
## Contributing
|
|
83
84
|
|
|
84
85
|
We welcome contributions to this project.
|
data/releases.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v1.4.0
|
|
4
|
+
|
|
5
|
+
- Implement [Cooper-Harvey-Kennedy](https://www.cs.tufts.edu/~nr/cs257/archive/keith-cooper/dom14.pdf) algorithm for finding root objects in memory leaks.
|
|
6
|
+
- Rework capture to track objects by `object_id` exclusively.
|
|
7
|
+
|
|
3
8
|
## v1.3.0
|
|
4
9
|
|
|
5
10
|
- **Breaking**: Renamed `Capture#count_for` to `Capture#retained_count_of` for better clarity and consistency.
|
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.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -59,6 +59,8 @@ files:
|
|
|
59
59
|
- lib/memory/profiler/allocations.rb
|
|
60
60
|
- lib/memory/profiler/call_tree.rb
|
|
61
61
|
- lib/memory/profiler/capture.rb
|
|
62
|
+
- lib/memory/profiler/graph.rb
|
|
63
|
+
- lib/memory/profiler/native.rb
|
|
62
64
|
- lib/memory/profiler/sampler.rb
|
|
63
65
|
- lib/memory/profiler/version.rb
|
|
64
66
|
- license.md
|
metadata.gz.sig
CHANGED
|
Binary file
|