memory 0.10.0 → 0.11.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 +0 -0
- data/lib/memory/graph.rb +185 -104
- data/lib/memory/usage.rb +10 -6
- data/lib/memory/version.rb +1 -1
- data/readme.md +8 -8
- data/releases.md +8 -0
- data.tar.gz.sig +0 -0
- metadata +1 -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: 2ba9aa93caef68d159caf09cc6d68aefb4c145462ef256ddcbcc81d80eb4769e
|
|
4
|
+
data.tar.gz: f78095dd0562b782ab8ef36e7d4be2ae445f9efd308b3b81496369d5fb54537a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9340e0232dffc8aeb0dbf1dc03333c5cf00978bb76e046bd83102a8da51843c08034a0a0531204cf187e7411d9491ed437140c45517ef68aa7869a117dc3e785
|
|
7
|
+
data.tar.gz: 4691a3776dbaf2e79cc673d46e6b6b6a1d048fd0126828f841a6d258f36d1aad95b950222cc9563ecb461598f3b0a2bc2ba185668ae89d3aea76aa388117fc3a
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/lib/memory/graph.rb
CHANGED
|
@@ -3,139 +3,220 @@
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
4
|
# Copyright, 2025, by Samuel Williams.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
require_relative "usage"
|
|
7
|
+
require "json"
|
|
7
8
|
|
|
8
9
|
module Memory
|
|
9
10
|
# Tracks object traversal paths for memory analysis.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
# allowing you to trace the reference path from any object back to its root.
|
|
13
|
-
class Graph
|
|
14
|
-
def initialize
|
|
15
|
-
@mapping = Hash.new.compare_by_identity
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# The internal mapping of objects to their parents.
|
|
19
|
-
attr_reader :mapping
|
|
11
|
+
module Graph
|
|
12
|
+
IGNORE = Usage::IGNORE
|
|
20
13
|
|
|
21
|
-
#
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
# @returns [Object | Nil] The parent object, or nil if not tracked.
|
|
33
|
-
def [](child)
|
|
34
|
-
@mapping[child]
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Check if an object is tracked in the via mapping.
|
|
38
|
-
#
|
|
39
|
-
# @parameter object [Object] The object to check.
|
|
40
|
-
# @returns [Boolean] True if the object is tracked.
|
|
41
|
-
def key?(object)
|
|
42
|
-
@mapping.key?(object)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Find how a parent object references a child object.
|
|
46
|
-
#
|
|
47
|
-
# @parameter parent [Object] The parent object.
|
|
48
|
-
# @parameter child [Object] The child object to find.
|
|
49
|
-
# @returns [String | Nil] A human-readable description of the reference, or nil if not found.
|
|
50
|
-
def find_reference(parent, child)
|
|
51
|
-
# Check instance variables:
|
|
52
|
-
parent.instance_variables.each do |ivar|
|
|
53
|
-
value = parent.instance_variable_get(ivar)
|
|
54
|
-
if value.equal?(child)
|
|
55
|
-
return ivar.to_s
|
|
56
|
-
end
|
|
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
|
|
57
25
|
end
|
|
58
26
|
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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)
|
|
64
65
|
end
|
|
65
66
|
end
|
|
67
|
+
|
|
68
|
+
return @total_usage
|
|
66
69
|
end
|
|
67
70
|
|
|
68
|
-
#
|
|
69
|
-
|
|
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)
|
|
71
79
|
if value.equal?(child)
|
|
72
|
-
return
|
|
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
|
|
73
90
|
end
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
76
111
|
end
|
|
77
112
|
end
|
|
113
|
+
|
|
114
|
+
# Could not determine the reference:
|
|
115
|
+
return nil
|
|
78
116
|
end
|
|
79
117
|
|
|
80
|
-
#
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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) || "<??>")
|
|
85
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
|
+
json = {
|
|
155
|
+
path: path,
|
|
156
|
+
object: {
|
|
157
|
+
class: @object.class.name,
|
|
158
|
+
object_id: @object.object_id
|
|
159
|
+
},
|
|
160
|
+
usage: @usage.as_json,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if @children&.any?
|
|
164
|
+
json[:total_usage] = total_usage.as_json
|
|
165
|
+
json[:children] = @children.transform_values(&:as_json)
|
|
86
166
|
end
|
|
167
|
+
|
|
168
|
+
return json
|
|
87
169
|
end
|
|
88
170
|
|
|
89
|
-
#
|
|
90
|
-
|
|
171
|
+
# Convert this node to a JSON string.
|
|
172
|
+
#
|
|
173
|
+
# @parameter options [Hash] Options for JSON serialization.
|
|
174
|
+
# @returns [String] A JSON string representation of this node.
|
|
175
|
+
def to_json(...)
|
|
176
|
+
as_json.to_json(...)
|
|
177
|
+
end
|
|
91
178
|
end
|
|
92
179
|
|
|
93
|
-
#
|
|
180
|
+
# Build a graph of nodes from a root object, computing usage at each level.
|
|
94
181
|
#
|
|
95
|
-
# @parameter
|
|
96
|
-
# @parameter
|
|
97
|
-
# @
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
current = parent
|
|
107
|
-
|
|
108
|
-
# Stop if we reached the specified root:
|
|
109
|
-
break if root && current.equal?(root)
|
|
182
|
+
# @parameter root [Object] The root object to start from.
|
|
183
|
+
# @parameter depth [Integer] Maximum depth to traverse (nil for unlimited).
|
|
184
|
+
# @parameter seen [Set] Set of already seen objects (for internal use).
|
|
185
|
+
# @parameter ignore [Array] Array of types to ignore during traversal.
|
|
186
|
+
# @parameter parent [Node | Nil] The parent node (for internal use).
|
|
187
|
+
# @returns [Node] The root node with children populated.
|
|
188
|
+
def self.for(root, depth: nil, seen: Set.new.compare_by_identity, ignore: IGNORE, parent: nil)
|
|
189
|
+
if depth && depth <= 0
|
|
190
|
+
# Compute shallow usage for this object and it's children:
|
|
191
|
+
usage = Usage.of(root, seen: seen, ignore: ignore)
|
|
192
|
+
return Node.new(root, usage, parent)
|
|
110
193
|
end
|
|
111
194
|
|
|
112
|
-
#
|
|
113
|
-
|
|
195
|
+
# Compute shallow usage for just this object:
|
|
196
|
+
usage = Usage.new(ObjectSpace.memsize_of(root), 1)
|
|
114
197
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
# Start with the root object description:
|
|
127
|
-
parts = ["#<#{object_path.first.class}:0x%016x>" % (object_path.first.object_id << 1)]
|
|
128
|
-
|
|
129
|
-
# Append each reference in the path:
|
|
130
|
-
(1...object_path.size).each do |i|
|
|
131
|
-
parent = object_path[i - 1]
|
|
132
|
-
child = object_path[i]
|
|
198
|
+
# Create the node:
|
|
199
|
+
node = Node.new(root, usage, parent)
|
|
200
|
+
|
|
201
|
+
# Mark this object as seen:
|
|
202
|
+
seen.add(root)
|
|
203
|
+
|
|
204
|
+
# Traverse children:
|
|
205
|
+
ObjectSpace.reachable_objects_from(root)&.each do |reachable_object|
|
|
206
|
+
# Skip ignored types:
|
|
207
|
+
next if ignore.any?{|type| reachable_object.is_a?(type)}
|
|
133
208
|
|
|
134
|
-
|
|
209
|
+
# Skip internal objects:
|
|
210
|
+
next if reachable_object.is_a?(ObjectSpace::InternalObjectWrapper)
|
|
211
|
+
|
|
212
|
+
# Skip already seen objects:
|
|
213
|
+
next if seen.include?(reachable_object)
|
|
214
|
+
|
|
215
|
+
# Recursively build child node:
|
|
216
|
+
node.add(self.for(reachable_object, depth: depth ? depth - 1 : nil, seen: seen, ignore: ignore, parent: node))
|
|
135
217
|
end
|
|
136
218
|
|
|
137
|
-
return
|
|
219
|
+
return node
|
|
138
220
|
end
|
|
139
221
|
end
|
|
140
222
|
end
|
|
141
|
-
|
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,
|
|
@@ -62,9 +71,8 @@ module Memory
|
|
|
62
71
|
#
|
|
63
72
|
# @parameter root [Object] The root object to start traversal from.
|
|
64
73
|
# @parameter seen [Hash(Object, Integer)] The seen objects (should be compare_by_identity).
|
|
65
|
-
# @parameter via [Hash(Object, Object) | Nil] The traversal path. The key object was seen via the value object.
|
|
66
74
|
# @returns [Usage] The usage of the object and all reachable objects from it.
|
|
67
|
-
def self.of(root, seen: Set.new.compare_by_identity, ignore: IGNORE
|
|
75
|
+
def self.of(root, seen: Set.new.compare_by_identity, ignore: IGNORE)
|
|
68
76
|
count = 0
|
|
69
77
|
size = 0
|
|
70
78
|
|
|
@@ -88,10 +96,6 @@ module Memory
|
|
|
88
96
|
# Skip objects we have already seen:
|
|
89
97
|
next if seen.include?(reachable_object)
|
|
90
98
|
|
|
91
|
-
if via
|
|
92
|
-
via[reachable_object] ||= object
|
|
93
|
-
end
|
|
94
|
-
|
|
95
99
|
queue << reachable_object
|
|
96
100
|
end
|
|
97
101
|
end
|
data/lib/memory/version.rb
CHANGED
data/readme.md
CHANGED
|
@@ -94,6 +94,14 @@ 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.1
|
|
98
|
+
|
|
99
|
+
- Compresed `Memory::Graph::Node` JSON representation for leaf nodes.
|
|
100
|
+
|
|
101
|
+
### v0.11.0
|
|
102
|
+
|
|
103
|
+
- 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.
|
|
104
|
+
|
|
97
105
|
### v0.10.0
|
|
98
106
|
|
|
99
107
|
- Add support for `Memory::Usage.of(..., via:)` for tracking reachability of objects.
|
|
@@ -128,14 +136,6 @@ Please see the [project releases](https://socketry.github.io/memory/releases/ind
|
|
|
128
136
|
|
|
129
137
|
- Ensure aggregate keys are safe for serialization (and printing).
|
|
130
138
|
|
|
131
|
-
### v0.7.0
|
|
132
|
-
|
|
133
|
-
- Add `Memory::Sampler#as_json` and `#to_json`.
|
|
134
|
-
|
|
135
|
-
### v0.6.0
|
|
136
|
-
|
|
137
|
-
- Add agent context.
|
|
138
|
-
|
|
139
139
|
## Contributing
|
|
140
140
|
|
|
141
141
|
We welcome contributions to this project.
|
data/releases.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.11.1
|
|
4
|
+
|
|
5
|
+
- Compresed `Memory::Graph::Node` JSON representation for leaf nodes.
|
|
6
|
+
|
|
7
|
+
## v0.11.0
|
|
8
|
+
|
|
9
|
+
- 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.
|
|
10
|
+
|
|
3
11
|
## v0.10.0
|
|
4
12
|
|
|
5
13
|
- Add support for `Memory::Usage.of(..., via:)` for tracking reachability of objects.
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
metadata.gz.sig
CHANGED
|
Binary file
|