memory-profiler 1.0.3 → 1.1.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 +5 -5
- checksums.yaml.gz.sig +0 -0
- data/context/getting-started.md +229 -0
- data/context/index.yaml +12 -0
- data/ext/extconf.rb +37 -0
- data/ext/memory/profiler/capture.c +576 -0
- data/ext/memory/profiler/capture.h +9 -0
- data/ext/memory/profiler/profiler.c +17 -0
- data/lib/memory/profiler/call_tree.rb +189 -0
- data/lib/memory/profiler/capture.rb +6 -0
- data/lib/memory/profiler/sampler.rb +292 -0
- data/lib/memory/profiler/version.rb +13 -0
- data/lib/memory/tracker.rb +9 -0
- data/license.md +21 -0
- data/readme.md +45 -0
- data/releases.md +5 -0
- data.tar.gz.sig +1 -0
- metadata +61 -36
- metadata.gz.sig +0 -0
- data/Gemfile +0 -11
- data/LICENSE +0 -13
- data/README +0 -71
- data/Rakefile +0 -7
- data/lib/memory-profiler.rb +0 -338
- data/memory-profiler.gemspec +0 -27
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 72517cae4d567a611d0fb3a2265ac2dd443fd3abae577ddbee0ec5e9b1d0f611
|
|
4
|
+
data.tar.gz: 003571cf8650a518d5a73ff0a30f4fba92ef0ee4572ff928ec261dce5fc6b2f0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1920d0e7762f0ee1132555dea5aaab002a9afa9c14f9fda4e4ca763ab043b756b620b8379250e47c279f6a3d2e54de6720016cfb7e415ce30e561b89616b8911
|
|
7
|
+
data.tar.gz: d36826fb4449de4569357a71f4613dac7f0d5db5a5b39ea116980e65e27a95da7626c3cbf51eec1811eae92b214119c93c53c17fee2646ad9ed8462080ab9be5
|
checksums.yaml.gz.sig
ADDED
|
Binary file
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
This guide explains how to use `memory-profiler` to detect and diagnose memory leaks in Ruby applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add the gem to your project:
|
|
8
|
+
|
|
9
|
+
~~~ bash
|
|
10
|
+
$ bundle add memory-profiler
|
|
11
|
+
~~~
|
|
12
|
+
|
|
13
|
+
## Core Concepts
|
|
14
|
+
|
|
15
|
+
Memory leaks happen when your application creates objects that should be garbage collected but remain referenced indefinitely. Over time, this causes memory usage to grow unbounded, eventually leading to performance degradation or out-of-memory crashes.
|
|
16
|
+
|
|
17
|
+
`memory-profiler` helps you find memory leaks by tracking object allocations in real-time:
|
|
18
|
+
|
|
19
|
+
- **{ruby Memory::Profiler::Capture}** monitors allocations using Ruby's internal NEWOBJ/FREEOBJ events.
|
|
20
|
+
- **{ruby Memory::Profiler::CallTree}** aggregates allocation call paths to identify leak sources.
|
|
21
|
+
- **No heap enumeration** - uses O(1) counters updated automatically by the VM.
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
### Monitor Memory Growth
|
|
26
|
+
|
|
27
|
+
Start by identifying which classes are accumulating objects:
|
|
28
|
+
|
|
29
|
+
~~~ ruby
|
|
30
|
+
require 'memory/profiler'
|
|
31
|
+
|
|
32
|
+
# Create a capture instance:
|
|
33
|
+
capture = Memory::Profiler::Capture.new
|
|
34
|
+
|
|
35
|
+
# Start tracking all object allocations:
|
|
36
|
+
capture.start
|
|
37
|
+
|
|
38
|
+
# Run your application code...
|
|
39
|
+
run_your_app
|
|
40
|
+
|
|
41
|
+
# Check live object counts for common classes:
|
|
42
|
+
puts "Hashes: #{capture.count_for(Hash)}"
|
|
43
|
+
puts "Arrays: #{capture.count_for(Array)}"
|
|
44
|
+
puts "Strings: #{capture.count_for(String)}"
|
|
45
|
+
|
|
46
|
+
capture.stop
|
|
47
|
+
~~~
|
|
48
|
+
|
|
49
|
+
**What this tells you**: Which object types are growing over time. If Hash count keeps increasing across multiple samples, you likely have a Hash leak.
|
|
50
|
+
|
|
51
|
+
### Find the Leak Source
|
|
52
|
+
|
|
53
|
+
Once you've identified a leaking class, use call path analysis to find WHERE allocations come from:
|
|
54
|
+
|
|
55
|
+
~~~ ruby
|
|
56
|
+
# Create a sampler with call path analysis:
|
|
57
|
+
sampler = Memory::Profiler::Sampler.new(depth: 10)
|
|
58
|
+
|
|
59
|
+
# Track the leaking class with analysis:
|
|
60
|
+
sampler.track_with_analysis(Hash)
|
|
61
|
+
sampler.start
|
|
62
|
+
|
|
63
|
+
# Run code that triggers the leak:
|
|
64
|
+
simulate_leak
|
|
65
|
+
|
|
66
|
+
# Analyze where allocations come from:
|
|
67
|
+
statistics = sampler.statistics(Hash)
|
|
68
|
+
|
|
69
|
+
puts "Live objects: #{statistics[:live_count]}"
|
|
70
|
+
puts "\nTop allocation sources:"
|
|
71
|
+
statistics[:top_paths].first(5).each do |path_data|
|
|
72
|
+
puts "\n#{path_data[:count]} allocations from:"
|
|
73
|
+
path_data[:path].each { |frame| puts " #{frame}" }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
sampler.stop
|
|
77
|
+
~~~
|
|
78
|
+
|
|
79
|
+
**What this shows**: The complete call stacks that led to Hash allocations. Look for unexpected paths or paths that appear repeatedly.
|
|
80
|
+
|
|
81
|
+
## Real-World Example
|
|
82
|
+
|
|
83
|
+
Let's say you notice your app's memory growing over time. Here's how to diagnose it:
|
|
84
|
+
|
|
85
|
+
~~~ ruby
|
|
86
|
+
require 'memory/profiler'
|
|
87
|
+
|
|
88
|
+
# Setup monitoring:
|
|
89
|
+
capture = Memory::Profiler::Capture.new
|
|
90
|
+
capture.start
|
|
91
|
+
|
|
92
|
+
# Take baseline measurement:
|
|
93
|
+
GC.start # Clean up old objects first
|
|
94
|
+
baseline = {
|
|
95
|
+
hashes: capture.count_for(Hash),
|
|
96
|
+
arrays: capture.count_for(Array),
|
|
97
|
+
strings: capture.count_for(String)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Run your application for a period:
|
|
101
|
+
# In production: sample periodically (every 60 seconds)
|
|
102
|
+
# In development: run through typical workflows
|
|
103
|
+
sleep 60
|
|
104
|
+
|
|
105
|
+
# Check what grew:
|
|
106
|
+
current = {
|
|
107
|
+
hashes: capture.count_for(Hash),
|
|
108
|
+
arrays: capture.count_for(Array),
|
|
109
|
+
strings: capture.count_for(String)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# Report growth:
|
|
113
|
+
current.each do |type, count|
|
|
114
|
+
growth = count - baseline[type]
|
|
115
|
+
if growth > 100
|
|
116
|
+
puts "⚠️ #{type} grew by #{growth} objects"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
capture.stop
|
|
121
|
+
~~~
|
|
122
|
+
|
|
123
|
+
If Hashes grew significantly, enable detailed tracking:
|
|
124
|
+
|
|
125
|
+
~~~ ruby
|
|
126
|
+
# Create detailed sampler:
|
|
127
|
+
sampler = Memory::Profiler::Sampler.new(depth: 15)
|
|
128
|
+
sampler.track_with_analysis(Hash)
|
|
129
|
+
sampler.start
|
|
130
|
+
|
|
131
|
+
# Run suspicious code path:
|
|
132
|
+
process_user_requests(1000)
|
|
133
|
+
|
|
134
|
+
# Find the culprits:
|
|
135
|
+
statistics = sampler.statistics(Hash)
|
|
136
|
+
statistics[:top_paths].first(3).each_with_index do |path_data, i|
|
|
137
|
+
puts "\n#{i+1}. #{path_data[:count]} Hash allocations:"
|
|
138
|
+
path_data[:path].first(5).each { |frame| puts " #{frame}" }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
sampler.stop
|
|
142
|
+
~~~
|
|
143
|
+
|
|
144
|
+
## Best Practices
|
|
145
|
+
|
|
146
|
+
### When Tracking in Production
|
|
147
|
+
|
|
148
|
+
1. **Start tracking AFTER startup**: Call `GC.start` before `capture.start` to avoid counting initialization objects
|
|
149
|
+
2. **Use count-only mode for monitoring**: `capture.track(Hash)` (no callback) has minimal overhead
|
|
150
|
+
3. **Enable analysis only when investigating**: Call path analysis has higher overhead
|
|
151
|
+
4. **Sample periodically**: Take measurements every 60 seconds rather than continuously
|
|
152
|
+
5. **Stop when done**: Always call `stop()` to remove event hooks
|
|
153
|
+
|
|
154
|
+
### Performance Considerations
|
|
155
|
+
|
|
156
|
+
**Count-only tracking** (no callback):
|
|
157
|
+
- Minimal overhead (~5-10% on allocation hotpath)
|
|
158
|
+
- Safe for production monitoring
|
|
159
|
+
- Tracks all classes automatically
|
|
160
|
+
|
|
161
|
+
**Call path analysis** (with callback):
|
|
162
|
+
- Higher overhead (captures `caller_locations` on every allocation)
|
|
163
|
+
- Use during investigation, not continuous monitoring
|
|
164
|
+
- Only track specific classes you're investigating
|
|
165
|
+
|
|
166
|
+
### Avoiding False Positives
|
|
167
|
+
|
|
168
|
+
Objects allocated before tracking starts but freed after will show as negative or zero:
|
|
169
|
+
|
|
170
|
+
~~~ ruby
|
|
171
|
+
# ❌ Wrong - counts existing objects:
|
|
172
|
+
capture.start
|
|
173
|
+
100.times { {} }
|
|
174
|
+
GC.start # Frees old + new objects → underflow
|
|
175
|
+
|
|
176
|
+
# ✅ Right - clean slate first:
|
|
177
|
+
GC.start # Clear old objects
|
|
178
|
+
capture.start
|
|
179
|
+
100.times { {} }
|
|
180
|
+
~~~
|
|
181
|
+
|
|
182
|
+
## Common Scenarios
|
|
183
|
+
|
|
184
|
+
### Detecting Cache Leaks
|
|
185
|
+
|
|
186
|
+
~~~ ruby
|
|
187
|
+
# Monitor your cache class:
|
|
188
|
+
capture = Memory::Profiler::Capture.new
|
|
189
|
+
capture.start
|
|
190
|
+
|
|
191
|
+
cache_baseline = capture.count_for(CacheEntry)
|
|
192
|
+
|
|
193
|
+
# Run for a period:
|
|
194
|
+
sleep 300 # 5 minutes
|
|
195
|
+
|
|
196
|
+
cache_current = capture.count_for(CacheEntry)
|
|
197
|
+
|
|
198
|
+
if cache_current > cache_baseline * 2
|
|
199
|
+
puts "⚠️ Cache is leaking! #{cache_current - cache_baseline} entries added"
|
|
200
|
+
# Enable detailed tracking to find the source
|
|
201
|
+
end
|
|
202
|
+
~~~
|
|
203
|
+
|
|
204
|
+
### Finding Retention in Request Processing
|
|
205
|
+
|
|
206
|
+
~~~ ruby
|
|
207
|
+
# Track during request processing:
|
|
208
|
+
sampler = Memory::Profiler::Sampler.new
|
|
209
|
+
sampler.track_with_analysis(Hash)
|
|
210
|
+
sampler.start
|
|
211
|
+
|
|
212
|
+
# Process requests:
|
|
213
|
+
1000.times do
|
|
214
|
+
process_request
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Check if Hashes are being retained:
|
|
218
|
+
statistics = sampler.statistics(Hash)
|
|
219
|
+
|
|
220
|
+
if statistics[:live_count] > 1000
|
|
221
|
+
puts "Leaking #{statistics[:live_count]} Hashes per 1000 requests!"
|
|
222
|
+
statistics[:top_paths].first(3).each do |path_data|
|
|
223
|
+
puts "\n#{path_data[:count]}x from:"
|
|
224
|
+
puts path_data[:path].join("\n ")
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
sampler.stop
|
|
229
|
+
~~~
|
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: Efficient memory allocation tracking with call path analysis.
|
|
5
|
+
metadata:
|
|
6
|
+
documentation_uri: https://socketry.github.io/memory-profiler/
|
|
7
|
+
source_code_uri: https://github.com/socketry/memory-profiler
|
|
8
|
+
files:
|
|
9
|
+
- path: getting-started.md
|
|
10
|
+
title: Getting Started
|
|
11
|
+
description: This guide explains how to use `memory-profiler` to detect and diagnose
|
|
12
|
+
memory leaks in Ruby applications.
|
data/ext/extconf.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Released under the MIT License.
|
|
5
|
+
# Copyright, 2025, by Samuel Williams.
|
|
6
|
+
|
|
7
|
+
require "mkmf"
|
|
8
|
+
|
|
9
|
+
extension_name = "Memory_Profiler"
|
|
10
|
+
|
|
11
|
+
append_cflags(["-Wall", "-Wno-unknown-pragmas", "-std=c99"])
|
|
12
|
+
|
|
13
|
+
if ENV.key?("RUBY_DEBUG")
|
|
14
|
+
$stderr.puts "Enabling debug mode..."
|
|
15
|
+
|
|
16
|
+
append_cflags(["-DRUBY_DEBUG", "-O0"])
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
$srcs = ["memory/profiler/profiler.c", "memory/profiler/capture.c"]
|
|
20
|
+
$VPATH << "$(srcdir)/memory/profiler"
|
|
21
|
+
|
|
22
|
+
# Check for required headers
|
|
23
|
+
have_header("ruby/debug.h") or abort "ruby/debug.h is required"
|
|
24
|
+
have_func("rb_ext_ractor_safe")
|
|
25
|
+
|
|
26
|
+
if ENV.key?("RUBY_SANITIZE")
|
|
27
|
+
$stderr.puts "Enabling sanitizers..."
|
|
28
|
+
|
|
29
|
+
# Add address and undefined behaviour sanitizers:
|
|
30
|
+
$CFLAGS << " -fsanitize=address -fsanitize=undefined -fno-omit-frame-pointer"
|
|
31
|
+
$LDFLAGS << " -fsanitize=address -fsanitize=undefined"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
create_header
|
|
35
|
+
|
|
36
|
+
# Generate the makefile to compile the native binary into `lib`:
|
|
37
|
+
create_makefile(extension_name)
|