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
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
module Memory
|
|
7
|
+
module Profiler
|
|
8
|
+
# Efficient tree structure for tracking allocation call paths.
|
|
9
|
+
# Each node represents a frame in the call stack, with counts of how many
|
|
10
|
+
# allocations occurred at this point in the call path.
|
|
11
|
+
class CallTree
|
|
12
|
+
# Represents a node in the call tree.
|
|
13
|
+
#
|
|
14
|
+
# Each node tracks how many allocations occurred at a specific point in a call path.
|
|
15
|
+
# Nodes form a tree structure where each path from root to leaf represents a unique
|
|
16
|
+
# call stack that led to allocations.
|
|
17
|
+
#
|
|
18
|
+
# Nodes now track both total allocations and currently retained (live) allocations.
|
|
19
|
+
class Node
|
|
20
|
+
# Create a new call tree node.
|
|
21
|
+
#
|
|
22
|
+
# @parameter location [Thread::Backtrace::Location] The source location for this frame.
|
|
23
|
+
# @parameter parent [Node] The parent node in the tree.
|
|
24
|
+
def initialize(location = nil, parent = nil)
|
|
25
|
+
@location = location
|
|
26
|
+
@parent = parent
|
|
27
|
+
@total_count = 0 # Total allocations (never decrements)
|
|
28
|
+
@retained_count = 0 # Current live objects (decrements on free)
|
|
29
|
+
@children = nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @attribute [Thread::Backtrace::Location] The location of the call.
|
|
33
|
+
attr_reader :location, :parent, :children
|
|
34
|
+
attr_accessor :total_count, :retained_count
|
|
35
|
+
|
|
36
|
+
# Increment both total and retained counts up the entire path to root.
|
|
37
|
+
def increment_path!
|
|
38
|
+
current = self
|
|
39
|
+
while current
|
|
40
|
+
current.total_count += 1
|
|
41
|
+
current.retained_count += 1
|
|
42
|
+
current = current.parent
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Decrement retained count up the entire path to root.
|
|
47
|
+
def decrement_path!
|
|
48
|
+
current = self
|
|
49
|
+
while current
|
|
50
|
+
current.retained_count -= 1
|
|
51
|
+
current = current.parent
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Check if this node is a leaf (end of a call path).
|
|
56
|
+
#
|
|
57
|
+
# @returns [Boolean] True if this node has no children.
|
|
58
|
+
def leaf?
|
|
59
|
+
@children.nil?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Find or create a child node for the given location.
|
|
63
|
+
#
|
|
64
|
+
# @parameter location [Thread::Backtrace::Location] The frame location for the child node.
|
|
65
|
+
# @returns [Node] The child node for this location.
|
|
66
|
+
def find_or_create_child(location)
|
|
67
|
+
@children ||= {}
|
|
68
|
+
@children[location.to_s] ||= Node.new(location, self)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Iterate over child nodes.
|
|
72
|
+
#
|
|
73
|
+
# @yields {|child| ...} If a block is given, yields each child node.
|
|
74
|
+
def each_child(&block)
|
|
75
|
+
@children&.each_value(&block)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Enumerate all paths from this node to leaves with their counts
|
|
79
|
+
#
|
|
80
|
+
# @parameter prefix [Array] The path prefix (nodes traversed so far).
|
|
81
|
+
# @yields {|path, total_count, retained_count| ...} For each leaf path.
|
|
82
|
+
def each_path(prefix = [], &block)
|
|
83
|
+
current = prefix + [self]
|
|
84
|
+
|
|
85
|
+
if leaf?
|
|
86
|
+
yield current, @total_count, @retained_count
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
@children&.each_value do |child|
|
|
90
|
+
child.each_path(current, &block)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Create a new call tree for tracking allocation paths.
|
|
96
|
+
def initialize
|
|
97
|
+
@root = Node.new
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Record an allocation with the given caller locations.
|
|
101
|
+
#
|
|
102
|
+
# @parameter caller_locations [Array<Thread::Backtrace::Location>] The call stack.
|
|
103
|
+
# @returns [Node] The leaf node representing this allocation path.
|
|
104
|
+
def record(caller_locations)
|
|
105
|
+
return nil if caller_locations.empty?
|
|
106
|
+
|
|
107
|
+
current = @root
|
|
108
|
+
|
|
109
|
+
# Build tree path from root to leaf:
|
|
110
|
+
caller_locations.each do |location|
|
|
111
|
+
current = current.find_or_create_child(location)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Increment counts for entire path (from leaf back to root):
|
|
115
|
+
current.increment_path!
|
|
116
|
+
|
|
117
|
+
# Return leaf node for object tracking:
|
|
118
|
+
current
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Get the top N call paths by allocation count.
|
|
122
|
+
#
|
|
123
|
+
# @parameter limit [Integer] Maximum number of paths to return.
|
|
124
|
+
# @parameter by [Symbol] Sort by :total or :retained count.
|
|
125
|
+
# @returns [Array<Array>] Array of [locations, total_count, retained_count].
|
|
126
|
+
def top_paths(limit = 10, by: :retained)
|
|
127
|
+
paths = []
|
|
128
|
+
|
|
129
|
+
@root.each_path do |path, total_count, retained_count|
|
|
130
|
+
# Filter out root node (has nil location) and map to location strings
|
|
131
|
+
locations = path.select(&:location).map {|node| node.location.to_s}
|
|
132
|
+
paths << [locations, total_count, retained_count] unless locations.empty?
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Sort by the requested metric (default: retained, since that's what matters for leaks)
|
|
136
|
+
sort_index = (by == :total) ? 1 : 2
|
|
137
|
+
paths.sort_by {|path_data| -path_data[sort_index]}.first(limit)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Get hotspot locations (individual frames with highest counts).
|
|
141
|
+
#
|
|
142
|
+
# @parameter limit [Integer] Maximum number of hotspots to return.
|
|
143
|
+
# @parameter by [Symbol] Sort by :total or :retained count.
|
|
144
|
+
# @returns [Hash] Map of location => [total_count, retained_count].
|
|
145
|
+
def hotspots(limit = 20, by: :retained)
|
|
146
|
+
frames = Hash.new {|h, k| h[k] = [0, 0]}
|
|
147
|
+
|
|
148
|
+
collect_frames(@root, frames)
|
|
149
|
+
|
|
150
|
+
# Sort by the requested metric
|
|
151
|
+
sort_index = (by == :total) ? 0 : 1
|
|
152
|
+
frames.sort_by {|_, counts| -counts[sort_index]}.first(limit).to_h
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Total number of allocations tracked.
|
|
156
|
+
#
|
|
157
|
+
# @returns [Integer] Total allocation count.
|
|
158
|
+
def total_allocations
|
|
159
|
+
@root.total_count
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Number of currently retained (live) allocations.
|
|
163
|
+
#
|
|
164
|
+
# @returns [Integer] Retained allocation count.
|
|
165
|
+
def retained_allocations
|
|
166
|
+
@root.retained_count
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Clear all tracking data
|
|
170
|
+
def clear!
|
|
171
|
+
@root = Node.new
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
private
|
|
175
|
+
|
|
176
|
+
def collect_frames(node, frames)
|
|
177
|
+
# Skip root node (has no location)
|
|
178
|
+
if node.location
|
|
179
|
+
location_str = node.location.to_s
|
|
180
|
+
frames[location_str][0] += node.total_count
|
|
181
|
+
frames[location_str][1] += node.retained_count
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
node.each_child {|child| collect_frames(child, frames)}
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "capture"
|
|
7
|
+
require_relative "call_tree"
|
|
8
|
+
|
|
9
|
+
module Memory
|
|
10
|
+
module Profiler
|
|
11
|
+
# Periodic sampler for monitoring memory growth over time.
|
|
12
|
+
#
|
|
13
|
+
# Samples class allocation counts at regular intervals and detects potential memory leaks
|
|
14
|
+
# by tracking when counts increase beyond a threshold. When a class exceeds the increases
|
|
15
|
+
# threshold, automatically enables detailed call path tracking for diagnosis.
|
|
16
|
+
class Sampler
|
|
17
|
+
# Tracks memory growth for a specific class.
|
|
18
|
+
#
|
|
19
|
+
# Records allocation counts over time and detects sustained growth patterns
|
|
20
|
+
# that indicate potential memory leaks.
|
|
21
|
+
class Sample
|
|
22
|
+
# Create a new sample profiler for a class.
|
|
23
|
+
#
|
|
24
|
+
# @parameter target [Class] The class being sampled.
|
|
25
|
+
# @parameter size [Integer] Initial object count.
|
|
26
|
+
# @parameter threshold [Integer] Minimum increase to consider significant.
|
|
27
|
+
def initialize(target, size = 0, threshold: 1000)
|
|
28
|
+
@target = target
|
|
29
|
+
@current_size = size
|
|
30
|
+
@maximum_observed_size = size
|
|
31
|
+
@threshold = threshold
|
|
32
|
+
|
|
33
|
+
@sample_count = 0
|
|
34
|
+
@increases = 0
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
attr_reader :target, :current_size, :maximum_observed_size, :threshold, :sample_count, :increases
|
|
38
|
+
|
|
39
|
+
# Record a new sample measurement.
|
|
40
|
+
#
|
|
41
|
+
# @parameter size [Integer] Current object count for this class.
|
|
42
|
+
# @returns [Boolean] True if count increased significantly.
|
|
43
|
+
def sample!(size)
|
|
44
|
+
@sample_count += 1
|
|
45
|
+
@current_size = size
|
|
46
|
+
|
|
47
|
+
# @maximum_observed_count ratchets up in units of at least @threshold counts.
|
|
48
|
+
# When it does, we bump @increases to track a potential memory leak.
|
|
49
|
+
if @maximum_observed_size
|
|
50
|
+
delta = @current_size - @maximum_observed_size
|
|
51
|
+
if delta > @threshold
|
|
52
|
+
@maximum_observed_size = size
|
|
53
|
+
@increases += 1
|
|
54
|
+
|
|
55
|
+
return true
|
|
56
|
+
end
|
|
57
|
+
else
|
|
58
|
+
@maximum_observed_size = size
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
return false
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Convert sample data to JSON-compatible hash.
|
|
65
|
+
#
|
|
66
|
+
# @returns [Hash] Sample data as a hash.
|
|
67
|
+
def as_json(...)
|
|
68
|
+
{
|
|
69
|
+
target: @target.name || "(anonymous class)",
|
|
70
|
+
current_size: @current_size,
|
|
71
|
+
maximum_observed_size: @maximum_observed_size,
|
|
72
|
+
increases: @increases,
|
|
73
|
+
sample_count: @sample_count,
|
|
74
|
+
threshold: @threshold,
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Convert sample data to JSON string.
|
|
79
|
+
#
|
|
80
|
+
# @returns [String] Sample data as JSON.
|
|
81
|
+
def to_json(...)
|
|
82
|
+
as_json.to_json(...)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
attr_reader :depth
|
|
87
|
+
|
|
88
|
+
# Create a new memory sampler.
|
|
89
|
+
#
|
|
90
|
+
# @parameter depth [Integer] Number of stack frames to capture for call path analysis.
|
|
91
|
+
# @parameter filter [Proc] Optional filter to exclude frames from call paths.
|
|
92
|
+
# @parameter increases_threshold [Integer] Number of increases before enabling detailed tracking.
|
|
93
|
+
def initialize(depth: 10, filter: nil, increases_threshold: 10)
|
|
94
|
+
@depth = depth
|
|
95
|
+
@filter = filter || default_filter
|
|
96
|
+
@increases_threshold = increases_threshold
|
|
97
|
+
@capture = Capture.new
|
|
98
|
+
@call_trees = {}
|
|
99
|
+
@samples = {}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Start capturing allocations.
|
|
103
|
+
def start
|
|
104
|
+
@capture.start
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Stop capturing allocations.
|
|
108
|
+
def stop
|
|
109
|
+
@capture.stop
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Run periodic sampling in a loop.
|
|
113
|
+
#
|
|
114
|
+
# Samples allocation counts at the specified interval and reports when
|
|
115
|
+
# classes show sustained memory growth. Automatically tracks ALL classes
|
|
116
|
+
# that allocate objects - no need to specify them upfront.
|
|
117
|
+
#
|
|
118
|
+
# @parameter interval [Numeric] Seconds between samples.
|
|
119
|
+
# @yields {|sample| ...} Called when a class shows significant growth.
|
|
120
|
+
def run(interval: 60, &block)
|
|
121
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
122
|
+
|
|
123
|
+
while true
|
|
124
|
+
sample!(&block)
|
|
125
|
+
|
|
126
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
127
|
+
delta = interval - (now - start_time)
|
|
128
|
+
sleep(delta) if delta > 0
|
|
129
|
+
start_time = now
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Take a single sample of memory usage for all tracked classes.
|
|
134
|
+
#
|
|
135
|
+
# @yields {|sample| ...} Called when a class shows significant growth.
|
|
136
|
+
def sample!
|
|
137
|
+
@capture.each do |klass, allocations|
|
|
138
|
+
count = allocations.retained_count
|
|
139
|
+
sample = @samples[klass] ||= Sample.new(klass, count)
|
|
140
|
+
|
|
141
|
+
if sample.sample!(count)
|
|
142
|
+
# Check if we should enable detailed tracking
|
|
143
|
+
if sample.increases >= @increases_threshold && !@call_trees.key?(klass)
|
|
144
|
+
track_with_analysis_internal(klass, allocations)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Notify about growth if block given
|
|
148
|
+
yield sample if block_given?
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Internal: Enable tracking with analysis using allocations object
|
|
154
|
+
private def track_with_analysis_internal(klass, allocations)
|
|
155
|
+
tree = @call_trees[klass] = CallTree.new
|
|
156
|
+
depth = @depth
|
|
157
|
+
filter = @filter
|
|
158
|
+
|
|
159
|
+
# Register callback on allocations object with new signature:
|
|
160
|
+
# - On :newobj - returns state (leaf node) which C extension stores
|
|
161
|
+
# - On :freeobj - receives state back from C extension
|
|
162
|
+
allocations.track do |klass, event, state|
|
|
163
|
+
case event
|
|
164
|
+
when :newobj
|
|
165
|
+
# Capture call stack and record in tree
|
|
166
|
+
locations = caller_locations(1, depth)
|
|
167
|
+
filtered = locations.select(&filter)
|
|
168
|
+
unless filtered.empty?
|
|
169
|
+
# Record returns the leaf node - return it so C can store it
|
|
170
|
+
tree.record(filtered)
|
|
171
|
+
end
|
|
172
|
+
# Return nil or the node - C will store whatever we return
|
|
173
|
+
when :freeobj
|
|
174
|
+
# Decrement using the state (leaf node) passed back from C
|
|
175
|
+
if state
|
|
176
|
+
state.decrement_path!
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
rescue Exception => error
|
|
180
|
+
warn "Error in track_with_analysis_internal: #{error.message}\n#{error.backtrace.join("\n")}"
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Start tracking allocations for a class (count only).
|
|
185
|
+
def track(klass)
|
|
186
|
+
return if @capture.tracking?(klass)
|
|
187
|
+
|
|
188
|
+
@capture.track(klass)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Start tracking with call path analysis.
|
|
192
|
+
#
|
|
193
|
+
# @parameter klass [Class] The class to track with detailed analysis.
|
|
194
|
+
def track_with_analysis(klass)
|
|
195
|
+
# Track the class if not already tracked
|
|
196
|
+
unless @capture.tracking?(klass)
|
|
197
|
+
@capture.track(klass)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Enable analysis by setting callback on the allocations object
|
|
201
|
+
@capture.each do |tracked_klass, allocations|
|
|
202
|
+
if tracked_klass == klass
|
|
203
|
+
track_with_analysis_internal(klass, allocations)
|
|
204
|
+
break
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Stop tracking a specific class.
|
|
210
|
+
def untrack(klass)
|
|
211
|
+
@capture.untrack(klass)
|
|
212
|
+
@call_trees.delete(klass)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Check if a class is being tracked.
|
|
216
|
+
def tracking?(klass)
|
|
217
|
+
@capture.tracking?(klass)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Get live object count for a class.
|
|
221
|
+
def count(klass)
|
|
222
|
+
@capture.count_for(klass)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Get the call tree for a specific class.
|
|
226
|
+
def call_tree(klass)
|
|
227
|
+
@call_trees[klass]
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Get allocation statistics for a tracked class.
|
|
231
|
+
#
|
|
232
|
+
# @parameter klass [Class] The class to get statistics for.
|
|
233
|
+
# @returns [Hash] Statistics including total, retained, paths, and hotspots.
|
|
234
|
+
def statistics(klass)
|
|
235
|
+
tree = @call_trees[klass]
|
|
236
|
+
return nil unless tree
|
|
237
|
+
|
|
238
|
+
{
|
|
239
|
+
live_count: @capture.count_for(klass),
|
|
240
|
+
total_allocations: tree.total_allocations,
|
|
241
|
+
retained_allocations: tree.retained_allocations,
|
|
242
|
+
top_paths: tree.top_paths(10).map {|path, total, retained|
|
|
243
|
+
{ path: path, total_count: total, retained_count: retained }
|
|
244
|
+
},
|
|
245
|
+
hotspots: tree.hotspots(20).transform_values {|total, retained|
|
|
246
|
+
{ total_count: total, retained_count: retained }
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Get statistics for all tracked classes.
|
|
252
|
+
def all_statistics
|
|
253
|
+
@call_trees.keys.each_with_object({}) do |klass, result|
|
|
254
|
+
result[klass] = statistics(klass) if tracking?(klass)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Clear tracking data for a class.
|
|
259
|
+
def clear(klass)
|
|
260
|
+
tree = @call_trees[klass]
|
|
261
|
+
tree&.clear!
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Clear all tracking data.
|
|
265
|
+
def clear_all!
|
|
266
|
+
@call_trees.each_value(&:clear!)
|
|
267
|
+
@capture.clear
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Stop all tracking and clean up.
|
|
271
|
+
def stop!
|
|
272
|
+
@capture.stop
|
|
273
|
+
@call_trees.each_key do |klass|
|
|
274
|
+
@capture.untrack(klass)
|
|
275
|
+
end
|
|
276
|
+
@capture.clear
|
|
277
|
+
@call_trees.clear
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
private
|
|
281
|
+
|
|
282
|
+
def default_filter
|
|
283
|
+
->(location) {path = location.path
|
|
284
|
+
!path.include?("/gems/") &&
|
|
285
|
+
!path.include?("/ruby/") &&
|
|
286
|
+
!path.start_with?("(eval)")
|
|
287
|
+
}
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "profiler/version"
|
|
7
|
+
require_relative "profiler/call_tree"
|
|
8
|
+
require_relative "profiler/capture"
|
|
9
|
+
require_relative "profiler/sampler"
|
data/license.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# MIT License
|
|
2
|
+
|
|
3
|
+
Copyright, 2025, by Samuel Williams.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/readme.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Memory::Profiler
|
|
2
|
+
|
|
3
|
+
Efficient memory allocation tracking focused on **retained objects only**. Automatically tracks allocations and cleans up when objects are freed, giving you precise data on memory leaks.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/socketry/memory-profiler/actions?workflow=Test)
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Retained Objects Only**: Uses `RUBY_INTERNAL_EVENT_NEWOBJ` and `RUBY_INTERNAL_EVENT_FREEOBJ` to automatically track only objects that survive GC.
|
|
10
|
+
- **O(1) Live Counts**: Maintains per-class counters updated on alloc/free - no heap enumeration needed\!
|
|
11
|
+
- **Tree-Based Analysis**: Deduplicates common call paths using an efficient tree structure.
|
|
12
|
+
- **Native C Extension**: **Required** - uses Ruby internal events not available in pure Ruby.
|
|
13
|
+
- **Configurable Depth**: Control how deep to capture call stacks.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
Please see the [project documentation](https://socketry.github.io/memory-profiler/) for more details.
|
|
18
|
+
|
|
19
|
+
- [Getting Started](https://socketry.github.io/memory-profiler/guides/getting-started/index) - This guide explains how to use `memory-profiler` to detect and diagnose memory leaks in Ruby applications.
|
|
20
|
+
|
|
21
|
+
## Releases
|
|
22
|
+
|
|
23
|
+
Please see the [project releases](https://socketry.github.io/memory-profiler/releases/index) for all releases.
|
|
24
|
+
|
|
25
|
+
### v0.1.0
|
|
26
|
+
|
|
27
|
+
- Initial implementation.
|
|
28
|
+
|
|
29
|
+
## Contributing
|
|
30
|
+
|
|
31
|
+
We welcome contributions to this project.
|
|
32
|
+
|
|
33
|
+
1. Fork it.
|
|
34
|
+
2. Create your feature branch (`git checkout -b my-new-feature`).
|
|
35
|
+
3. Commit your changes (`git commit -am 'Add some feature'`).
|
|
36
|
+
4. Push to the branch (`git push origin my-new-feature`).
|
|
37
|
+
5. Create new Pull Request.
|
|
38
|
+
|
|
39
|
+
### Developer Certificate of Origin
|
|
40
|
+
|
|
41
|
+
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.
|
|
42
|
+
|
|
43
|
+
### Community Guidelines
|
|
44
|
+
|
|
45
|
+
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
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Ps#(�����{L��+n]-Sd,�9�u"��@.��~�+"��<�S��ύ���v�{\��Wf�;V%UK�Ϗe����.v���Py�k���xcF �(zZb�e�Lnd-Z�����2�6V���:=��ܑM\�~Kp��wGQa���l�1�F*l�`��A/��j���r|���p�wD��0�n�_��H�{�7]����{^c�a�)}�9å�J����a@o&��oi�k��d�����&sI�t���k�i�U�R�-�,�X�3���R��勖8����#�s:�����I�QٟM�ݼ�3=զy8~I%����M�e�G�KH��iH�`�����=ZnV#��>�\엝�YCu����wOv#�f!�rgmz/�K�
|