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 +4 -4
- checksums.yaml.gz.sig +2 -4
- data/bake/memory/report.rb +31 -0
- data/bake/memory/sampler.rb +89 -0
- data/context/getting-started.md +238 -0
- data/context/index.yaml +12 -0
- data/lib/memory/aggregate.rb +48 -10
- data/lib/memory/cache.rb +24 -7
- data/lib/memory/deque.rb +18 -3
- data/lib/memory/report.rb +26 -5
- data/lib/memory/rspec/profiler.rb +7 -2
- data/lib/memory/sampler.rb +92 -5
- data/lib/memory/version.rb +1 -1
- data/lib/memory.rb +10 -1
- data/license.md +1 -1
- data/readme.md +23 -36
- data/releases.md +5 -0
- data.tar.gz.sig +0 -0
- metadata +9 -10
- metadata.gz.sig +0 -0
- data/bake/memory/profiler.rb +0 -39
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8ab40ebc992286597b6a01b5157fca5393b29563c7131483d2e7839f15172541
|
|
4
|
+
data.tar.gz: d5b9b73ea305840435c242a6e34bf755d94e77350846f6618467fef4fa7561ff
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5a10c287ab778c7deb2e12b5a04e7214107ae7afbdf12e57af3423128fe0dc9c233735892ae97ec18d90f47ba46856f08ab5624389a1cb61dbdc4d9af3733bf3
|
|
7
|
+
data.tar.gz: db0d83a8c69a4bdf37e058d6a9e304a7b0608791f8a1f89ad1f2864f2d4119aa8c61c9726b813800e4b45506e1cfa2eec7227ca8eeed99170b931927878e4efb
|
checksums.yaml.gz.sig
CHANGED
|
@@ -1,4 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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�Ws����':G
|
|
1
|
+
5�^�R�M�(�KD��n����^�z<~�"�i�yAP����Qt��p�<D��2jlyIe�xf ���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.
|
data/context/index.yaml
ADDED
|
@@ -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/aggregate.rb
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2020-
|
|
4
|
+
# Copyright, 2020-2025, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
module Memory
|
|
7
7
|
UNITS = {
|
|
8
|
-
0 =>
|
|
9
|
-
3 =>
|
|
10
|
-
6 =>
|
|
11
|
-
9 =>
|
|
12
|
-
12 =>
|
|
13
|
-
15 =>
|
|
14
|
-
18 =>
|
|
15
|
-
21 =>
|
|
16
|
-
24 =>
|
|
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-
|
|
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 {
|
|
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) ||
|
|
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-
|
|
4
|
+
# Copyright, 2020-2025, by Samuel Williams.
|
|
5
5
|
|
|
6
|
-
require
|
|
7
|
-
require
|
|
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-
|
|
4
|
+
# Copyright, 2020-2025, by Samuel Williams.
|
|
5
5
|
|
|
6
|
-
require_relative
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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-
|
|
4
|
+
# Copyright, 2020-2025, by Samuel Williams.
|
|
5
5
|
|
|
6
|
-
require_relative
|
|
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
|
|
data/lib/memory/sampler.rb
CHANGED
|
@@ -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-
|
|
14
|
+
# Copyright, 2020-2025, by Samuel Williams.
|
|
15
15
|
|
|
16
|
-
require
|
|
17
|
-
require
|
|
18
|
-
require
|
|
16
|
+
require "objspace"
|
|
17
|
+
require "msgpack"
|
|
18
|
+
require "json"
|
|
19
|
+
require "console"
|
|
19
20
|
|
|
20
|
-
require_relative
|
|
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
|
|
data/lib/memory/version.rb
CHANGED
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
###
|
|
115
|
+
### Community Guidelines
|
|
129
116
|
|
|
130
|
-
This project is
|
|
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
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.
|
|
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:
|
|
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/
|
|
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.
|
|
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.
|
|
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
|
data/bake/memory/profiler.rb
DELETED
|
@@ -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
|