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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 535ecdd0c6181d94b31384e4f71b4b4ce52d499d43daa647120fb22cf3dc244b
4
- data.tar.gz: bfc66be86d65f7065ebc6a4193420ccbe50a98ad5e581ba6cf613484d1846012
3
+ metadata.gz: 91e6bf9d1a607a68dd5d8f368d6dfe4a4319bf5730b6283194f99fb587b96364
4
+ data.tar.gz: 9a4335bfa899416c7fec144236dc5e711382f49f5631a4b0dbde03d73e84a58e
5
5
  SHA512:
6
- metadata.gz: e2df82be2210853cdc3ede7d47d450d1a38bc14291ee4fdaa3bece83809ce78ffb73ae714d52da9843dbd43d2659e6a97b17005ecbf3abe1f81fe8a687b0cbc5
7
- data.tar.gz: de6c8972b60a489e7f62834fe926674ed55103fae47a401ef81674b43a8a79b271a8b21051948c56b094a0372abb5493c78739265686b4c4591272ffd70b8f4b
6
+ metadata.gz: 0a704a533f5ac299f5b0da35814c7d93d13dc9881fda6c69c8f274a9331dedf3f9b57e1afa22b28888892092f2daf9a0c583259fbcbf26757d2f3c3805594caf
7
+ data.tar.gz: cc67f1e0781d93b648ed720efb1a4ce6d05c5d18c15e2b781ecbb00321fec08c674d2be2a051f1835bf9de1f2e1a90c5ee6df4be72ac46d0e4ba6f50ec48f5f5
checksums.yaml.gz.sig CHANGED
Binary file
@@ -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.any?
68
- object = queue.shift
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
- if reachable_objects = ObjectSpace.reachable_objects_from(object)
86
- queue.concat(reachable_objects)
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
 
@@ -7,5 +7,5 @@
7
7
  # Copyright, 2020-2025, by Samuel Williams.
8
8
 
9
9
  module Memory
10
- VERSION = "0.9.0"
10
+ VERSION = "0.11.0"
11
11
  end
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.9.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