Skip to content

Commit 20c75cf

Browse files
committed
Explicit ignore: and seen: options.
1 parent 16aa20c commit 20c75cf

File tree

3 files changed

+243
-16
lines changed

3 files changed

+243
-16
lines changed

lib/memory/usage.rb

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,22 +39,36 @@ def << allocation
3939
return self
4040
end
4141

42+
IGNORE = [
43+
# Skip modules and symbols, they are usually "global":
44+
Module,
45+
# Note that `reachable_objects_from` does not include symbols, numbers, or other value types, AFAICT.
46+
47+
Proc,
48+
Method,
49+
UnboundMethod,
50+
Binding,
51+
TracePoint,
52+
53+
# We don't want to traverse into shared state:
54+
Ractor,
55+
Thread,
56+
Fiber
57+
]
58+
4259
# Compute the usage of an object and all reachable objects from it.
4360
# @parameter root [Object] The root object to start traversal from.
4461
# @returns [Usage] The usage of the object and all reachable objects from it.
45-
def self.of(root)
46-
seen = Set.new.compare_by_identity
47-
62+
def self.of(root, seen: Set.new.compare_by_identity, ignore: IGNORE)
4863
count = 0
4964
size = 0
5065

5166
queue = [root]
5267
while queue.any?
5368
object = queue.shift
5469

55-
# Skip modules and symbols, they are usually "global":
56-
next if object.is_a?(Module)
57-
# Note that `reachable_objects_from` does not include symbols, numbers, or other value types, AFAICT.
70+
# Skip ignored types:
71+
next if ignore.any?{|type| object.is_a?(type)}
5872

5973
# Skip internal objects - they don't behave correctly when added to `seen` and create unbounded recursion:
6074
next if object.is_a?(ObjectSpace::InternalObjectWrapper)

releases.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Releases
22

3+
## Unreleased
4+
5+
- Explicit `ignore:` and `seen:` parameters for `Memory::Usage.of` to allow customization of ignored types and tracking of seen objects.
6+
37
## v0.8.4
48

59
- Fix bugs when printing reports due to interface mismatch with `Memory::Usage`.

test/memory/usage.rb

Lines changed: 219 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -132,16 +132,18 @@
132132
)
133133
end
134134

