memory 0.5.0 → 0.6.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 116b9be7bb4a7d1c7b906923132406708953a8e3e7d43687d82f7920192ad74f
4
- data.tar.gz: bf093a25998cefbf01f8fbc79d92d23bc972c016596acb3678cd6303eb09e034
3
+ metadata.gz: 8ab40ebc992286597b6a01b5157fca5393b29563c7131483d2e7839f15172541
4
+ data.tar.gz: d5b9b73ea305840435c242a6e34bf755d94e77350846f6618467fef4fa7561ff
5
5
  SHA512:
6
- metadata.gz: 403676e22f95cd9239f3da72898a9be935c100e236bd51a6fca58d2ebf472a33609efb373dff628cf5f6f1e7ed9ace28dc3abc45adbbdc9975d489539b2f853d
7
- data.tar.gz: be14f62bb150ab3daec0c9743800e887e606b0ba1545725143484362278779451f9a98dab2a35126d4396d6a57e08c1ccdce28b1a21fe5e894516b16e235b79c
6
+ metadata.gz: 5a10c287ab778c7deb2e12b5a04e7214107ae7afbdf12e57af3423128fe0dc9c233735892ae97ec18d90f47ba46856f08ab5624389a1cb61dbdc4d9af3733bf3
7
+ data.tar.gz: db0d83a8c69a4bdf37e058d6a9e304a7b0608791f8a1f89ad1f2864f2d4119aa8c61c9726b813800e4b45506e1cfa2eec7227ca8eeed99170b931927878e4efb
checksums.yaml.gz.sig CHANGED
@@ -1,4 +1,2 @@
1
- r��[
2
- �g�.z]�ǝ�T�6��6�I I[�㙢~���[uP��f�f^t�
3
- ��P�����@�\G�v� a��F/`���-$�h{��r 'q�o�E1$.0�Z�����i<��N�:"r��;�%_�)����1k��V�N'(�1�M {�O��/����d��\_ gI4�{y��K�_�ŋ���a��y�?�Wb�����J
4
- ��2>/���L�??{]������KԔr�Rf=�~� <X�1�Ws����':G
1
+ 5�^�R�M�(�KD��n����^�z<~�"�i�yAP����Qt��p�<D��2jlyIexf ���W�bS��:L�n��@
2
+ g��A]�%��Pn�fG>b��?x�#�ն�h� ���D�&y
@@ -0,0 +1,31 @@
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
+ # Print a memory report.
13
+ #
14
+ # Accepts either a `Memory::Sampler` or `Memory::Report` as input.
15
+ #
16
+ # @parameter input [Memory::Report] The sampler or report to print.
17
+ def print(input:)
18
+ # Convert Sampler to Report if needed:
19
+ report = case input
20
+ when Memory::Sampler
21
+ input.report
22
+ when Memory::Report
23
+ input
24
+ else
25
+ raise ArgumentError, "Expected Memory::Sampler or Memory::Report, got #{input.class}"
26
+ end
27
+
28
+ report.print($stderr)
29
+
30
+ return report
31
+ end
@@ -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
@@ -0,0 +1,238 @@
1
+ # Getting Started
2
+
3
+ This guide explains how to get started with `memory`, a Ruby gem for profiling memory allocations in your applications.
4
+
5
+ ## Installation
6
+
7
+ Add the gem to your project:
8
+
9
+ ``` bash
10
+ $ bundle add memory
11
+ ```
12
+
13
+ ## Core Concepts
14
+
15
+ `memory` helps you understand where your Ruby application allocates memory and which allocations are retained (not garbage collected). It has several core concepts:
16
+
17
+ - A {ruby Memory::Sampler} which captures allocation data during code execution.
18
+ - A {ruby Memory::Report} which aggregates and presents allocation statistics.
19
+ - A {ruby Memory::Aggregate} which groups allocations by specific metrics (gem, file, class, etc.).
20
+
21
+ ## Usage
22
+
23
+ The simplest way to profile memory allocations is using the `Memory.report` method:
24
+
25
+ ``` ruby
26
+ require "memory"
27
+
28
+ # Profile a block of code:
29
+ report = Memory.report do
30
+ # Your code here - e.g., process 1000 user records:
31
+ users = []
32
+ 1000.times do |i|
33
+ users << {id: i, name: "User #{i}", email: "user#{i}@example.com"}
34
+ end
35
+
36
+ # Process the data:
37
+ users.each do |user|
38
+ formatted = "#{user[:name]} <#{user[:email]}>"
39
+ end
40
+ end
41
+
42
+ # Display the results:
43
+ report.print
44
+ ```
45
+
46
+ This will output a detailed report showing:
47
+
48
+ - **Total Allocated**: All objects created during execution
49
+ - **Total Retained**: Objects that survived garbage collection
50
+ - **By Gem**: Memory usage grouped by gem/library
51
+ - **By File**: Memory usage grouped by source file
52
+ - **By Location**: Memory usage by specific file:line locations
53
+ - **By Class**: Memory usage by object class
54
+ - **Strings**: Special analysis of string allocations
55
+
56
+ ### Understanding the Output
57
+
58
+ The report shows memory allocations in human-readable units (B, KiB, MiB, etc.):
59
+
60
+ ```
61
+ # Retained Memory Profile
62
+
63
+ - Total Allocated: (1.50 MiB in 15234 allocations)
64
+ - Total Retained: (856.32 KiB in 8912 allocations)
65
+
66
+ ## By Gem (856.32 KiB in 8912 allocations)
67
+
68
+ - (645.21 KiB in 6543 allocations) my_app/lib
69
+ - (128.45 KiB in 1234 allocations) activerecord-7.0.8
70
+ - (82.66 KiB in 1135 allocations) activesupport-7.0.8
71
+ ```
72
+
73
+ Each line shows:
74
+ - The memory consumed and number of allocations in that category
75
+ - The category name (gem, file, class, etc.)
76
+
77
+ ### Manual Start/Stop
78
+
79
+ For more control, use the {ruby Memory::Sampler} directly:
80
+
81
+ ``` ruby
82
+ require "memory"
83
+
84
+ sampler = Memory::Sampler.new
85
+
86
+ # Start profiling:
87
+ sampler.start
88
+
89
+ # Run your code:
90
+ items = []
91
+ 10000.times do |i|
92
+ items << "Item #{i}"
93
+ end
94
+
95
+ # Stop profiling:
96
+ sampler.stop
97
+
98
+ # Generate and print the report:
99
+ report = sampler.report
100
+ report.print
101
+ ```
102
+
103
+ This approach is useful when:
104
+ - You need to profile specific sections of long-running code
105
+ - You want to control exactly when profiling begins and ends
106
+ - You're integrating with test frameworks or benchmarking tools
107
+
108
+ ### Filtering Allocations
109
+
110
+ You can filter which allocations to track by providing a filter block to {ruby Memory::Sampler}:
111
+
112
+ ``` ruby
113
+ # Only track String allocations from your application code:
114
+ sampler = Memory::Sampler.new do |klass, file|
115
+ klass == String && file.include?("/lib/my_app/")
116
+ end
117
+
118
+ sampler.start
119
+ # Your code here
120
+ sampler.stop
121
+
122
+ report = sampler.report
123
+ report.print
124
+ ```
125
+
126
+ The filter block receives:
127
+ - `klass`: The class of the allocated object
128
+ - `file`: The source file where the allocation occurred
129
+
130
+ Return `true` to include the allocation, `false` to exclude it.
131
+
132
+ ### Persisting Results
133
+
134
+ For analyzing large applications or comparing runs over time, you can persist allocation data to disk:
135
+
136
+ ``` ruby
137
+ sampler = Memory::Sampler.new
138
+
139
+ sampler.start
140
+ # Run your code
141
+ sampler.stop
142
+
143
+ # Save the allocation data:
144
+ File.open("profile.mprof", "w", encoding: Encoding::BINARY) do |io|
145
+ sampler.dump(io)
146
+ end
147
+ ```
148
+
149
+ Later, you can load and analyze the data:
150
+
151
+ ``` ruby
152
+ sampler = Memory::Sampler.new
153
+
154
+ # Load saved allocation data:
155
+ data = File.read("profile.mprof", encoding: Encoding::BINARY)
156
+ sampler.load(data)
157
+
158
+ # Generate a report:
159
+ report = sampler.report
160
+ report.print
161
+ ```
162
+
163
+ You can even combine multiple profile files:
164
+
165
+ ``` ruby
166
+ sampler = Memory::Sampler.new
167
+
168
+ # Load multiple profile files:
169
+ Dir.glob("profiles/*.mprof") do |path|
170
+ puts "Loading #{path}..."
171
+ sampler.load(File.read(path, encoding: Encoding::BINARY))
172
+ end
173
+
174
+ # Generate a combined report:
175
+ report = sampler.report
176
+ report.print
177
+ ```
178
+
179
+ This is particularly useful for:
180
+ - **Profiling test suites**: Profile each test file separately, then combine results
181
+ - **Production analysis**: Capture profiles from production environments and analyze offline
182
+ - **Trend analysis**: Compare memory usage across different code versions
183
+
184
+ ### Custom Reports
185
+
186
+ You can create custom reports with specific aggregates:
187
+
188
+ ``` ruby
189
+ require "memory"
190
+
191
+ # Create a custom report with only specific aggregates:
192
+ report = Memory::Report.new([
193
+ Memory::Aggregate.new("By Class", &:class_name),
194
+ Memory::Aggregate.new("By Gem", &:gem)
195
+ ], retained_only: true)
196
+
197
+ sampler = Memory::Sampler.new
198
+ sampler.run do
199
+ # Your code here
200
+ 10000.times {"test string"}
201
+ end
202
+
203
+ # Add samples to the custom report:
204
+ report.add(sampler)
205
+ report.print
206
+ ```
207
+
208
+ The `retained_only: true` option (default) focuses on memory leaks by only showing allocations that weren't garbage collected. Set it to `false` to see all allocations:
209
+
210
+ ``` ruby
211
+ # Show all allocations, not just retained ones:
212
+ report = Memory::Report.new([
213
+ Memory::Aggregate.new("By Class", &:class_name)
214
+ ], retained_only: false)
215
+ ```
216
+
217
+ ### Exporting to JSON
218
+
219
+ Reports can be exported as JSON for integration with other tools:
220
+
221
+ ``` ruby
222
+ report = Memory.report do
223
+ # Your code
224
+ data = Array.new(1000) {{value: rand(1000)}}
225
+ end
226
+
227
+ # Export as JSON:
228
+ json_output = report.to_json
229
+ puts json_output
230
+
231
+ # Or as a Ruby hash:
232
+ hash_output = report.as_json
233
+ ```
234
+
235
+ This is useful for:
236
+ - Building custom visualization tools.
237
+ - Integrating with CI/CD pipelines.
238
+ - Tracking memory metrics over time in dashboards.
@@ -0,0 +1,12 @@
1
+ # Automatically generated context index for Utopia::Project guides.
2
+ # Do not edit then files in this directory directly, instead edit the guides and then run `bake utopia:project:agent:context:update`.
3
+ ---
4
+ description: Memory profiling routines for Ruby 2.3+
5
+ metadata:
6
+ documentation_uri: https://socketry.github.io/memory/
7
+ source_code_uri: https://github.com/socketry/memory.git
8
+ files:
9
+ - path: getting-started.md
10
+ title: Getting Started
11
+ description: This guide explains how to get started with `memory`, a Ruby gem for
12
+ profiling memory allocations in your applications.
@@ -1,21 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2020-2024, by Samuel Williams.
4
+ # Copyright, 2020-2025, by Samuel Williams.
5
5
 
6
6
  module Memory
7
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'
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
17
  }.freeze
18
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").
19
22
  def self.formatted_bytes(bytes)
20
23
  return "0 B" if bytes.zero?
21
24
 
@@ -24,6 +27,8 @@ module Memory
24
27
  "%.2f #{UNITS[scale]}" % (bytes / 10.0**scale)
25
28
  end
26
29
 
30
+ # Aggregates memory allocations by a given metric.
31
+ # Groups allocations and tracks totals for memory usage and allocation counts.
27
32
  class Aggregate
28
33
  Total = Struct.new(:memory, :count) do
29
34
  def initialize
@@ -51,6 +56,9 @@ module Memory
51
56
  end
52
57
  end
53
58
 
59
+ # Initialize a new aggregate with a title and metric block.
60
+ # @parameter title [String] The title for this aggregate.
61
+ # @parameter block [Block] A block that extracts the metric from an allocation.
54
62
  def initialize(title, &block)
55
63
  @title = title
56
64
  @metric = block
@@ -64,6 +72,8 @@ module Memory
64
72
  attr :total
65
73
  attr :totals
66
74
 
75
+ # Add an allocation to this aggregate.
76
+ # @parameter allocation [Allocation] The allocation to add.
67
77
  def << allocation
68
78
  metric = @metric.call(allocation)
69
79
  total = @totals[metric]
@@ -75,10 +85,18 @@ module Memory
75
85
  @total.count += 1
76
86
  end
77
87
 
88
+ # Sort totals by a given key.
89
+ # @parameter key [Symbol] The key to sort by (e.g., :memory or :count).
90
+ # @returns [Array] Sorted array of [metric, total] pairs.
78
91
  def totals_by(key)
79
92
  @totals.sort_by{|metric, total| [total[key], metric]}
80
93
  end
81
94
 
95
+ # Print this aggregate to an IO stream.
96
+ # @parameter io [IO] The output stream to write to.
97
+ # @parameter limit [Integer] Maximum number of items to display.
98
+ # @parameter title [String] Optional title override.
99
+ # @parameter level [Integer] Markdown heading level for output.
82
100
  def print(io = $stderr, limit: 10, title: @title, level: 2)
83
101
  io.puts "#{'#' * level} #{title} #{@total}", nil
84
102
 
@@ -89,6 +107,9 @@ module Memory
89
107
  io.puts nil
90
108
  end
91
109
 
110
+ # Convert this aggregate to a JSON-compatible hash.
111
+ # @parameter options [Hash | Nil] Optional JSON serialization options.
112
+ # @returns [Hash] JSON-compatible representation.
92
113
  def as_json(options = nil)
93
114
  {
94
115
  title: @title,
@@ -98,7 +119,12 @@ module Memory
98
119
  end
99
120
  end
100
121
 
122
+ # Aggregates memory allocations by value.
123
+ # Groups allocations by their actual values (e.g., string contents) and creates sub-aggregates.
101
124
  class ValueAggregate
125
+ # Initialize a new value aggregate.
126
+ # @parameter title [String] The title for this aggregate.
127
+ # @parameter block [Block] A block that extracts the metric from an allocation.
102
128
  def initialize(title, &block)
103
129
  @title = title
104
130
  @metric = block
@@ -110,6 +136,8 @@ module Memory
110
136
  attr :metric
111
137
  attr :aggregates
112
138
 
139
+ # Add an allocation to this value aggregate.
140
+ # @parameter allocation [Allocation] The allocation to add.
113
141
  def << allocation
114
142
  if value = allocation.value
115
143
  aggregate = @aggregates[value]
@@ -118,10 +146,17 @@ module Memory
118
146
  end
119
147
  end
120
148
 
149
+ # Sort aggregates by a given key.
150
+ # @parameter key [Symbol] The key to sort by (e.g., :memory or :count).
151
+ # @returns [Array] Sorted array of [value, aggregate] pairs.
121
152
  def aggregates_by(key)
122
153
  @aggregates.sort_by{|value, aggregate| [aggregate.total[key], value]}
123
154
  end
124
155
 
156
+ # Print this value aggregate to an IO stream.
157
+ # @parameter io [IO] The output stream to write to.
158
+ # @parameter limit [Integer] Maximum number of items to display.
159
+ # @parameter level [Integer] Markdown heading level for output.
125
160
  def print(io = $stderr, limit: 10, level: 2)
126
161
  io.puts "#{'#' * level} #{@title}", nil
127
162
 
@@ -130,6 +165,9 @@ module Memory
130
165
  end
131
166
  end
132
167
 
168
+ # Convert this value aggregate to a JSON-compatible hash.
169
+ # @parameter options [Hash | Nil] Optional JSON serialization options.
170
+ # @returns [Hash] JSON-compatible representation.
133
171
  def as_json(options = nil)
134
172
  {
135
173
  title: @title,
data/lib/memory/cache.rb CHANGED
@@ -8,17 +8,23 @@
8
8
  # Copyright, 2016, by Hamdi Akoğuz.
9
9
  # Copyright, 2018, by Jonas Peschla.
10
10
  # Copyright, 2020, by Jean Boussier.
11
- # Copyright, 2020-2022, by Samuel Williams.
11
+ # Copyright, 2020-2025, by Samuel Williams.
12
12
 
13
13
  module Memory
14
+ # Cache for storing and looking up allocation metadata.
15
+ # Caches gem names, file locations, class names, and string values to reduce memory overhead during profiling.
14
16
  class Cache
17
+ # Initialize a new cache with empty lookup tables.
15
18
  def initialize
16
19
  @gem_guess_cache = Hash.new
17
- @location_cache = Hash.new { |h, k| h[k] = Hash.new.compare_by_identity }
20
+ @location_cache = Hash.new {|h, k| h[k] = Hash.new.compare_by_identity}
18
21
  @class_name_cache = Hash.new.compare_by_identity
19
22
  @string_cache = Hash.new
20
23
  end
21
-
24
+
25
+ # Guess the gem or library name from a file path.
26
+ # @parameter path [String] The file path to analyze.
27
+ # @returns [String] The guessed gem name, stdlib component, or "other".
22
28
  def guess_gem(path)
23
29
  @gem_guess_cache[path] ||=
24
30
  if /(\/gems\/.*)*\/gems\/(?<gemname>[^\/]+)/ =~ path
@@ -33,15 +39,26 @@ module Memory
33
39
  "other"
34
40
  end
35
41
  end
36
-
42
+
43
+ # Look up and cache a file location string.
44
+ # @parameter file [String] The source file path.
45
+ # @parameter line [Integer] The line number.
46
+ # @returns [String] The formatted location string "file:line".
37
47
  def lookup_location(file, line)
38
48
  @location_cache[file][line] ||= "#{file}:#{line}"
39
49
  end
40
-
50
+
51
+ # Look up and cache a class name.
52
+ # @parameter klass [Class] The class object.
53
+ # @returns [String] The class name or `unknown` if unavailable.
41
54
  def lookup_class_name(klass)
42
- @class_name_cache[klass] ||= ((klass.is_a?(Class) && klass.name) || '<<Unknown>>').to_s
55
+ @class_name_cache[klass] ||= ((klass.is_a?(Class) && klass.name) || "unknown").to_s
43
56
  end
44
-
57
+
58
+ # Look up and cache a string value.
59
+ # Strings are truncated to 64 characters to reduce memory usage.
60
+ # @parameter obj [String] The string object to cache.
61
+ # @returns [String] A cached copy of the string (truncated to 64 characters).
45
62
  def lookup_string(obj)
46
63
  # This string is shortened to 200 characters which is what the string report shows
47
64
  # The string report can still list unique strings longer than 200 characters
data/lib/memory/deque.rb CHANGED
@@ -1,18 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2020-2022, by Samuel Williams.
4
+ # Copyright, 2020-2025, by Samuel Williams.
5
5
 
6
- require 'objspace'
7
- require 'msgpack'
6
+ require "objspace"
7
+ require "msgpack"
8
8
 
9
9
  module Memory
10
+ # A double-ended queue implementation optimized for memory profiling.
11
+ # Stores items in segments to reduce memory reallocation overhead.
10
12
  class Deque
13
+ # Initialize a new empty deque.
11
14
  def initialize
12
15
  @segments = []
13
16
  @last = nil
14
17
  end
15
18
 
19
+ # Freeze this deque and all its segments.
20
+ # @returns [Deque] Self.
16
21
  def freeze
17
22
  return self if frozen?
18
23
 
@@ -24,6 +29,9 @@ module Memory
24
29
 
25
30
  include Enumerable
26
31
 
32
+ # Concatenate an array segment to this deque.
33
+ # @parameter segment [Array] The segment to append.
34
+ # @returns [Deque] Self.
27
35
  def concat(segment)
28
36
  @segments << segment
29
37
  @last = nil
@@ -31,6 +39,9 @@ module Memory
31
39
  return self
32
40
  end
33
41
 
42
+ # Append an item to this deque.
43
+ # @parameter item [Object] The item to append.
44
+ # @returns [Deque] Self.
34
45
  def << item
35
46
  unless @last
36
47
  @last = []
@@ -42,12 +53,16 @@ module Memory
42
53
  return self
43
54
  end
44
55
 
56
+ # Iterate over all items in the deque.
57
+ # @parameter block [Block] The block to yield each item to.
45
58
  def each(&block)
46
59
  @segments.each do |segment|
47
60
  segment.each(&block)
48
61
  end
49
62
  end
50
63
 
64
+ # Get the total number of items in the deque.
65
+ # @returns [Integer] The total number of items across all segments.
51
66
  def size
52
67
  @segments.sum(&:size)
53
68
  end
data/lib/memory/report.rb CHANGED
@@ -1,12 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2020-2024, by Samuel Williams.
4
+ # Copyright, 2020-2025, by Samuel Williams.
5
5
 
6
- require_relative 'aggregate'
6
+ require_relative "aggregate"
7
7
 
8
8
  module Memory
9
+ # A report containing aggregated memory allocation statistics.
10
+ # Collects and organizes allocation data by various metrics.
9
11
  class Report
12
+ # Create a general-purpose report with standard aggregates.
13
+ # @parameter options [Hash] Options to pass to the report constructor.
14
+ # @returns [Report] A new report with standard aggregates.
10
15
  def self.general(**options)
11
16
  Report.new([
12
17
  Aggregate.new("By Gem", &:gem),
@@ -18,6 +23,9 @@ module Memory
18
23
  ], **options)
19
24
  end
20
25
 
26
+ # Initialize a new report with the given aggregates.
27
+ # @parameter aggregates [Array] Array of Aggregate or ValueAggregate instances.
28
+ # @parameter retained_only [Boolean] Whether to only include retained allocations in aggregates.
21
29
  def initialize(aggregates, retained_only: true)
22
30
  @retained_only = retained_only
23
31
 
@@ -53,6 +61,8 @@ module Memory
53
61
  end
54
62
  end
55
63
 
64
+ # Print this report to an IO stream.
65
+ # @parameter io [IO] The output stream to write to.
56
66
  def print(io = $stderr)
57
67
  if @retained_only
58
68
  io.puts "\# Retained Memory Profile", nil
@@ -69,6 +79,9 @@ module Memory
69
79
  end
70
80
  end
71
81
 
82
+ # Convert this report to a JSON-compatible hash.
83
+ # @parameter options [Hash | Nil] Optional JSON serialization options.
84
+ # @returns [Hash] JSON-compatible representation.
72
85
  def as_json(options = nil)
73
86
  {
74
87
  total_allocated: @total_allocated.as_json(options),
@@ -77,8 +90,16 @@ module Memory
77
90
  }
78
91
  end
79
92
 
80
- def to_json(...)
81
- as_json.to_json(...)
82
- 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>"
83
103
  end
84
104
  end
105
+ end
@@ -1,13 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2020-2022, by Samuel Williams.
4
+ # Copyright, 2020-2025, by Samuel Williams.
5
5
 
6
- require_relative '../sampler'
6
+ require_relative "../sampler"
7
7
 
8
8
  module Memory
9
+ # @namespace
9
10
  module RSpec
11
+ # Integration with RSpec for memory profiling test suites.
12
+ # Profiles memory allocations for RSpec example groups and generates reports.
10
13
  module Profiler
14
+ # Profile memory allocations for RSpec examples.
15
+ # @parameter scope [Object] The RSpec scope to apply profiling hooks to.
11
16
  def self.profile(scope)
12
17
  memory_sampler = nil
13
18
 
@@ -11,16 +11,21 @@
11
11
  # Copyright, 2018, by Jonas Peschla.
12
12
  # Copyright, 2018, by Espartaco Palma.
13
13
  # Copyright, 2020, by Jean Boussier.
14
- # Copyright, 2020-2024, by Samuel Williams.
14
+ # Copyright, 2020-2025, by Samuel Williams.
15
15
 
16
- require 'objspace'
17
- require 'msgpack'
18
- require 'console'
16
+ require "objspace"
17
+ require "msgpack"
18
+ require "json"
19
+ require "console"
19
20
 
20
- require_relative 'cache'
21
+ require_relative "cache"
21
22
 
22
23
  module Memory
24
+ # MessagePack factory wrapper for serializing allocations.
25
+ # Registers custom types for efficient serialization of allocation data.
23
26
  class Wrapper < MessagePack::Factory
27
+ # Initialize the wrapper with a cache.
28
+ # @parameter cache [Cache] The cache to use for allocation metadata.
24
29
  def initialize(cache)
25
30
  super()
26
31
 
@@ -61,6 +66,74 @@ module Memory
61
66
  # end
62
67
  # ~~~
63
68
  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
+
135
+ # Initialize a new sampler.
136
+ # @parameter filter [Block | Nil] Optional filter block to select which allocations to track.
64
137
  def initialize(&filter)
65
138
  @filter = filter
66
139
 
@@ -69,6 +142,8 @@ module Memory
69
142
  @allocated = Array.new
70
143
  end
71
144
 
145
+ # Generate a human-readable representation of this sampler.
146
+ # @returns [String] String showing the number of allocations tracked.
72
147
  def inspect
73
148
  "#<#{self.class} #{@allocated.size} allocations>"
74
149
  end
@@ -79,6 +154,8 @@ module Memory
79
154
  attr :wrapper
80
155
  attr :allocated
81
156
 
157
+ # Start tracking memory allocations.
158
+ # Disables GC and begins ObjectSpace allocation tracing.
82
159
  def start
83
160
  GC.disable
84
161
  3.times{GC.start}
@@ -90,6 +167,8 @@ module Memory
90
167
  ObjectSpace.trace_object_allocations_start
91
168
  end
92
169
 
170
+ # Stop tracking allocations and determine which ones were retained.
171
+ # Re-enables GC and marks retained allocations.
93
172
  def stop
94
173
  ObjectSpace.trace_object_allocations_stop
95
174
  allocated = track_allocations(@generation)
@@ -116,6 +195,9 @@ module Memory
116
195
  ObjectSpace.trace_object_allocations_clear
117
196
  end
118
197
 
198
+ # Serialize allocations to MessagePack format.
199
+ # @parameter io [IO | Nil] Optional IO stream to write to. If nil, returns serialized data.
200
+ # @returns [String | Nil] Serialized data if no IO stream provided, otherwise nil.
119
201
  def dump(io = nil)
120
202
  Console.logger.debug(self, "Dumping allocations: #{@allocated.size}")
121
203
 
@@ -128,6 +210,8 @@ module Memory
128
210
  end
129
211
  end
130
212
 
213
+ # Load allocations from MessagePack-serialized data.
214
+ # @parameter data [String] The serialized allocation data.
131
215
  def load(data)
132
216
  allocations = @wrapper.load(data)
133
217
 
@@ -136,6 +220,9 @@ module Memory
136
220
  @allocated.concat(allocations)
137
221
  end
138
222
 
223
+ # Generate a report from the tracked allocations.
224
+ # @parameter options [Hash] Options to pass to the report constructor.
225
+ # @returns [Report] A report containing allocation statistics.
139
226
  def report(**options)
140
227
  report = Report.general(**options)
141
228
 
@@ -7,5 +7,5 @@
7
7
  # Copyright, 2020-2024, by Samuel Williams.
8
8
 
9
9
  module Memory
10
- VERSION = "0.5.0"
10
+ VERSION = "0.6.1"
11
11
  end
data/lib/memory.rb CHANGED
@@ -5,14 +5,20 @@
5
5
  # Copyright, 2014, by Søren Skovsbøll.
6
6
  # Copyright, 2017, by Nick LaMuro.
7
7
  # Copyright, 2018, by Jonas Peschla.
8
- # Copyright, 2020-2022, by Samuel Williams.
8
+ # Copyright, 2020-2025, by Samuel Williams.
9
9
 
10
10
  require_relative "memory/version"
11
11
  require_relative "memory/cache"
12
12
  require_relative "memory/report"
13
13
  require_relative "memory/sampler"
14
14
 
15
+ # Memory profiler for Ruby applications.
16
+ # Provides tools to track and analyze memory allocations and retention.
15
17
  module Memory
18
+ # Capture memory allocations from a block of code.
19
+ # @parameter report [Report | Nil] Optional report instance to add samples to.
20
+ # @parameter block [Block] The code to profile.
21
+ # @returns [Report] A report containing allocation statistics.
16
22
  def self.capture(report = nil, &block)
17
23
  sampler = Sampler.new
18
24
  sampler.run(&block)
@@ -23,6 +29,9 @@ module Memory
23
29
  return report
24
30
  end
25
31
 
32
+ # Generate a memory allocation report for a block of code.
33
+ # @parameter block [Block] The code to profile.
34
+ # @returns [Report] A report containing allocation statistics.
26
35
  def self.report(&block)
27
36
  self.capture(&block)
28
37
  end
data/license.md CHANGED
@@ -24,7 +24,7 @@ Copyright, 2019-2020, by Jean Boussier.
24
24
  Copyright, 2019, by Ashwin Maroli.
25
25
  Copyright, 2019, by Olle Jonsson.
26
26
  Copyright, 2019, by Danny Ben Shitrit.
27
- Copyright, 2020-2024, by Samuel Williams.
27
+ Copyright, 2020-2025, by Samuel Williams.
28
28
 
29
29
  Permission is hereby granted, free of charge, to any person obtaining a copy
30
30
  of this software and associated documentation files (the "Software"), to deal
data/readme.md CHANGED
@@ -19,68 +19,47 @@ $ bundle add 'memory'
19
19
 
20
20
  ## Usage
21
21
 
22
- ``` ruby
23
- require 'memory'
24
-
25
- report = Memory.report do
26
- # run your code here
27
- end
28
-
29
- report.print
30
- ```
31
-
32
- Or, you can use the `.start`/`.stop` methods as well:
33
-
34
- ``` ruby
35
- require 'memory'
36
-
37
- sampler = Memory::Sampler.new
22
+ Please see the [project documentation](https://socketry.github.io/memory/) for more details.
38
23
 
39
- sampler.start
40
- # run your code here
41
- sampler.stop
42
-
43
- report = sampler.report
44
- report.print
45
- ```
24
+ - [Getting Started](https://socketry.github.io/memory/guides/getting-started/index) - This guide explains how to get started with `memory`, a Ruby gem for profiling memory allocations in your applications.
46
25
 
47
26
  ### RSpec Integration
48
27
 
49
28
  ``` ruby
50
29
  memory_sampler = nil
51
30
  config.before(:all) do |example_group|
52
- name = example_group.class.description.gsub(/[^\w]+/, '-')
31
+ name = example_group.class.description.gsub(/[^\w]+/, "-")
53
32
  path = "#{name}.mprof"
54
-
33
+
55
34
  skip if File.exist?(path)
56
-
35
+
57
36
  memory_sampler = Memory::Sampler.new
58
37
  memory_sampler.start
59
38
  end
60
39
 
61
40
  config.after(:all) do |example_group|
62
- name = example_group.class.description.gsub(/[^\w]+/, '-')
41
+ name = example_group.class.description.gsub(/[^\w]+/, "-")
63
42
  path = "#{name}.mprof"
64
-
43
+
65
44
  if memory_sampler
66
45
  memory_sampler.stop
67
-
46
+
68
47
  File.open(path, "w", encoding: Encoding::BINARY) do |io|
69
48
  memory_sampler.dump(io)
70
49
  end
71
-
50
+
72
51
  memory_sampler = nil
73
52
  end
74
53
  end
75
54
 
76
55
  config.after(:suite) do
77
56
  memory_sampler = Memory::Sampler.new
78
-
79
- Dir.glob('*.mprof') do |path|
57
+
58
+ Dir.glob("*.mprof") do |path|
80
59
  $stderr.puts "Loading #{path}..."
81
60
  memory_sampler.load(File.read(path, encoding: Encoding::BINARY))
82
61
  end
83
-
62
+
84
63
  $stderr.puts "Memory usage:"
85
64
  memory_sampler.report.print
86
65
  end
@@ -111,6 +90,14 @@ config.after(:suite) do |example|
111
90
  end
112
91
  ```
113
92
 
93
+ ## Releases
94
+
95
+ Please see the [project releases](https://socketry.github.io/memory/releases/index) for all releases.
96
+
97
+ ### v0.6.0
98
+
99
+ - Add agent context.
100
+
114
101
  ## Contributing
115
102
 
116
103
  We welcome contributions to this project.
@@ -123,8 +110,8 @@ We welcome contributions to this project.
123
110
 
124
111
  ### Developer Certificate of Origin
125
112
 
126
- This project uses the [Developer Certificate of Origin](https://developercertificate.org/). All contributors to this project must agree to this document to have their contributions accepted.
113
+ In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
127
114
 
128
- ### Contributor Covenant
115
+ ### Community Guidelines
129
116
 
130
- This project is governed by the [Contributor Covenant](https://www.contributor-covenant.org/). All contributors and participants agree to abide by its terms.
117
+ This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
data/releases.md ADDED
@@ -0,0 +1,5 @@
1
+ # Releases
2
+
3
+ ## v0.6.0
4
+
5
+ - Add agent context.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: memory
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Saffron
@@ -29,7 +29,6 @@ authors:
29
29
  - Olle Jonsson
30
30
  - Vasily Kolesnikov
31
31
  - William Tabi
32
- autorequire:
33
32
  bindir: bin
34
33
  cert_chain:
35
34
  - |
@@ -61,7 +60,7 @@ cert_chain:
61
60
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
62
61
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
63
62
  -----END CERTIFICATE-----
64
- date: 2024-06-27 00:00:00.000000000 Z
63
+ date: 1980-01-02 00:00:00.000000000 Z
65
64
  dependencies:
66
65
  - !ruby/object:Gem::Dependency
67
66
  name: bake
@@ -105,13 +104,14 @@ dependencies:
105
104
  - - ">="
106
105
  - !ruby/object:Gem::Version
107
106
  version: '0'
108
- description:
109
- email:
110
107
  executables: []
111
108
  extensions: []
112
109
  extra_rdoc_files: []
113
110
  files:
114
- - bake/memory/profiler.rb
111
+ - bake/memory/report.rb
112
+ - bake/memory/sampler.rb
113
+ - context/getting-started.md
114
+ - context/index.yaml
115
115
  - lib/memory.rb
116
116
  - lib/memory/aggregate.rb
117
117
  - lib/memory/cache.rb
@@ -122,13 +122,13 @@ files:
122
122
  - lib/memory/version.rb
123
123
  - license.md
124
124
  - readme.md
125
+ - releases.md
125
126
  homepage: https://github.com/socketry/memory
126
127
  licenses:
127
128
  - MIT
128
129
  metadata:
129
130
  documentation_uri: https://socketry.github.io/memory/
130
131
  source_code_uri: https://github.com/socketry/memory.git
131
- post_install_message:
132
132
  rdoc_options: []
133
133
  require_paths:
134
134
  - lib
@@ -136,15 +136,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
136
136
  requirements:
137
137
  - - ">="
138
138
  - !ruby/object:Gem::Version
139
- version: '3.1'
139
+ version: '3.2'
140
140
  required_rubygems_version: !ruby/object:Gem::Requirement
141
141
  requirements:
142
142
  - - ">="
143
143
  - !ruby/object:Gem::Version
144
144
  version: '0'
145
145
  requirements: []
146
- rubygems_version: 3.5.11
147
- signing_key:
146
+ rubygems_version: 3.7.2
148
147
  specification_version: 4
149
148
  summary: Memory profiling routines for Ruby 2.3+
150
149
  test_files: []
metadata.gz.sig CHANGED
Binary file
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2013-2018, by Sam Saffron.
5
- # Copyright, 2014, by Richard Schneeman.
6
- # Copyright, 2018, by Jonas Peschla.
7
- # Copyright, 2020-2022, by Samuel Williams.
8
-
9
- def check(paths:)
10
- require 'console'
11
-
12
- total_size = paths.sum{|path| File.size(path)}
13
-
14
- require_relative '../../lib/memory'
15
-
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
-
38
- report.print($stdout)
39
- end