memory 0.9.0 → 0.11.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/lib/memory/graph.rb +217 -0
- data/lib/memory/usage.rb +28 -15
- data/lib/memory/version.rb +1 -1
- data/lib/memory.rb +2 -0
- data/readme.md +9 -4
- data/releases.md +9 -0
- data.tar.gz.sig +0 -0
- metadata +2 -1
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 91e6bf9d1a607a68dd5d8f368d6dfe4a4319bf5730b6283194f99fb587b96364
|
|
4
|
+
data.tar.gz: 9a4335bfa899416c7fec144236dc5e711382f49f5631a4b0dbde03d73e84a58e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0a704a533f5ac299f5b0da35814c7d93d13dc9881fda6c69c8f274a9331dedf3f9b57e1afa22b28888892092f2daf9a0c583259fbcbf26757d2f3c3805594caf
|
|
7
|
+
data.tar.gz: cc67f1e0781d93b648ed720efb1a4ce6d05c5d18c15e2b781ecbb00321fec08c674d2be2a051f1835bf9de1f2e1a90c5ee6df4be72ac46d0e4ba6f50ec48f5f5
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/lib/memory/graph.rb
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "usage"
|
|
7
|
+
require "json"
|
|
8
|
+
|
|
9
|
+
module Memory
|
|
10
|
+
# Tracks object traversal paths for memory analysis.
|
|
11
|
+
module Graph
|
|
12
|
+
IGNORE = Usage::IGNORE
|
|
13
|
+
|
|
14
|
+
# Represents a node in the object graph with usage information.
|
|
15
|
+
class Node
|
|
16
|
+
def initialize(object, usage = Usage.new, parent = nil, reference: nil)
|
|
17
|
+
@object = object
|
|
18
|
+
@usage = usage
|
|
19
|
+
@parent = parent
|
|
20
|
+
@children = nil
|
|
21
|
+
|
|
22
|
+
@reference = reference || parent&.find_reference(object)
|
|
23
|
+
@total_usage = nil
|
|
24
|
+
@path = nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @attribute [Object] The object this node represents.
|
|
28
|
+
attr_accessor :object
|
|
29
|
+
|
|
30
|
+
# @attribute [Usage] The memory usage of this object (not including children).
|
|
31
|
+
attr_accessor :usage
|
|
32
|
+
|
|
33
|
+
# @attribute [Node | Nil] The parent node (nil for root).
|
|
34
|
+
attr_accessor :parent
|
|
35
|
+
|
|
36
|
+
# @attribute [Hash(String, Node) | Nil] Child nodes reachable from this object (hash of reference => node).
|
|
37
|
+
attr_accessor :children
|
|
38
|
+
|
|
39
|
+
# @attribute [String | Nil] The reference to the parent object (nil for root).
|
|
40
|
+
attr_accessor :reference
|
|
41
|
+
|
|
42
|
+
# Add a child node to this node.
|
|
43
|
+
#
|
|
44
|
+
# @parameter child [Node] The child node to add.
|
|
45
|
+
# @returns [self] Returns self for chaining.
|
|
46
|
+
def add(child)
|
|
47
|
+
@children ||= {}
|
|
48
|
+
|
|
49
|
+
# Use the reference as the key, or a fallback if not found:
|
|
50
|
+
key = child.reference || "(#{@children.size})"
|
|
51
|
+
|
|
52
|
+
@children[key] = child
|
|
53
|
+
|
|
54
|
+
return self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Compute total usage including all children.
|
|
58
|
+
def total_usage
|
|
59
|
+
unless @total_usage
|
|
60
|
+
@total_usage = Usage.new(@usage.size, @usage.count)
|
|
61
|
+
|
|
62
|
+
@children&.each_value do |child|
|
|
63
|
+
child_total = child.total_usage
|
|
64
|
+
@total_usage.add!(child_total)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
return @total_usage
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Find how this node references a child object.
|
|
72
|
+
#
|
|
73
|
+
# @parameter child [Object] The child object to find.
|
|
74
|
+
# @returns [String | Nil] A human-readable description of the reference, or nil if not found.
|
|
75
|
+
def find_reference(child)
|
|
76
|
+
# Check instance variables:
|
|
77
|
+
@object.instance_variables.each do |ivar|
|
|
78
|
+
value = @object.instance_variable_get(ivar)
|
|
79
|
+
if value.equal?(child)
|
|
80
|
+
return ivar.to_s
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Check array elements:
|
|
85
|
+
if @object.is_a?(Array)
|
|
86
|
+
@object.each_with_index do |element, index|
|
|
87
|
+
if element.equal?(child)
|
|
88
|
+
return "[#{index}]"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Check hash keys and values:
|
|
94
|
+
if @object.is_a?(Hash)
|
|
95
|
+
@object.each do |key, value|
|
|
96
|
+
if value.equal?(child)
|
|
97
|
+
return "[#{key.inspect}]"
|
|
98
|
+
end
|
|
99
|
+
if key.equal?(child)
|
|
100
|
+
return "(key: #{key.inspect})"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Check struct members:
|
|
106
|
+
if @object.is_a?(Struct)
|
|
107
|
+
@object.each_pair do |member, value|
|
|
108
|
+
if value.equal?(child)
|
|
109
|
+
return ".#{member}"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Could not determine the reference:
|
|
115
|
+
return nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Get the path string from root to this node (cached).
|
|
119
|
+
#
|
|
120
|
+
# @returns [String | Nil] The formatted path string, or nil if no graph available.
|
|
121
|
+
def path
|
|
122
|
+
unless @path
|
|
123
|
+
# Build object path from root to this node:
|
|
124
|
+
object_path = []
|
|
125
|
+
current = self
|
|
126
|
+
|
|
127
|
+
while current
|
|
128
|
+
object_path.unshift(current)
|
|
129
|
+
current = current.parent
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Format the path:
|
|
133
|
+
parts = ["#<#{object_path.first.object.class}:0x%016x>" % (object_path.first.object.object_id << 1)]
|
|
134
|
+
|
|
135
|
+
# Append each reference in the path:
|
|
136
|
+
(1...object_path.size).each do |i|
|
|
137
|
+
parent_node = object_path[i - 1]
|
|
138
|
+
child_node = object_path[i]
|
|
139
|
+
|
|
140
|
+
parts << (parent_node.find_reference(child_node.object) || "<??>")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
@path = parts.join
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
return @path
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Convert this node to a JSON-compatible hash.
|
|
150
|
+
#
|
|
151
|
+
# @parameter options [Hash] Options for JSON serialization.
|
|
152
|
+
# @returns [Hash] A hash representation of this node.
|
|
153
|
+
def as_json(*)
|
|
154
|
+
{
|
|
155
|
+
path: path,
|
|
156
|
+
object: {
|
|
157
|
+
class: @object.class.name,
|
|
158
|
+
object_id: @object.object_id
|
|
159
|
+
},
|
|
160
|
+
usage: @usage.as_json,
|
|
161
|
+
total_usage: total_usage.as_json,
|
|
162
|
+
children: @children&.transform_values(&:as_json)
|
|
163
|
+
}
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Convert this node to a JSON string.
|
|
167
|
+
#
|
|
168
|
+
# @parameter options [Hash] Options for JSON serialization.
|
|
169
|
+
# @returns [String] A JSON string representation of this node.
|
|
170
|
+
def to_json(...)
|
|
171
|
+
as_json.to_json(...)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Build a graph of nodes from a root object, computing usage at each level.
|
|
176
|
+
#
|
|
177
|
+
# @parameter root [Object] The root object to start from.
|
|
178
|
+
# @parameter depth [Integer] Maximum depth to traverse (nil for unlimited).
|
|
179
|
+
# @parameter seen [Set] Set of already seen objects (for internal use).
|
|
180
|
+
# @parameter ignore [Array] Array of types to ignore during traversal.
|
|
181
|
+
# @parameter parent [Node | Nil] The parent node (for internal use).
|
|
182
|
+
# @returns [Node] The root node with children populated.
|
|
183
|
+
def self.for(root, depth: nil, seen: Set.new.compare_by_identity, ignore: IGNORE, parent: nil)
|
|
184
|
+
if depth && depth <= 0
|
|
185
|
+
# Compute shallow usage for this object and it's children:
|
|
186
|
+
usage = Usage.of(root, seen: seen, ignore: ignore)
|
|
187
|
+
return Node.new(root, usage, parent)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Compute shallow usage for just this object:
|
|
191
|
+
usage = Usage.new(ObjectSpace.memsize_of(root), 1)
|
|
192
|
+
|
|
193
|
+
# Create the node:
|
|
194
|
+
node = Node.new(root, usage, parent)
|
|
195
|
+
|
|
196
|
+
# Mark this object as seen:
|
|
197
|
+
seen.add(root)
|
|
198
|
+
|
|
199
|
+
# Traverse children:
|
|
200
|
+
ObjectSpace.reachable_objects_from(root)&.each do |reachable_object|
|
|
201
|
+
# Skip ignored types:
|
|
202
|
+
next if ignore.any?{|type| reachable_object.is_a?(type)}
|
|
203
|
+
|
|
204
|
+
# Skip internal objects:
|
|
205
|
+
next if reachable_object.is_a?(ObjectSpace::InternalObjectWrapper)
|
|
206
|
+
|
|
207
|
+
# Skip already seen objects:
|
|
208
|
+
next if seen.include?(reachable_object)
|
|
209
|
+
|
|
210
|
+
# Recursively build child node:
|
|
211
|
+
node.add(self.for(reachable_object, depth: depth ? depth - 1 : nil, seen: seen, ignore: ignore, parent: node))
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
return node
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
data/lib/memory/usage.rb
CHANGED
|
@@ -39,6 +39,15 @@ module Memory
|
|
|
39
39
|
return self
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# Add another usage to this usage.
|
|
43
|
+
# @parameter other [Usage] The usage to add.
|
|
44
|
+
def add!(other)
|
|
45
|
+
self.size += other.size
|
|
46
|
+
self.count += other.count
|
|
47
|
+
|
|
48
|
+
return self
|
|
49
|
+
end
|
|
50
|
+
|
|
42
51
|
IGNORE = [
|
|
43
52
|
# Skip modules and symbols, they are usually "global":
|
|
44
53
|
Module,
|
|
@@ -57,33 +66,37 @@ module Memory
|
|
|
57
66
|
]
|
|
58
67
|
|
|
59
68
|
# Compute the usage of an object and all reachable objects from it.
|
|
69
|
+
#
|
|
70
|
+
# The root is always visited even if it is in `seen`.
|
|
71
|
+
#
|
|
60
72
|
# @parameter root [Object] The root object to start traversal from.
|
|
73
|
+
# @parameter seen [Hash(Object, Integer)] The seen objects (should be compare_by_identity).
|
|
61
74
|
# @returns [Usage] The usage of the object and all reachable objects from it.
|
|
62
75
|
def self.of(root, seen: Set.new.compare_by_identity, ignore: IGNORE)
|
|
63
76
|
count = 0
|
|
64
77
|
size = 0
|
|
65
78
|
|
|
66
79
|
queue = [root]
|
|
67
|
-
while queue.
|
|
68
|
-
object
|
|
69
|
-
|
|
70
|
-
# Skip ignored types:
|
|
71
|
-
next if ignore.any?{|type| object.is_a?(type)}
|
|
72
|
-
|
|
73
|
-
# Skip internal objects - they don't behave correctly when added to `seen` and create unbounded recursion:
|
|
74
|
-
next if object.is_a?(ObjectSpace::InternalObjectWrapper)
|
|
75
|
-
|
|
76
|
-
# Skip objects we have already seen:
|
|
77
|
-
next if seen.include?(object)
|
|
78
|
-
|
|
79
|
-
# Add the object to the seen set and update the count and size:
|
|
80
|
+
while object = queue.shift
|
|
81
|
+
# Add the object to the seen set:
|
|
80
82
|
seen.add(object)
|
|
83
|
+
|
|
84
|
+
# Update the count and size:
|
|
81
85
|
count += 1
|
|
82
86
|
size += ObjectSpace.memsize_of(object)
|
|
83
87
|
|
|
84
88
|
# Add the object's reachable objects to the queue:
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
ObjectSpace.reachable_objects_from(object)&.each do |reachable_object|
|
|
90
|
+
# Skip ignored types:
|
|
91
|
+
next if ignore.any?{|type| reachable_object.is_a?(type)}
|
|
92
|
+
|
|
93
|
+
# Skip internal objects - they don't behave correctly when added to `seen` and create unbounded recursion:
|
|
94
|
+
next if reachable_object.is_a?(ObjectSpace::InternalObjectWrapper)
|
|
95
|
+
|
|
96
|
+
# Skip objects we have already seen:
|
|
97
|
+
next if seen.include?(reachable_object)
|
|
98
|
+
|
|
99
|
+
queue << reachable_object
|
|
87
100
|
end
|
|
88
101
|
end
|
|
89
102
|
|
data/lib/memory/version.rb
CHANGED
data/lib/memory.rb
CHANGED
|
@@ -11,6 +11,8 @@ require_relative "memory/version"
|
|
|
11
11
|
require_relative "memory/cache"
|
|
12
12
|
require_relative "memory/report"
|
|
13
13
|
require_relative "memory/sampler"
|
|
14
|
+
require_relative "memory/usage"
|
|
15
|
+
require_relative "memory/graph"
|
|
14
16
|
|
|
15
17
|
# Memory profiler for Ruby applications.
|
|
16
18
|
# Provides tools to track and analyze memory allocations and retention.
|
data/readme.md
CHANGED
|
@@ -94,6 +94,15 @@ end
|
|
|
94
94
|
|
|
95
95
|
Please see the [project releases](https://socketry.github.io/memory/releases/index) for all releases.
|
|
96
96
|
|
|
97
|
+
### v0.11.0
|
|
98
|
+
|
|
99
|
+
- Remove support for `Memory::Usage.of(..., via:)` and instead use `Memory::Graph.for` which collects more detailed usage until the specified depth, at which point it delgates to `Memory::Usage.of`. This should be more practical.
|
|
100
|
+
|
|
101
|
+
### v0.10.0
|
|
102
|
+
|
|
103
|
+
- Add support for `Memory::Usage.of(..., via:)` for tracking reachability of objects.
|
|
104
|
+
- Introduce `Memory::Graph` for computing paths between parent/child objects.
|
|
105
|
+
|
|
97
106
|
### v0.9.0
|
|
98
107
|
|
|
99
108
|
- Explicit `ignore:` and `seen:` parameters for `Memory::Usage.of` to allow customization of ignored types and tracking of seen objects.
|
|
@@ -127,10 +136,6 @@ Please see the [project releases](https://socketry.github.io/memory/releases/ind
|
|
|
127
136
|
|
|
128
137
|
- Add `Memory::Sampler#as_json` and `#to_json`.
|
|
129
138
|
|
|
130
|
-
### v0.6.0
|
|
131
|
-
|
|
132
|
-
- Add agent context.
|
|
133
|
-
|
|
134
139
|
## Contributing
|
|
135
140
|
|
|
136
141
|
We welcome contributions to this project.
|
data/releases.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.11.0
|
|
4
|
+
|
|
5
|
+
- Remove support for `Memory::Usage.of(..., via:)` and instead use `Memory::Graph.for` which collects more detailed usage until the specified depth, at which point it delgates to `Memory::Usage.of`. This should be more practical.
|
|
6
|
+
|
|
7
|
+
## v0.10.0
|
|
8
|
+
|
|
9
|
+
- Add support for `Memory::Usage.of(..., via:)` for tracking reachability of objects.
|
|
10
|
+
- Introduce `Memory::Graph` for computing paths between parent/child objects.
|
|
11
|
+
|
|
3
12
|
## v0.9.0
|
|
4
13
|
|
|
5
14
|
- Explicit `ignore:` and `seen:` parameters for `Memory::Usage.of` to allow customization of ignored types and tracking of seen objects.
|
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.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sam Saffron
|
|
@@ -117,6 +117,7 @@ files:
|
|
|
117
117
|
- lib/memory/cache.rb
|
|
118
118
|
- lib/memory/deque.rb
|
|
119
119
|
- lib/memory/format.rb
|
|
120
|
+
- lib/memory/graph.rb
|
|
120
121
|
- lib/memory/report.rb
|
|
121
122
|
- lib/memory/sampler.rb
|
|
122
123
|
- lib/memory/usage.rb
|
metadata.gz.sig
CHANGED
|
Binary file
|