Class: HeapScope::Graph

Inherits:
Object
  • Object
show all
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

Classes: Edge, Node, Path

Instance Method Summary collapse

Constructor Details

#initialize(runtime: Runtime.current, config: HeapScope.config) ⇒ Graph

Returns a new instance of Graph.



11
12
13
14
# File 'lib/heapscope/graph.rb', line 11

def initialize(runtime: Runtime.current, config: HeapScope.config)
  @runtime = runtime
  @config = config
end

Instance Method Details

#available?Boolean

Returns:

  • (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.message)
end

#thread_local_inventoryObject



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