Class: HeapScope::Graph
- Inherits:
-
Object
- Object
- HeapScope::Graph
- Defined in:
- lib/heapscope/graph.rb
Overview
Bounded reachability / retention-path analysis for deep mode. Never fabricates roots; reports nearest observed retainer when uncertain.
Defined Under Namespace
Instance Method Summary collapse
- #available? ⇒ Boolean
- #estimate_retained_size(root, max_objects: nil, max_depth: nil) ⇒ Object
-
#initialize(runtime: Runtime.current, config: HeapScope.config) ⇒ Graph
constructor
A new instance of Graph.
-
#retention_path(target, roots: nil, max_depth: nil, max_edges: nil) ⇒ Object
BFS for a short human-understandable retention path from known roots.
- #thread_local_inventory ⇒ Object
- #unbounded_collection_candidates(_samples) ⇒ Object
Constructor Details
Instance Method Details
#available? ⇒ Boolean
16 17 18 |
# File 'lib/heapscope/graph.rb', line 16 def available? @runtime.reachable_objects? end |
#estimate_retained_size(root, max_objects: nil, max_depth: nil) ⇒ Object
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/heapscope/graph.rb', line 85 def estimate_retained_size(root, max_objects: nil, max_depth: nil) max_objects ||= [@config.max_objects, 10_000].min max_depth ||= @config.max_graph_depth return { shallow: @runtime.memsize_of(root), retained: nil, note: "unavailable" } unless available? seen = {} total = 0 count = 0 queue = [[root, 0]] seen[root.__id__] = true while (pair = queue.shift) obj, depth = pair count += 1 break if count >= max_objects total += @runtime.memsize_of(obj).to_i next if depth >= max_depth safe_reachable(obj).each do |child| cid = child.__id__ next if seen[cid] seen[cid] = true queue << [child, depth + 1] end end { shallow: @runtime.memsize_of(root), retained: total, objects: count, approximate: true, note: "Approximate retained size via bounded traversal" } end |
#retention_path(target, roots: nil, max_depth: nil, max_edges: nil) ⇒ Object
BFS for a short human-understandable retention path from known roots.
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/heapscope/graph.rb', line 21 def retention_path(target, roots: nil, max_depth: nil, max_edges: nil) max_depth ||= @config.max_graph_depth max_edges ||= @config.max_edges return unavailable_path("reachable_objects API unavailable") unless available? roots ||= default_roots visited = {} parent = {} via = {} queue = [] edges = 0 target_id = target.__id__ roots.each do |root, label| id = root.__id__ next if visited[id] visited[id] = true queue << root parent[id] = nil via[id] = label end found = nil while (current = queue.shift) break if edges >= max_edges depth = depth_of(current.__id__, parent) next if depth >= max_depth children = safe_reachable(current) children.each_with_index do |child, idx| edges += 1 break if edges >= max_edges cid = child.__id__ next if visited[cid] visited[cid] = true parent[cid] = current.__id__ via[cid] = edge_label(current, child, idx) queue << child if cid == target_id found = child break end end break if found end if found build_path(found, parent, via, :high, "Bounded BFS path from known roots") else nearest = nearest_retainer(target) Path.new( nodes: nearest, confidence: :low, note: "Nearest observed retainer; true root not identified within budget" ) end rescue AnalysisLimitError => e Path.new(nodes: [], confidence: :low, note: e.) end |
#thread_local_inventory ⇒ Object
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'lib/heapscope/graph.rb', line 122 def thread_local_inventory Thread.list.map do |thread| keys = if thread.respond_to?(:keys) thread.keys else [] end values = keys.each_with_object({}) do |key, hash| value = thread[key] hash[key.inspect] = { class: value.class.name, shallow_bytes: @runtime.memsize_of(value) } rescue StandardError hash[key.inspect] = { class: "unknown" } end { name: thread.name || "thread-#{thread.object_id}", status: thread.status, keys: values } end end |
#unbounded_collection_candidates(_samples) ⇒ Object
148 149 150 151 |
# File 'lib/heapscope/graph.rb', line 148 def unbounded_collection_candidates(_samples) # samples: array of { object_local_id/class/size } or class series sizes for Array/Hash [] end |