memory 0.9.0 → 0.10.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: 610a559d7ecf64e0ec26f443e53492bcc8853f4c2a384ccb4384bf14855b9805
4
+ data.tar.gz: 53c34a593cf01eb64661d762b46a5a1e2d0b4ef3597f399cf6c2be76244ad2d4
5
5
  SHA512:
6
- metadata.gz: e2df82be2210853cdc3ede7d47d450d1a38bc14291ee4fdaa3bece83809ce78ffb73ae714d52da9843dbd43d2659e6a97b17005ecbf3abe1f81fe8a687b0cbc5
7
- data.tar.gz: de6c8972b60a489e7f62834fe926674ed55103fae47a401ef81674b43a8a79b271a8b21051948c56b094a0372abb5493c78739265686b4c4591272ffd70b8f4b
6
+ metadata.gz: 3d014fe210c6b215773e275a7b9caa1ddcf346dae6fb21e06c637d01b638ff193154b77e3e4b54a6f9fca7a92bb9d18a8d56bf75ea0cc057d2749fe6b75b5d47
7
+ data.tar.gz: 7918321809f910e3979384a44863e02f6c37e797918caf6e1524103e4a808be847a2bec37ddc1a1cbf35e761e45c15aef2fb4f85d521828ae252e0a5042660d5
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require "set"
7
+
8
+ module Memory
9
+ # 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
20
+
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
57
+ end
58
+
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}]"
64
+ end
65
+ end
66
+ end
67
+
68
+ # Check hash keys and values:
69
+ if parent.is_a?(Hash)
70
+ parent.each do |key, value|
71
+ if value.equal?(child)
72
+ return "[#{key.inspect}]"
73
+ end
74
+ if key.equal?(child)
75
+ return "(key: #{key.inspect})"
76
+ end
77
+ end
78
+ end
79
+
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}"
85
+ end
86
+ end
87
+ end
88
+
89
+ # Could not determine the reference:
90
+ return nil
91
+ end
92
+
93
+ # Construct a human-readable path from an object back to a root.
94
+ #
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)
110
+ end
111
+
112
+ # Reverse to get path from root to object:
113
+ object_path.reverse!
114
+
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]
133
+
134
+ parts << (find_reference(parent, child) || "<??>")
135
+ end
136
+
137
+ return parts.join
138
+ end
139
+ end
140
+ end
141
+
data/lib/memory/usage.rb CHANGED
@@ -57,33 +57,42 @@ module Memory
57
57
  ]
58
58
 
59
59
  # Compute the usage of an object and all reachable objects from it.
60
+ #
61
+ # The root is always visited even if it is in `seen`.
62
+ #
60
63
  # @parameter root [Object] The root object to start traversal from.
64
+ # @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.
61
66
  # @returns [Usage] The usage of the object and all reachable objects from it.
62
- def self.of(root, seen: Set.new.compare_by_identity, ignore: IGNORE)
67
+ def self.of(root, seen: Set.new.compare_by_identity, ignore: IGNORE, via: nil)
63
68
  count = 0
64
69
  size = 0
65
70
 
66
71
  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:
72
+ while object = queue.shift
73
+ # Add the object to the seen set:
80
74
  seen.add(object)
75
+
76
+ # Update the count and size:
81
77
  count += 1
82
78
  size += ObjectSpace.memsize_of(object)
83
79
 
84
80
  # Add the object's reachable objects to the queue:
85
- if reachable_objects = ObjectSpace.reachable_objects_from(object)
86
- queue.concat(reachable_objects)
81
+ ObjectSpace.reachable_objects_from(object)&.each do |reachable_object|
82
+ # Skip ignored types:
83
+ next if ignore.any?{|type| reachable_object.is_a?(type)}
84
+
85
+ # Skip internal objects - they don't behave correctly when added to `seen` and create unbounded recursion:
86
+ next if reachable_object.is_a?(ObjectSpace::InternalObjectWrapper)
87
+
88
+ # Skip objects we have already seen:
89
+ next if seen.include?(reachable_object)
90
+
91
+ if via
92
+ via[reachable_object] ||= object
93
+ end
94
+
95
+ queue << reachable_object
87
96
  end
88
97
  end
89
98
 
@@ -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.10.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,11 @@ end
94
94
 
95
95
  Please see the [project releases](https://socketry.github.io/memory/releases/index) for all releases.
96
96
 
97
+ ### v0.10.0
98
+
99
+ - Add support for `Memory::Usage.of(..., via:)` for tracking reachability of objects.
100
+ - Introduce `Memory::Graph` for computing paths between parent/child objects.
101
+
97
102
  ### v0.9.0
98
103
 
99
104
  - Explicit `ignore:` and `seen:` parameters for `Memory::Usage.of` to allow customization of ignored types and tracking of seen objects.
data/releases.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Releases
2
2
 
3
+ ## v0.10.0
4
+
5
+ - Add support for `Memory::Usage.of(..., via:)` for tracking reachability of objects.
6
+ - Introduce `Memory::Graph` for computing paths between parent/child objects.
7
+
3
8
  ## v0.9.0
4
9
 
5
10
  - 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.10.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