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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 9dc143b51ee61fd99bfbe646490321b4dc106981
4
- data.tar.gz: 679e188284bee7611d02e3bdf79b19a2ba2902e7
2
+ SHA256:
3
+ metadata.gz: 72517cae4d567a611d0fb3a2265ac2dd443fd3abae577ddbee0ec5e9b1d0f611
4
+ data.tar.gz: 003571cf8650a518d5a73ff0a30f4fba92ef0ee4572ff928ec261dce5fc6b2f0
5
5
  SHA512:
6
- metadata.gz: 9ee3cde0472d1c96deb9d30ed7f180ceae46046000bb54f676f19179f5c185a1cc34ab0ce01cf497b3465c2603b03f5541632706d10cacd994a6212de3a943ba
7
- data.tar.gz: 9002ee4f5c8d1444c833ddf25930918c9645af00f82859b839cd93a1301bc46d489b648bfd501649944d9d7494d88126c023f26e2b47e2dfc4b9f26d33b350fa
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
+ ~~~
@@ -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)