Skip to content

Commit 1b40e6a

Browse files
samuel-williams-shopifyioquatix
authored andcommitted
Introduce Memory::Usage and remove RSpec integration.
1 parent d9e9f77 commit 1b40e6a

File tree

8 files changed

+259
-119
lines changed

8 files changed

+259
-119
lines changed

lib/memory/aggregate.rb

Lines changed: 6 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,68 +3,21 @@
33
# Released under the MIT License.
44
# Copyright, 2020-2025, by Samuel Williams.
55

6+
require_relative "usage"
7+
68
module Memory
7-
UNITS = {
8-
0 => "B",
9-
3 => "KiB",
10-
6 => "MiB",
11-
9 => "GiB",
12-
12 => "TiB",
13-
15 => "PiB",
14-
18 => "EiB",
15-
21 => "ZiB",
16-
24 => "YiB"
17-
}.freeze
18-
19-
# Format bytes into human-readable units.
20-
# @parameter bytes [Integer] The number of bytes to format.
21-
# @returns [String] Formatted string with appropriate unit (e.g., "1.50 MiB").
22-
def self.formatted_bytes(bytes)
23-
return "0 B" if bytes.zero?
24-
25-
scale = Math.log2(bytes).div(10) * 3
26-
scale = 24 if scale > 24
27-
"%.2f #{UNITS[scale]}" % (bytes / 10.0**scale)
28-
end
29-
309
# Aggregates memory allocations by a given metric.
3110
# Groups allocations and tracks totals for memory usage and allocation counts.
3211
class Aggregate
33-
Total = Struct.new(:memory, :count) do
34-
def initialize
35-
super(0, 0)
36-
end
37-
38-
def << allocation
39-
self.memory += allocation.memsize
40-
self.count += 1
41-
end
42-
43-
def formatted_memory
44-
self.memory
45-
end
46-
47-
def to_s
48-
"(#{Memory.formatted_bytes memory} in #{count} allocations)"
49-
end
50-
51-
def as_json(options = nil)
52-
{
53-
memory: memory,
54-
count: count
55-
}
56-
end
57-
end
58-
5912
# Initialize a new aggregate with a title and metric block.
6013
# @parameter title [String] The title for this aggregate.
6114
# @parameter block [Block] A block that extracts the metric from an allocation.
6215
def initialize(title, &block)
6316
@title = title
6417
@metric = block
6518

66-
@total = Total.new
67-
@totals = Hash.new{|h,k| h[k] = Total.new}
19+
@total = Usage.new
20+
@totals = Hash.new{|h,k| h[k] = Usage.new}
6821
end
6922

7023
attr :title
@@ -78,11 +31,8 @@ def << allocation
7831
metric = @metric.call(allocation)
7932
total = @totals[metric]
8033

81-
total.memory += allocation.memsize
82-
total.count += 1
83-
84-
@total.memory += allocation.memsize
85-
@total.count += 1
34+
total << allocation
35+
@total << allocation
8636
end
8737

8838
# Sort totals by a given key.

lib/memory/format.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2025, by Samuel Williams.
5+
6+
module Memory
7+
UNITS = {
8+
0 => "B",
9+
3 => "KiB",
10+
6 => "MiB",
11+
9 => "GiB",
12+
12 => "TiB",
13+
15 => "PiB",
14+
18 => "EiB",
15+
21 => "ZiB",
16+
24 => "YiB"
17+
}.freeze
18+
19+
# Format bytes into human-readable units.
20+
# @parameter bytes [Integer] The number of bytes to format.
21+
# @returns [String] Formatted string with appropriate unit (e.g., "1.50 MiB").
22+
def self.formatted_bytes(bytes)
23+
return "0 B" if bytes.zero?
24+
25+
scale = Math.log2(bytes).div(10) * 3
26+
scale = 24 if scale > 24
27+
"%.2f #{UNITS[scale]}" % (bytes / 10.0**scale)
28+
end
29+
end

lib/memory/report.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ def self.general(**options)
2929
def initialize(aggregates, retained_only: true)
3030
@retained_only = retained_only
3131

32-
@total_allocated = Aggregate::Total.new
33-
@total_retained = Aggregate::Total.new
32+
@total_allocated = Usage.new
33+
@total_retained = Usage.new
3434

3535
@aggregates = aggregates
3636
end

lib/memory/rspec/profiler.rb

Lines changed: 0 additions & 59 deletions
This file was deleted.

lib/memory/usage.rb

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2025, by Samuel Williams.
5+
6+
require_relative "format"
7+
8+
require "set"
9+
require "objspace"
10+
11+
module Memory
12+
class Usage
13+
def initialize(size = 0, count = 0)
14+
@size = size
15+
@count = count
16+
end
17+
18+
# @attribute size [Integer] The total size of the usage in bytes.
19+
attr_accessor :size
20+
21+
alias memsize size
22+
23+
# @attribute count [Integer] The total count of the usage in object instances.
24+
attr_accessor :count
25+
26+
# Add an allocation to this usage.
27+
# @parameter allocation [Allocation] The allocation to add.
28+
def << allocation
29+
self.size += allocation.memsize
30+
self.count += 1
31+
32+
return self
33+
end
34+
35+
# Compute the usage of an object and all reachable objects from it.
36+
# @parameter root [Object] The root object to start traversal from.
37+
# @returns [Usage] The usage of the object and all reachable objects from it.
38+
def self.of(root)
39+
seen = Set.new.compare_by_identity
40+
41+
count = 0
42+
size = 0
43+
44+
queue = [root]
45+
while queue.any?
46+
object = queue.shift
47+
# Skip modules and symbols, they are usually "global":
48+
next if object.is_a?(Module)
49+
# Note that `reachable_objects_from` does not include symbols, numbers, or other value types, AFAICT.
50+
51+
# Skip objects we have already seen:
52+
next if seen.include?(object)
53+
54+
# Add the object to the seen set and update the count and size:
55+
seen.add(object)
56+
count += 1
57+
size += ObjectSpace.memsize_of(object)
58+
59+
# Add the object's reachable objects to the queue:
60+
queue.concat(ObjectSpace.reachable_objects_from(object))
61+
end
62+
63+
return new(size, count)
64+
end
65+
66+
def as_json(...)
67+
{
68+
size: @size,
69+
count: @count
70+
}
71+
end
72+
73+
def to_json(...)
74+
as_json.to_json(...)
75+
end
76+
77+
def to_s
78+
"(#{Memory.formatted_bytes(memory)} in #{count} allocations)"
79+
end
80+
end
81+
end

releases.md

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

3+
## Unreleased
4+
5+
- Removed old `RSpec` integration.
6+
- Introduced `Memory::Usage` and `Memory::Usage.of(object)` which recursively computes memory usage of an object and its contents.
7+
38
## v0.7.1
49

510
- Ensure aggregate keys are safe for serialization (and printing).

test/memory/report.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@
2727
)
2828

2929
expect(result[:total_allocated]).to have_keys(
30-
memory: be_a(Integer),
30+
size: be_a(Integer),
3131
count: be_a(Integer),
3232
)
3333

3434
expect(result[:total_retained]).to have_keys(
35-
memory: be_a(Integer),
35+
size: be_a(Integer),
3636
count: be_a(Integer),
3737
)
3838

0 commit comments

Comments
 (0)