Skip to content

Commit 23525dd

Browse files
Improved bake tasks.
1 parent 350f113 commit 23525dd

File tree

5 files changed

+208
-31
lines changed

5 files changed

+208
-31
lines changed

bake/memory/profiler.rb

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,22 @@
66
# Copyright, 2018, by Jonas Peschla.
77
# Copyright, 2020-2022, by Samuel Williams.
88

9-
def check(paths:)
10-
require "console"
11-
12-
total_size = paths.sum{|path| File.size(path)}
9+
def initialize(...)
10+
super
1311

1412
require_relative "../../lib/memory"
13+
end
14+
15+
# Load and print a report from one or more .mprof files.
16+
#
17+
# This is a convenience task that combines sampler:load and report:print.
18+
#
19+
# @parameter paths [Array<String>] Paths to .mprof files.
20+
def check(*paths)
21+
# Delegate to sampler:load
22+
sampler = context.call("memory:sampler:load", *paths)
1523

16-
report = Memory::Report.general
17-
18-
cache = Memory::Cache.new
19-
wrapper = Memory::Wrapper.new(cache)
20-
21-
progress = Console.logger.progress(report, total_size)
22-
23-
paths.each do |path|
24-
Console.logger.info(report, "Loading #{path}, #{Memory.formatted_bytes File.size(path)}")
25-
26-
File.open(path) do |io|
27-
unpacker = wrapper.unpacker(io)
28-
count = unpacker.read_array_header
29-
30-
report.concat(unpacker)
31-
32-
progress.increment(io.size)
33-
end
34-
35-
Console.logger.info(report, "Loaded allocations, #{report.total_allocated}")
36-
end
37-
24+
# Generate and print report
25+
report = sampler.report
3826
report.print($stdout)
3927
end

bake/memory/report.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
2+
def initialize(...)
3+
super
4+
5+
require_relative "../../lib/memory"
6+
end
7+
8+
# Print a memory report.
9+
# Accepts either a `Memory::Sampler` or `Memory::Report` as input.
10+
# If given a Sampler, generates a report first.
11+
#
12+
# @parameter input [Memory::Report] The sampler or report to print.
13+
def print(input:)
14+
# Convert Sampler to Report if needed:
15+
report = case input
16+
when Memory::Sampler
17+
input.report
18+
when Memory::Report
19+
input
20+
else
21+
raise ArgumentError, "Expected Memory::Sampler or Memory::Report, got #{input.class}"
22+
end
23+
24+
report.print($stderr)
25+
26+
return report
27+
end

bake/memory/sampler.rb

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2025, by Samuel Williams.
5+
6+
def initialize(...)
7+
super
8+
9+
require_relative '../../lib/memory'
10+
end
11+
12+
# Load a sampler from one or more .mprof files.
13+
#
14+
# Multiple files will be combined into a single sampler, useful for
15+
# analyzing aggregated profiling data.
16+
#
17+
# @parameter paths [Array(String)] Paths to .mprof files.
18+
# @returns [Memory::Sampler] The loaded sampler with all allocations.
19+
def load(paths:)
20+
sampler = Memory::Sampler.new
21+
cache = sampler.cache
22+
wrapper = sampler.wrapper
23+
24+
total_size = paths.sum{|path| File.size(path)}
25+
progress = Console.logger.progress(sampler, total_size)
26+
27+
paths.each do |path|
28+
Console.logger.info(sampler, "Loading #{path}, #{Memory.formatted_bytes File.size(path)}")
29+
30+
File.open(path, 'r', encoding: Encoding::BINARY) do |io|
31+
unpacker = wrapper.unpacker(io)
32+
count = unpacker.read_array_header
33+
34+
last_pos = 0
35+
36+
# Read allocations directly into the sampler's array:
37+
unpacker.each do |allocation|
38+
sampler.allocated << allocation
39+
40+
# Update progress based on bytes read:
41+
current_pos = io.pos
42+
progress.increment(current_pos - last_pos)
43+
last_pos = current_pos
44+
end
45+
end
46+
47+
Console.logger.info(sampler, "Loaded #{sampler.allocated.size} allocations")
48+
end
49+
50+
return sampler
51+
end
52+
53+
# Dump a sampler to a .mprof file.
54+
#
55+
# @parameter input [Memory::Sampler] The sampler to dump.
56+
# @parameter output [String] Path to write the .mprof file.
57+
# @returns [Memory::Sampler] The input sampler.
58+
def dump(path, input:)
59+
File.open(path, 'w', encoding: Encoding::BINARY) do |io|
60+
input.dump(io)
61+
end
62+
63+
Console.logger.info(self, "Saved sampler to #{path} (#{File.size(path)} bytes)")
64+
65+
return input
66+
end
67+
68+
# Load a sampler from an ObjectSpace heap dump.
69+
#
70+
# @parameter path [String] Path to the heap dump JSON file.
71+
# @returns [Memory::Sampler] A sampler populated with allocations from the heap dump.
72+
def load_object_space_dump(path)
73+
file_size = File.size(path)
74+
progress = Console.logger.progress(self, file_size)
75+
76+
Console.logger.info(self, "Loading heap dump from #{path} (#{Memory.formatted_bytes(file_size)})")
77+
78+
sampler = nil
79+
File.open(path, 'r') do |io|
80+
sampler = Memory::Sampler.load_object_space_dump(io) do |line_count, object_count|
81+
# Update progress based on bytes read:
82+
progress.increment(io.pos - progress.current)
83+
end
84+
end
85+
86+
Console.logger.info(self, "Loaded #{sampler.allocated.size} objects from heap dump")
87+
88+
return sampler
89+
end

