Index: tools/clang/blink_gc_plugin/process-graph.py |
diff --git a/tools/clang/blink_gc_plugin/process-graph.py b/tools/clang/blink_gc_plugin/process-graph.py |
new file mode 100755 |
index 0000000000000000000000000000000000000000..155634cead84c069b05a9113f4e922936c5d4523 |
--- /dev/null |
+++ b/tools/clang/blink_gc_plugin/process-graph.py |
@@ -0,0 +1,283 @@ |
+#!/usr/bin/env python |
+# Copyright 2014 The Chromium Authors. All rights reserved. |
+# Use of this source code is governed by a BSD-style license that can be |
+# found in the LICENSE file. |
+ |
+import argparse, os, sys, json, subprocess |
+ |
+parser = argparse.ArgumentParser( |
+ description = |
+ "Process the Blink points-to graph generated by the Blink GC plugin.") |
+ |
+parser.add_argument( |
+ '-', dest='use_stdin', action='store_true', |
+ help='Read JSON graph files from stdin') |
+ |
+parser.add_argument( |
+ '-v', '--verbose', action='store_true', |
+ help='Verbose output') |
+ |
+parser.add_argument( |
+ '-c', '--detect-cycles', action='store_true', default=True, |
+ help='Detect cycles containing GC roots') |
+ |
+parser.add_argument( |
+ 'files', metavar='FILE', nargs='*', default=[], |
+ help='JSON graph files or directories containing them') |
+ |
+# Command line args after parsing. |
+args = None |
+ |
+# Map from node labels to nodes. |
+graph = {} |
+ |
+# Set of root nodes. |
+roots = [] |
+ |
+# Global flag to determine exit code. |
+global_reported_error = False |
+ |
+def set_reported_error(value): |
+ global global_reported_error |
+ global_reported_error = value |
+ |
+def reported_error(): |
+ return global_reported_error |
+ |
+def log(msg): |
+ if args.verbose: |
+ print msg |
+ |
+global_inc_copy = 0 |
+def inc_copy(): |
+ global global_inc_copy |
+ global_inc_copy += 1 |
+ |
+def get_node(name): |
+ return graph.setdefault(name, Node(name)) |
+ |
+# Representation of graph nodes. Basically a map of directed edges. |
+class Node: |
+ def __init__(self, name): |
+ self.name = name |
+ self.edges = {} |
+ self.reset() |
+ def __repr__(self): |
+ return "%s(%s) %s" % (self.name, self.visited, self.edges) |
+ def update_node(self, decl): |
+ # Currently we don't track any node info besides its edges. |
+ pass |
+ def update_edge(self, e): |
+ new_edge = Edge(**e) |
+ edge = self.edges.get(new_edge.key) |
+ if edge: |
+ # If an edge exist, its kind is the strongest of the two. |
+ edge.kind = max(edge.kind, new_edge.kind) |
+ else: |
+ self.edges[new_edge.key] = new_edge |
+ def super_edges(self): |
+ return [ e for e in self.edges.itervalues() if e.is_super() ] |
+ def reset(self): |
+ self.cost = sys.maxint |
+ self.visited = False |
+ self.path = None |
+ |
+# Representation of directed graph edges. |
+class Edge: |
+ def __init__(self, **decl): |
+ self.src = decl['src'] |
+ self.dst = decl['dst'] |
+ self.lbl = decl['lbl'] |
+ self.kind = decl['kind'] # 0 = weak, 1 = strong, 2 = root |
+ self.loc = decl['loc'] |
+ # The label does not uniquely determine an edge from a node. We |
+ # define the semi-unique key to be the concatenation of the |
+ # label and dst name. This is sufficient to track the strongest |
+ # edge to a particular type. For example, if the field A::m_f |
+ # has type HashMap<WeakMember<B>, Member<B>> we will have a |
+ # strong edge with key m_f#B from A to B. |
+ self.key = '%s#%s' % (self.lbl, self.dst) |
+ def copy(self): |
+ return Edge( |
+ lbl = self.lbl, |
+ kind = self.kind, |
+ src = self.src, |
+ dst = self.dst, |
+ loc = self.loc) |
+ def __repr__(self): |
+ return '%s (%s) => %s' % (self.src, self.lbl, self.dst) |
+ def is_root(self): |
+ return self.kind == 2 |
+ def is_weak(self): |
+ return self.kind == 0 |
+ def keeps_alive(self): |
+ return self.kind > 0 |
+ def is_subclass(self): |
+ return self.lbl.startswith('<subclass>') |
+ def is_super(self): |
+ return self.lbl.startswith('<super>') |
+ |
+def parse_file(filename): |
+ obj = json.load(open(filename)) |
+ return obj |
+ |
+def build_graphs_in_dir(dirname): |
+ # TODO: Use plateform independent code, eg, os.walk |
+ files = subprocess.check_output( |
+ ['find', dirname, '-name', '*.graph.json']).split('\n') |
+ log("Found %d files" % len(files)) |
+ for f in files: |
+ f.strip() |
+ if len(f) < 1: |
+ continue |
+ build_graph(f) |
+ |
+def build_graph(filename): |
+ for decl in parse_file(filename): |
+ if decl.has_key('name'): |
+ # Add/update a node entry |
+ name = decl['name'] |
+ node = get_node(name) |
+ node.update_node(decl) |
+ else: |
+ # Add/update an edge entry |
+ name = decl['src'] |
+ node = get_node(name) |
+ node.update_edge(decl) |
+ |
+# Copy all non-weak edges from super classes to their subclasses. |
+# This causes all fields of a super to be considered fields of a |
+# derived class without tranitively relating derived classes with |
+# each other. For example, if B <: A, C <: A, and for some D, D => B, |
+# we don't want that to entail that D => C. |
+def copy_super_edges(edge): |
+ if edge.is_weak() or not edge.is_super(): |
+ return |
+ inc_copy() |
+ # Make the super-class edge weak (prohibits processing twice). |
+ edge.kind = 0 |
+ # If the super class is not in our graph exit early. |
+ super_node = graph.get(edge.dst) |
+ if super_node is None: return |
+ # Recursively copy all super-class edges. |
+ for e in super_node.super_edges(): |
+ copy_super_edges(e) |
+ # Copy strong super-class edges (ignoring sub-class edges) to the sub class. |
+ sub_node = graph[edge.src] |
+ for e in super_node.edges.itervalues(): |
+ if e.keeps_alive() and not e.is_subclass(): |
+ new_edge = e.copy() |
+ new_edge.src = sub_node.name |
+ new_edge.lbl = '%s <: %s' % (super_node.name, e.lbl) |
+ sub_node.edges[new_edge.key] = new_edge |
+ # Add a strong sub-class edge. |
+ sub_edge = edge.copy() |
+ sub_edge.kind = 1 |
+ sub_edge.lbl = '<subclass>' |
+ sub_edge.src = super_node.name |
+ sub_edge.dst = sub_node.name |
+ super_node.edges[sub_edge.key] = sub_edge |
+ |
+def complete_graph(): |
+ for node in graph.itervalues(): |
+ for edge in node.super_edges(): |
+ copy_super_edges(edge) |
+ for edge in node.edges.itervalues(): |
+ if edge.is_root(): |
+ roots.append(edge) |
+ log("Copied edges down <super> edges for %d graph nodes" % global_inc_copy) |
+ |
+def reset_graph(): |
+ for n in graph.itervalues(): |
+ n.reset() |
+ |
+def shortest_path(start, end): |
+ start.cost = 0 |
+ minlist = [start] |
+ while len(minlist) > 0: |
+ minlist.sort(key=lambda n: -n.cost) |
+ current = minlist.pop() |
+ current.visited = True |
+ if current == end or current.cost >= end.cost + 1: |
+ return |
+ for e in current.edges.itervalues(): |
+ if not e.keeps_alive(): |
+ continue |
+ dst = graph.get(e.dst) |
+ if dst is None or dst.visited: |
+ continue |
+ if current.cost < dst.cost: |
+ dst.cost = current.cost + 1 |
+ dst.path = e |
+ minlist.append(dst) |
+ |
+def detect_cycles(): |
+ for root_edge in roots: |
+ reset_graph() |
+ src = graph[root_edge.src] |
+ dst = graph.get(root_edge.dst) |
+ if dst is None: |
+ print "\nPersistent root to incomplete destination object:" |
+ print root_edge |
+ set_reported_error(True) |
+ continue |
+ # Find the shortest path from the root target (dst) to its host (src) |
+ shortest_path(dst, src) |
+ if src.cost < sys.maxint: |
+ report_cycle(root_edge) |
+ |
+def report_cycle(root_edge): |
+ dst = graph[root_edge.dst] |
+ print "\nFound a potentially leaking cycle starting from a GC root:" |
+ path = [] |
+ edge = root_edge |
+ dst.path = None |
+ while edge: |
+ path.append(edge) |
+ edge = graph[edge.src].path |
+ path.append(root_edge) |
+ path.reverse() |
+ # Find the max loc length for pretty printing. |
+ max_loc = 0 |
+ for p in path: |
+ if len(p.loc) > max_loc: |
+ max_loc = len(p.loc) |
+ for p in path[:-1]: |
+ print (p.loc + ':').ljust(max_loc + 1), p |
+ set_reported_error(True) |
+ |
+def main(): |
+ global args |
+ args = parser.parse_args() |
+ if not args.detect_cycles: |
+ print "Please select an operation to perform (eg, -c to detect cycles)" |
+ parser.print_help() |
+ return 1 |
+ if args.use_stdin: |
+ log("Reading files from stdin") |
+ for f in sys.stdin: |
+ build_graph(f.strip()) |
+ else: |
+ log("Reading files and directories from command line") |
+ if len(args.files) == 0: |
+ print "Please provide files or directores for building the graph" |
+ parser.print_help() |
+ return 1 |
+ for f in args.files: |
+ if os.path.isdir(f): |
+ log("Building graph from files in directory: " + f) |
+ build_graphs_in_dir(f) |
+ else: |
+ log("Building graph from file: " + f) |
+ build_graph(f) |
+ log("Completing graph construction (%d graph nodes)" % len(graph)) |
+ complete_graph() |
+ if args.detect_cycles: |
+ log("Detecting cycles containg GC roots") |
+ detect_cycles() |
+ if reported_error(): |
+ return 1 |
+ return 0 |
+ |
+if __name__ == '__main__': |
+ sys.exit(main()) |