memory 0.10.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 610a559d7ecf64e0ec26f443e53492bcc8853f4c2a384ccb4384bf14855b9805
4
- data.tar.gz: 53c34a593cf01eb64661d762b46a5a1e2d0b4ef3597f399cf6c2be76244ad2d4
3
+ metadata.gz: 91e6bf9d1a607a68dd5d8f368d6dfe4a4319bf5730b6283194f99fb587b96364
4
+ data.tar.gz: 9a4335bfa899416c7fec144236dc5e711382f49f5631a4b0dbde03d73e84a58e
5
5
  SHA512:
6
- metadata.gz: 3d014fe210c6b215773e275a7b9caa1ddcf346dae6fb21e06c637d01b638ff193154b77e3e4b54a6f9fca7a92bb9d18a8d56bf75ea0cc057d2749fe6b75b5d47
7
- data.tar.gz: 7918321809f910e3979384a44863e02f6c37e797918caf6e1524103e4a808be847a2bec37ddc1a1cbf35e761e45c15aef2fb4f85d521828ae252e0a5042660d5
6
+ metadata.gz: 0a704a533f5ac299f5b0da35814c7d93d13dc9881fda6c69c8f274a9331dedf3f9b57e1afa22b28888892092f2daf9a0c583259fbcbf26757d2f3c3805594caf
7
+ data.tar.gz: cc67f1e0781d93b648ed720efb1a4ce6d05c5d18c15e2b781ecbb00321fec08c674d2be2a051f1835bf9de1f2e1a90c5ee6df4be72ac46d0e4ba6f50ec48f5f5
checksums.yaml.gz.sig CHANGED
Binary file
data/lib/memory/graph.rb CHANGED
@@ -3,139 +3,215 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2025, by Samuel Williams.
5
5
 
6
- require "set"
6
+ require_relative "usage"
7
+ require "json"
7
8
 
8
9
  module Memory
9
10
  # Tracks object traversal paths for memory analysis.
10
- #
11
- # The Graph class maintains a mapping of objects to their parent objects,
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
- # Add a parent-child relationship to the via mapping.
22
- #
23
- # @parameter child [Object] The child object.
24
- # @parameter parent [Object] The parent object that references the child.
25
- def []=(child, parent)
26
- @mapping[child] = parent
27
- end
28
-
29
- # Get the parent of an object.
30
- #
31
- # @parameter child [Object] The child object.
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
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
57
55
  end
58
56
 
59
- # Check array elements:
60
- if parent.is_a?(Array)
61
- parent.each_with_index do |element, index|
62
- if element.equal?(child)
63
- return "[#{index}]"
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
- # Check hash keys and values:
69
- if parent.is_a?(Hash)
70
- parent.each do |key, value|
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 "[#{key.inspect}]"
80
+ return ivar.to_s
73
81
  end
74
- if key.equal?(child)
75
- return "(key: #{key.inspect})"
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
76
111
  end
77
112
  end
113
+
114
+ # Could not determine the reference:
115
+ return nil
78
116
  end
79
117
 
80
- # Check struct members:
81
- if parent.is_a?(Struct)
82
- parent.each_pair do |member, value|
83
- if value.equal?(child)
84
- return ".#{member}"
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
86
144
  end
145
+
146
+ return @path
87
147
  end
88
148
 
89
- # Could not determine the reference:
90
- return nil
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
91
173
  end
92
174
 
93
- # Construct a human-readable path from an object back to a root.
175
+ # Build a graph of nodes from a root object, computing usage at each level.
94
176
  #
95
- # @parameter object [Object] The object to trace back from.
96
- # @parameter root [Object | Nil] The root object to trace to. If nil, traces to any root.
97
- # @returns [Array(Array(Object), Array(String))] A tuple of [object_path, reference_path].
98
- def path_to(object, root = nil)
99
- # Build the object path by following via backwards:
100
- object_path = [object]
101
- current = object
102
-
103
- while @mapping.key?(current)
104
- parent = @mapping[current]
105
- object_path << parent
106
- current = parent
107
-
108
- # Stop if we reached the specified root:
109
- break if root && current.equal?(root)
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)
110
188
  end
111
189
 
112
- # Reverse to get path from root to object:
113
- object_path.reverse!
190
+ # Compute shallow usage for just this object:
191
+ usage = Usage.new(ObjectSpace.memsize_of(root), 1)
114
192
 
115
- return object_path
116
- end
117
-
118
- # Format a human-readable path string.
119
- #
120
- # @parameter object [Object] The object to trace back from.
121
- # @parameter root [Object | Nil] The root object to trace to. If nil, traces to any root.
122
- # @returns [String] A formatted path string.
123
- def path(object, root = nil)
124
- object_path = path_to(object, root)
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]
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)}
133
203
 
134
- parts << (find_reference(parent, child) || "<??>")
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))
135
212
  end
136
213
 
137
- return parts.join
214
+ return node
138
215
  end
139
216
  end
140
217
  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, via: nil)
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
@@ -7,5 +7,5 @@
7
7
  # Copyright, 2020-2025, by Samuel Williams.
8
8
 
9
9
  module Memory
10
- VERSION = "0.10.0"
10
+ VERSION = "0.11.0"
11
11
  end
data/readme.md CHANGED
@@ -94,6 +94,10 @@ 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
+
97
101
  ### v0.10.0
98
102
 
99
103
  - Add support for `Memory::Usage.of(..., via:)` for tracking reachability of objects.
@@ -132,10 +136,6 @@ Please see the [project releases](https://socketry.github.io/memory/releases/ind
132
136
 
133
137
  - Add `Memory::Sampler#as_json` and `#to_json`.
134
138
 
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,9 @@
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
+
3
7
  ## v0.10.0
4
8
 
5
9
  - Add support for `Memory::Usage.of(..., via:)` for tracking reachability of 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.10.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Saffron
metadata.gz.sig CHANGED
Binary file