lib/memory/report.rb

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,16 @@ def as_json(options = nil)
9090
}
9191
end
9292

93-
# Convert this report to a JSON string.
94-
# @returns [String] JSON representation of this report.
95-
def to_json(...)
96-
as_json.to_json(...)
97-
end
93+
# Convert this report to a JSON string.
94+
# @returns [String] JSON representation of this report.
95+
def to_json(...)
96+
as_json.to_json(...)
97+
end
98+
99+
# Generate a human-readable representation of this report.
100+
# @returns [String] Summary showing allocated and retained totals.
101+
def inspect
102+
"#<#{self.class}: #{@total_allocated} allocated, #{@total_retained} retained>"
98103
end
99104
end
105+
end

lib/memory/sampler.rb

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
require "objspace"
1717
require "msgpack"
18+
require "json"
1819
require "console"
1920

2021
require_relative "cache"
@@ -65,6 +66,72 @@ def self.unpack(cache, fields)
6566
# end
6667
# ~~~
6768
class Sampler
69+
# Load allocations from an ObjectSpace heap dump.
70+
#
71+
# If a block is given, it will be called periodically with progress information.
72+
#
73+
# @parameter io [IO] The IO stream containing the heap dump JSON.
74+
# @yields [line_count, object_count] Progress callback with current line and object counts.
75+
# @returns [Sampler] A new sampler populated with allocations from the heap dump.
76+
def self.load_object_space_dump(io, &block)
77+
sampler = new
78+
cache = sampler.cache
79+
80+
line_count = 0
81+
object_count = 0
82+
report_interval = 10000
83+
84+
io.each_line do |line|
85+
line_count += 1
86+
87+
begin
88+
object = JSON.parse(line)
89+
rescue JSON::ParserError
90+
# Skip invalid JSON lines
91+
next
92+
end
93+
94+
# Skip non-object entries (ROOT, SHAPE, etc.)
95+
next unless object['address']
96+
97+
# Get allocation information (may be nil if tracing wasn't enabled)
98+
file = object['file'] || '(unknown)'
99+
line_number = object['line'] || 0
100+
101+
# Get object type/class
102+
type = object['type'] || 'unknown'
103+
104+
# Get memory size
105+
memsize = object['memsize'] || 0
106+
107+
# Get value for strings
108+
value = object['value']
109+
110+
allocation = Allocation.new(
111+
cache,
112+
type, # class_name
113+
file, # file
114+
line_number, # line
115+
memsize, # memsize
116+
value, # value (for strings)
117+
true # retained (all objects in heap dump are live)
118+
)
119+
120+
sampler.allocated << allocation
121+
object_count += 1
122+
123+
# Report progress periodically
124+
if block && (object_count % report_interval == 0)
125+
block.call(line_count, object_count)
126+
end
127+
end
128+
129+
# Final progress report
130+
block.call(line_count, object_count) if block
131+
132+
return sampler
133+
end
134+
68135
# Initialize a new sampler.
69136
# @parameter filter [Block | Nil] Optional filter block to select which allocations to track.
70137
def initialize(&filter)

0 commit comments

Comments
 (0)