memory-profiler 1.4.0 → 1.5.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/extconf.rb +1 -1
- data/ext/memory/profiler/capture.c +119 -198
- data/ext/memory/profiler/events.c +10 -4
- data/ext/memory/profiler/events.h +7 -5
- data/ext/memory/profiler/profiler.c +17 -6
- data/ext/memory/profiler/table.c +343 -0
- data/ext/memory/profiler/table.h +73 -0
- data/lib/memory/profiler/sampler.rb +23 -30
- data/lib/memory/profiler/version.rb +1 -1
- data/lib/memory/profiler.rb +0 -1
- data/readme.md +8 -4
- data/releases.md +8 -0
- data.tar.gz.sig +0 -0
- metadata +3 -2
- metadata.gz.sig +0 -0
- data/lib/memory/profiler/graph.rb +0 -369
|
@@ -1,369 +0,0 @@
|
|
|
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
|
-
|