memory 0.6.0 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5bd34b6e9c3ddd2a10a3900d0507a8af35af246c5aa2ad760eb2a204731e6fbc
4
- data.tar.gz: 798ec525eeba73a5e08d9f557b98727096dfde83c01aa007d83efa6f066177e9
3
+ metadata.gz: 266bdfec74c35392a9c086c366df9bb6ab4fb5e1b162b98d3757f690fff09ded
4
+ data.tar.gz: dffe8472314144b5786b5c445c6e01973114e9c3915d08aae0352fc1be76d329
5
5
  SHA512:
6
- metadata.gz: b39e94db71df052b83bd85fc63aa4c37801470eb3daeaae687e5d3c208e280299bfba9dee11da71a82c2d9b34f9935d1662be1e18eb6f99043816963c2cd6591
7
- data.tar.gz: 87ef4c2aae0f96a507de45f0d60b167102ba07b05e7851af80c2e306ae6e56c3a35202c766e3202485956df8a0ca4fb14b111ade24344078e09ecdcc3cfaa56e
6
+ metadata.gz: 4dcf3e65b4a7dee68b55919dc44004149a6febe80fde00d539d2b0361eea62dd47ab742006ef7ed75a2980829f0f58cbf1c341098ae394ac4ac3b7a4d816752b
7
+ data.tar.gz: '038621f8d88603eb8b0b2a89fe13e4007bc100ce0c16c53febd9d02a6cf4fa3a00c6c3e967bfa8a6aa6507e3611266cae774dc016d330fe5b22d5322beee12e3'
checksums.yaml.gz.sig CHANGED
@@ -1,4 +1,2 @@
1
-
2
- 5�=Iπ,�4���׼k3�Ϟ�]
3
- ��V�K��q%o¯ڄ�?i4b�@�Y_�@���:�k�9��`ȁ2dR�x����5�������I'(����W�Ѕ#(T��G40q�UtT³{�H���C�t�J(��hjf_�%*Q�2�4�T%�.�:#fK���Gi�P�Z���h���nh�ڥ�ȇwS�vƏ�;&�
4
- �ČTk[wr��t�k�vN���&��c}Rn�cA�J�c�`�S��`������/x�� ��H�Ř�>
1
+ =��z�6�r��g9��j�V�,��$�$0�����SP��.��?֋�Yb
2
+ �F�$��}����rM�|H�n���U�̮�T��q��`�i0� q��zA=b:���@)��������3�;p������-N�����6.$Zc��] �d���O�{@"���M���?���SWQ�-�
@@ -5,7 +5,7 @@
5
5
 
6
6
  def initialize(...)
7
7
  super
8
-
8
+
9
9
  require_relative "../../lib/memory"
10
10
  end
11
11
 
@@ -26,6 +26,6 @@ def print(input:)
26
26
  end
27
27
 
28
28
  report.print($stderr)
29
-
29
+
30
30
  return report
31
31
  end
@@ -6,7 +6,7 @@
6
6
  def initialize(...)
7
7
  super
8
8
 
9
- require_relative '../../lib/memory'
9
+ require_relative "../../lib/memory"
10
10
  end
11
11
 
12
12
  # Load a sampler from one or more .mprof files.
@@ -27,7 +27,7 @@ def load(paths:)
27
27
  paths.each do |path|
28
28
  Console.logger.info(sampler, "Loading #{path}, #{Memory.formatted_bytes File.size(path)}")
29
29
 
30
- File.open(path, 'r', encoding: Encoding::BINARY) do |io|
30
+ File.open(path, "r", encoding: Encoding::BINARY) do |io|
31
31
  unpacker = wrapper.unpacker(io)
32
32
  count = unpacker.read_array_header
33
33
 
@@ -56,7 +56,7 @@ end
56
56
  # @parameter output [String] Path to write the .mprof file.
57
57
  # @returns [Memory::Sampler] The input sampler.
58
58
  def dump(path, input:)
59
- File.open(path, 'w', encoding: Encoding::BINARY) do |io|
59
+ File.open(path, "w", encoding: Encoding::BINARY) do |io|
60
60
  input.dump(io)
61
61
  end
62
62
 
@@ -76,7 +76,7 @@ def load_object_space_dump(path)
76
76
  Console.logger.info(self, "Loading heap dump from #{path} (#{Memory.formatted_bytes(file_size)})")
77
77
 
78
78
  sampler = nil
79
- File.open(path, 'r') do |io|
79
+ File.open(path, "r") do |io|
80
80
  sampler = Memory::Sampler.load_object_space_dump(io) do |line_count, object_count|
81
81
  # Update progress based on bytes read:
82
82
  progress.increment(io.pos - progress.current)
@@ -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.
data/lib/memory/report.rb CHANGED
@@ -90,16 +90,16 @@ module Memory
90
90
  }