135-
it "can compute usage of proc" do
136-
proc = Proc.new{|x| x * 2}
137-
usage = subject.of(proc)
138-
expect(usage).to have_attributes(
139-
count: be > 1,
140-
size: be > 0
141-
)
142-
end
143-
144-
it "can compute usage of nil" do
135+
it "can compute usage of proc" do
136+
proc = Proc.new{|x| x * 2}
137+
# Proc is in default IGNORE list, so we need a custom ignore that allows Proc
138+
# but still includes Module to prevent deep traversal
139+
usage = subject.of(proc, ignore: [Module])
140+
expect(usage).to have_attributes(
141+
count: be > 1,
142+
size: be > 0
143+
)
144+
end
145+
146+
it "can compute usage of nil" do
145147
usage = subject.of(nil)
146148
expect(usage).to have_attributes(
147149
count: be == 0,
@@ -156,6 +158,213 @@
156158
size: be == 0
157159
)
158160
end
161+
162+
with "seen parameter" do
163+
it "allows sharing seen set across multiple calls" do
164+
# Create a shared seen set
165+
seen = Set.new.compare_by_identity
166+
167+
shared_object = Object.new
168+
array1 = [shared_object]
169+
array2 = [shared_object]
170+
171+
# First call tracks the array and shared object
172+
usage1 = subject.of(array1, seen: seen)
173+
expect(usage1.count).to be == 2 # array1 + shared_object
174+
175+
# Second call should skip shared_object since it's already in seen
176+
usage2 = subject.of(array2, seen: seen)
177+
expect(usage2.count).to be == 1 # Only array2, shared_object already seen
178+
end
179+
180+
it "respects pre-populated seen set" do
181+
seen = Set.new.compare_by_identity
182+
183+
existing_object = Object.new
184+
seen.add(existing_object)
185+
186+
array = [existing_object, Object.new]
187+
usage = subject.of(array, seen: seen)
188+
189+
# Should count array + new object, but not existing_object
190+
expect(usage.count).to be == 2
191+
end
192+
193+
it "updates seen set during traversal" do
194+
seen = Set.new.compare_by_identity
195+
196+
object = [Object.new, Object.new]
197+
usage = subject.of(object, seen: seen)
198+
199+
# All 3 objects should now be in seen
200+
expect(seen.size).to be == 3
201+
end
202+
203+
it "prevents counting same object graph twice" do
204+
seen = Set.new.compare_by_identity
205+
206+
root = {data: [1, 2, 3]}
207+
208+
# First traversal
209+
usage1 = subject.of(root, seen: seen)
210+
first_count = usage1.count
211+
212+
# Second traversal with same seen set should find nothing new
213+
usage2 = subject.of(root, seen: seen)
214+
expect(usage2.count).to be == 0
215+
end
216+
end
217+
218+
with "ignore parameter" do
219+
it "skips specified types from traversal" do
220+
# Create a custom ignore list that includes String
221+
custom_ignore = [Module, String]
222+
223+
array = [Object.new, "ignored string", Object.new]
224+
usage = subject.of(array, ignore: custom_ignore)
225+
226+
# Should count array + 2 objects, but not the string
227+
expect(usage.count).to be == 3
228+
end
229+
230+
it "uses default IGNORE constant when not specified" do
231+
# Test that Module is ignored by default
232+
object = [Object.new, String]
233+
usage = subject.of(object)
234+
235+
expect(usage.count).to be == 2 # array + object, not String (Module).
236+
end
237+
238+
it "can provide different ignore list" do
239+
# Provide an ignore list that doesn't include String
240+
# (but still includes Module to avoid deep traversal)
241+
custom_ignore = [Module]
242+
243+
string = "test string"
244+
object = [string]
245+
246+
usage_with_default = subject.of(object)
247+
usage_with_custom = subject.of(object, ignore: custom_ignore)
248+
249+
# Both should count the array and string (String class is a Module, but string instances aren't)
250+
expect(usage_with_default.count).to be == 2
251+
expect(usage_with_custom.count).to be == 2
252+
end
253+
254+
it "ignores Proc by default" do
255+
proc = Proc.new { "test" }
256+
array = [proc]
257+
usage = subject.of(array)
258+
259+
# Should count only the array, not the Proc
260+
expect(usage.count).to be == 1
261+
end
262+
263+
it "ignores Thread by default" do
264+
thread = Thread.current
265+
array = [thread]
266+
usage = subject.of(array)
267+
268+
# Should count only the array, not the Thread
269+
expect(usage.count).to be == 1
270+
end
271+
272+
it "ignores Fiber by default" do
273+
fiber = Fiber.new { "test" }
274+
array = [fiber]
275+
usage = subject.of(array)
276+
277+
# Should count only the array, not the Fiber
278+
expect(usage.count).to be == 1
279+
end
280+
281+
it "ignores Method by default" do
282+
method = Object.new.method(:to_s)
283+
array = [method]
284+
usage = subject.of(array)
285+
286+
# Should count only the array, not the Method
287+
expect(usage.count).to be == 1
288+
end
289+
290+
it "can override ignore list to include custom types" do
291+
# Define a custom class
292+
custom_class = Class.new
293+
instance = custom_class.new
294+
295+
# Create ignore list that includes our custom class
296+
custom_ignore = [Module, custom_class]
297+
298+
array = [instance, Object.new]
299+
usage = subject.of(array, ignore: custom_ignore)
300+
301+
# Should count array + Object, but not custom_class instance
302+
expect(usage.count).to be == 2
303+
end
304+
305+
it "checks ignore list with is_a? for inheritance" do
306+
# Create a subclass
307+
parent_class = Class.new
308+
child_class = Class.new(parent_class)
309+
310+
parent_instance = parent_class.new
311+
child_instance = child_class.new
312+
313+
# Ignore parent class
314+
custom_ignore = [Module, parent_class]
315+
316+
array = [parent_instance, child_instance]
317+
usage = subject.of(array, ignore: custom_ignore)
318+
319+
# Both instances should be ignored (child is_a? parent)
320+
expect(usage.count).to be == 1 # Only the array
321+
end
322+
end
323+
324+
with "combined seen and ignore parameters" do
325+
it "applies both seen and ignore filters" do
326+
seen = Set.new.compare_by_identity
327+
existing = Object.new
328+
seen.add(existing)
329+
330+
custom_ignore = [Module, String]
331+
332+
array = [existing, "ignored", Object.new]
333+
usage = subject.of(array, seen: seen, ignore: custom_ignore)
334+
335+
# Should count array + new object only
336+
# (existing is in seen, string is in ignore)
337+
expect(usage.count).to be == 2
338+
end
339+
340+
it "checks ignore before adding to seen" do
341+
seen = Set.new.compare_by_identity
342+
custom_ignore = [Module, String]
343+
344+
array = ["ignored string"]
345+
usage = subject.of(array, seen: seen, ignore: custom_ignore)
346+
347+
# String should not be added to seen since it's ignored
348+
expect(seen.size).to be == 1 # Only array
349+
expect(seen).not.to be(:include?, "ignored string")
350+
end
351+
352+
it "preserves seen across multiple calls with different ignore lists" do
353+
seen = Set.new.compare_by_identity
354+
355+
# First call with default ignore
356+
obj1 = [Object.new]
357+
usage1 = subject.of(obj1, seen: seen)
358+
count1 = seen.size
359+
360+
# Second call with custom ignore
361+
obj2 = [Object.new, "test"]
362+
usage2 = subject.of(obj2, seen: seen, ignore: [Module])
363+
364+
# Seen should have accumulated objects from both calls
365+
expect(seen.size).to be > count1
366+
end
367+
end
159368
end
160369

161370
with "#to_s" do

0 commit comments

Comments
 (0)