91
91
  end
92
92
 
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>"
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>"
103
+ end
103
104
  end
104
105
  end
105
- end
@@ -66,72 +66,72 @@ module Memory
66
66
  # end
67
67
  # ~~~
68
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']
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
96
79
 
97
- # Get allocation information (may be nil if tracing wasn't enabled)
98
- file = object['file'] || '(unknown)'
99
- line_number = object['line'] || 0
80
+ line_count = 0
81
+ object_count = 0
82
+ report_interval = 10000
100
83
 
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
- )
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
119
128
 
120
- sampler.allocated << allocation
121
- object_count += 1
129
+ # Final progress report
130
+ block.call(line_count, object_count) if block
122
131
 
123
- # Report progress periodically
124
- if block && (object_count % report_interval == 0)
125
- block.call(line_count, object_count)
126
- end
132
+ return sampler
127
133
  end
128
134
 
129
- # Final progress report
130
- block.call(line_count, object_count) if block
131
-
132
- return sampler
133
- end
134
-
135
135
  # Initialize a new sampler.
136
136
  # @parameter filter [Block | Nil] Optional filter block to select which allocations to track.
137
137
  def initialize(&filter)
@@ -231,6 +231,22 @@ module Memory
231
231
  return report
232
232
  end
233
233
 
234
+ # Convert this sampler to a JSON-compatible summary.
235
+ # Returns the allocation count without iterating through all allocations.
236
+ # @parameter options [Hash | Nil] Optional JSON serialization options.
237
+ # @returns [Hash] JSON-compatible summary of sampler data.
238
+ def as_json(options = nil)
239
+ {
240
+ allocations: @allocated.size
241
+ }
242
+ end
243
+
244
+ # Convert this sampler to a JSON string.
245
+ # @returns [String] JSON representation of this sampler summary.
246
+ def to_json(...)
247
+ as_json.to_json(...)
248
+ end
249
+
234
250
  # Collects object allocation and memory of ruby code inside of passed block.
235
251
  def run(&block)
236
252
  start
@@ -4,8 +4,8 @@
4
4
  # Copyright, 2013-2019, by Sam Saffron.
5
5
  # Copyright, 2015-2016, by Dave Gynn.
6
6
  # Copyright, 2018, by Jonas Peschla.
7
- # Copyright, 2020-2024, by Samuel Williams.
7
+ # Copyright, 2020-2025, by Samuel Williams.
8
8
 
9
9
  module Memory
10
- VERSION = "0.6.0"
10
+ VERSION = "0.7.0"
11
11
  end
data/readme.md CHANGED
@@ -94,6 +94,10 @@ end
94
94
 
95
95
  Please see the [project releases](https://socketry.github.io/memory/releases/index) for all releases.
96
96
 
97
+ ### v0.7.0
98
+
99
+ - Add `Memory::Sampler#as_json` and `#to_json`.
100
+
97
101
  ### v0.6.0
98
102
 
99
103
  - Add agent context.
data/releases.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Releases
2
2
 
3
+ ## v0.7.0
4
+
5
+ - Add `Memory::Sampler#as_json` and `#to_json`.
6
+
3
7
  ## v0.6.0
4
8
 
5
9
  - 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.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Saffron
@@ -110,6 +110,8 @@ extra_rdoc_files: []
110
110
  files:
111
111
  - bake/memory/report.rb
112
112
  - bake/memory/sampler.rb
113
+ - context/getting-started.md
114
+ - context/index.yaml
113
115
  - lib/memory.rb
114
116
  - lib/memory/aggregate.rb
115
117
  - lib/memory/cache.rb
metadata.gz.sig CHANGED
Binary file