summaryrefslogtreecommitdiffstats
path: root/python
diff options
context:
space:
mode:
authorDavid Lamparter <equinox@diac24.net>2020-05-04 20:57:45 +0200
committerDavid Lamparter <equinox@diac24.net>2020-05-05 14:39:12 +0200
commit3d62176b18ce0bccf430d2bb48088c6763eb8201 (patch)
tree5fd1a67000039e447775a3a42c4a96be1c042586 /python
parentbuild: add LLVM bitcode targets (diff)
downloadfrr-3d62176b18ce0bccf430d2bb48088c6763eb8201.tar.xz
frr-3d62176b18ce0bccf430d2bb48088c6763eb8201.zip
python: add graphviz callgraphs
Uses the JSON data extracted from LLVM bitcode by tools/frr-llvm-cg. Signed-off-by: David Lamparter <equinox@opensourcerouting.org>
Diffstat (limited to 'python')
-rw-r--r--python/callgraph-dot.py476
1 files changed, 476 insertions, 0 deletions
diff --git a/python/callgraph-dot.py b/python/callgraph-dot.py
new file mode 100644
index 000000000..4faf1dae1
--- /dev/null
+++ b/python/callgraph-dot.py
@@ -0,0 +1,476 @@
+# callgraph json to graphviz generator for FRR
+#
+# Copyright (C) 2020 David Lamparter for NetDEF, Inc.
+#
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the Free
+# Software Foundation; either version 2 of the License, or (at your option)
+# any later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; see the file COPYING; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import re
+import sys
+import json
+
+class FunctionNode(object):
+ funcs = {}
+
+ def __init__(self, name):
+ super().__init__()
+ FunctionNode.funcs[name] = self
+
+ self.name = name
+ self.out = []
+ self.inb = []
+ self.rank = None
+ self.defined = False
+ self.defs = []
+
+ def __repr__(self):
+ return '<"%s()" rank=%r>' % (self.name, self.rank)
+
+ def define(self, attrs):
+ self.defined = True
+ self.defs.append((attrs['filename'], attrs['line']))
+ return self
+
+ def add_call(self, called, attrs):
+ return CallEdge(self, called, attrs)
+
+ def calls(self):
+ for e in self.out:
+ yield e.o
+
+ def calld(self):
+ for e in self.inb:
+ yield e.i
+
+ def unlink(self, other):
+ self.out = list([edge for edge in self.out if edge.o != other])
+ other.inb = list([edge for edge in other.inb if edge.i != other])
+
+ @classmethod
+ def get(cls, name):
+ if name in cls.funcs:
+ return cls.funcs[name]
+ return FunctionNode(name)
+
+class CallEdge(object):
+ def __init__(self, i, o, attrs):
+ self.i = i
+ self.o = o
+ self.is_external = attrs['is_external']
+ self.attrs = attrs
+
+ i.out.append(self)
+ o.inb.append(self)
+
+ def __repr__(self):
+ return '<"%s()" -> "%s()">' % (self.i.name, self.o.name)
+
+def nameclean(n):
+ if '.' in n:
+ return n.split('.', 1)[0]
+ return n
+
+def calc_rank(queue, direction):
+ nextq = queue
+
+ if direction == 1:
+ aggr = max
+ elem = lambda x: x.calls()
+ else:
+ aggr = min
+ elem = lambda x: x.calld()
+
+ currank = direction
+ cont = True
+
+ while len(nextq) > 0 and cont:
+ queue = nextq
+ nextq = []
+
+ #sys.stderr.write('rank %d\n' % currank)
+
+ cont = False
+
+ for node in queue:
+ if not node.defined:
+ node.rank = 0
+ continue
+
+ rank = direction
+ for other in elem(node):
+ if other is node:
+ continue
+ if other.rank is None:
+ nextq.append(node)
+ break
+ rank = aggr(rank, other.rank + direction)
+ else:
+ cont = True
+ node.rank = rank
+
+ currank += direction
+
+ return nextq
+
+class Graph(dict):
+ class Subgraph(set):
+ def __init__(self):
+ super().__init__()
+
+ class NodeGroup(set):
+ def __init__(self, members):
+ super().__init__(members)
+
+ class Node(object):
+ def __init__(self, graph, fn):
+ super().__init__()
+ self._fn = fn
+ self._fns = [fn]
+ self._graph = graph
+ self._calls = set()
+ self._calld = set()
+ self._group = None
+
+ def __repr__(self):
+ return '<Graph.Node "%s()"/%d>' % (self._fn.name, len(self._fns))
+
+ def __hash__(self):
+ return hash(self._fn.name)
+
+ def _finalize(self):
+ for called in self._fn.calls():
+ if called.name == self._fn.name:
+ continue
+ if called.name in self._graph:
+ self._calls.add(self._graph[called.name])
+ self._graph[called.name]._calld.add(self)
+
+ def unlink(self, other):
+ self._calls.remove(other)
+ other._calld.remove(self)
+
+ @property
+ def name(self):
+ return self._fn.name
+
+ def calls(self):
+ return self._calls
+ def calld(self):
+ return self._calld
+
+ def group(self, members):
+ assert self in members
+
+ pregroups = []
+ for g in [m._group for m in members]:
+ if g is None:
+ continue
+ if g in pregroups:
+ continue
+
+ assert g <= members
+ pregroups.append(g)
+
+ if len(pregroups) == 0:
+ group = self._graph.NodeGroup(members)
+ self._graph._groups.append(group)
+ elif len(pregroups) == 1:
+ group = pregroups[0]
+ group |= members
+ else:
+ for g in pregroups:
+ self._graph._groups.remove(g)
+ group = self._graph.NodeGroup(members)
+ self._graph._groups.append(group)
+
+ for m in members:
+ m._group = group
+ return group
+
+ def merge(self, other):
+ self._fns.extend(other._fns)
+ self._calls = (self._calls | other._calls) - {self, other}
+ self._calld = (self._calld | other._calld) - {self, other}
+ for c in other._calls:
+ if c == self:
+ continue
+ c._calld.remove(other)
+ c._calld.add(self)
+ for c in other._calld:
+ if c == self:
+ continue
+ c._calls.remove(other)
+ c._calls.add(self)
+ del self._graph[other._fn.name]
+
+ def __init__(self, funcs):
+ super().__init__()
+ self._funcs = funcs
+ for fn in funcs:
+ self[fn.name] = self.Node(self, fn)
+ for node in self.values():
+ node._finalize()
+ self._groups = []
+
+ def automerge(self):
+ nodes = list(self.values())
+
+ while len(nodes):
+ node = nodes.pop(0)
+
+ candidates = {node}
+ evalset = set(node.calls())
+ prevevalset = None
+
+ while prevevalset != evalset:
+ prevevalset = evalset
+ evalset = set()
+
+ for evnode in prevevalset:
+ inbound = set(evnode.calld())
+ if inbound <= candidates:
+ candidates.add(evnode)
+ evalset |= set(evnode.calls()) - candidates
+ else:
+ evalset.add(evnode)
+
+ #if len(candidates) > 1:
+ # for candidate in candidates:
+ # if candidate != node:
+ # #node.merge(candidate)
+ # if candidate in nodes:
+ # nodes.remove(candidate)
+ node.group(candidates)
+
+ for candidate in candidates:
+ if candidate in nodes:
+ nodes.remove(candidate)
+
+ def calc_subgraphs(self):
+ nodes = list(self.values())
+ self._subgraphs = []
+ up = {}
+ down = {}
+
+ self._linear_nodes = []
+
+ while len(nodes):
+ sys.stderr.write('%d\n' % len(nodes))
+ node = nodes.pop(0)
+
+ down[node] = set()
+ queue = [node]
+ while len(queue):
+ now = queue.pop()
+ down[node].add(now)
+ for calls in now.calls():
+ if calls in down[node]:
+ continue
+ queue.append(calls)
+
+ up[node] = set()
+ queue = [node]
+ while len(queue):
+ now = queue.pop()
+ up[node].add(now)
+ for calld in now.calld():
+ if calld in up[node]:
+ continue
+ queue.append(calld)
+
+ common = up[node] & down[node]
+
+ if len(common) == 1:
+ self._linear_nodes.append(node)
+ else:
+ sg = self.Subgraph()
+ sg |= common
+ self._subgraphs.append(sg)
+ for n in common:
+ if n != node:
+ nodes.remove(n)
+
+ return self._subgraphs, self._linear_nodes
+
+
+with open(sys.argv[1], 'r') as fd:
+ data = json.load(fd)
+
+extra_info = {
+ # zebra - LSP WQ
+ ('lsp_processq_add', 'work_queue_add'): [
+ 'lsp_process',
+ 'lsp_processq_del',
+ 'lsp_processq_complete',
+ ],
+ # zebra - main WQ
+ ('mq_add_handler', 'work_queue_add'): [
+ 'meta_queue_process',
+ ],
+ ('meta_queue_process', 'work_queue_add'): [
+ 'meta_queue_process',
+ ],
+ # bgpd - label pool WQ
+ ('bgp_lp_get', 'work_queue_add'): [
+ 'lp_cbq_docallback',
+ ],
+ ('bgp_lp_event_chunk', 'work_queue_add'): [
+ 'lp_cbq_docallback',
+ ],
+ ('bgp_lp_event_zebra_up', 'work_queue_add'): [
+ 'lp_cbq_docallback',
+ ],
+ # bgpd - main WQ
+ ('bgp_process', 'work_queue_add'): [
+ 'bgp_process_wq',
+ 'bgp_processq_del',
+ ],
+ ('bgp_add_eoiu_mark', 'work_queue_add'): [
+ 'bgp_process_wq',
+ 'bgp_processq_del',
+ ],
+ # clear node WQ
+ ('bgp_clear_route_table', 'work_queue_add'): [
+ 'bgp_clear_route_node',
+ 'bgp_clear_node_queue_del',
+ 'bgp_clear_node_complete',
+ ],
+ # rfapi WQs
+ ('rfapi_close', 'work_queue_add'): [
+ 'rfapi_deferred_close_workfunc',
+ ],
+ ('rfapiRibUpdatePendingNode', 'work_queue_add'): [
+ 'rfapiRibDoQueuedCallback',
+ 'rfapiRibQueueItemDelete',
+ ],
+}
+
+
+for func, fdata in data['functions'].items():
+ func = nameclean(func)
+ fnode = FunctionNode.get(func).define(fdata)
+
+ for call in fdata['calls']:
+ if call.get('type') in [None, 'unnamed', 'thread_sched']:
+ if call.get('target') is None:
+ continue
+ tgt = nameclean(call['target'])
+ fnode.add_call(FunctionNode.get(tgt), call)
+ for fptr in call.get('funcptrs', []):
+ fnode.add_call(FunctionNode.get(nameclean(fptr)), call)
+ if tgt == 'work_queue_add':
+ if (func, tgt) not in extra_info:
+ sys.stderr.write('%s:%d:%s(): work_queue_add() not handled\n' % (
+ call['filename'], call['line'], func))
+ else:
+ attrs = dict(call)
+ attrs.update({'is_external': False, 'type': 'workqueue'})
+ for dst in extra_info[func, tgt]:
+ fnode.add_call(FunctionNode.get(dst), call)
+ elif call['type'] == 'install_element':
+ vty_node = FunctionNode.get('VTY_NODE_%d' % call['vty_node'])
+ vty_node.add_call(FunctionNode.get(nameclean(call['target'])), call)
+ elif call['type'] == 'hook':
+ # TODO: edges for hooks from data['hooks']
+ pass
+
+n = FunctionNode.funcs
+
+# fix some very low end functions cycling back very far to the top
+if 'peer_free' in n:
+ n['peer_free'].unlink(n['bgp_timer_set'])
+ n['peer_free'].unlink(n['bgp_addpath_set_peer_type'])
+if 'bgp_path_info_extra_free' in n:
+ n['bgp_path_info_extra_free'].rank = 0
+
+if 'zlog_ref' in n:
+ n['zlog_ref'].rank = 0
+if 'mt_checkalloc' in n:
+ n['mt_checkalloc'].rank = 0
+
+queue = list(FunctionNode.funcs.values())
+queue = calc_rank(queue, 1)
+queue = calc_rank(queue, -1)
+
+sys.stderr.write('%d functions in cyclic set\n' % len(queue))
+
+graph = Graph(queue)
+graph.automerge()
+
+gv_nodes = []
+gv_edges = []
+
+sys.stderr.write('%d groups after automerge\n' % len(graph._groups))
+
+def is_vnc(n):
+ return n.startswith('rfapi') or n.startswith('vnc') or ('_vnc_' in n)
+
+_vncstyle = ',fillcolor="#ffffcc",style=filled'
+cyclic_set_names = set([fn.name for fn in graph.values()])
+
+for i, group in enumerate(graph._groups):
+ if len(group) > 1:
+ group.num = i
+ gv_nodes.append('\tsubgraph cluster_%d {' % i)
+ gv_nodes.append('\t\tcolor=blue;')
+ for gn in group:
+ has_cycle_callers = set(gn.calld()) - group
+ has_ext_callers = set([edge.i.name for edge in gn._fn.inb]) - cyclic_set_names
+
+ style = ''
+ etext = ''
+ if is_vnc(gn.name):
+ style += _vncstyle
+ if has_cycle_callers:
+ style += ',color=blue,penwidth=3'
+ if has_ext_callers:
+ style += ',fillcolor="#ffeebb",style=filled'
+ etext += '<br/><font point-size="10">(%d other callers)</font>' % (len(has_ext_callers))
+
+ gv_nodes.append('\t\t"%s" [shape=box,label=<%s%s>%s];' % (gn.name, '<br/>'.join([fn.name for fn in gn._fns]), etext, style))
+ gv_nodes.append('\t}')
+ else:
+ for gn in group:
+ has_ext_callers = set([edge.i.name for edge in gn._fn.inb]) - cyclic_set_names
+
+ style = ''
+ etext = ''
+ if is_vnc(gn.name):
+ style += _vncstyle
+ if has_ext_callers:
+ style += ',fillcolor="#ffeebb",style=filled'
+ etext += '<br/><font point-size="10">(%d other callers)</font>' % (len(has_ext_callers))
+ gv_nodes.append('\t"%s" [shape=box,label=<%s%s>%s];' % (gn.name, '<br/>'.join([fn.name for fn in gn._fns]), etext, style))
+
+edges = set()
+for gn in graph.values():
+ for calls in gn.calls():
+ if gn._group == calls._group:
+ gv_edges.append('\t"%s" -> "%s" [color="#55aa55",style=dashed];' % (gn.name, calls.name))
+ else:
+ def xname(nn):
+ if len(nn._group) > 1:
+ return 'cluster_%d' % nn._group.num
+ else:
+ return nn.name
+ tup = xname(gn), calls.name
+ if tup[0] != tup[1] and tup not in edges:
+ gv_edges.append('\t"%s" -> "%s" [weight=0.0,w=0.0,color=blue];' % tup)
+ edges.add(tup)
+
+with open(sys.argv[2], 'w') as fd:
+ fd.write('''digraph {
+ node [fontsize=13,fontname="Fira Sans"];
+%s
+}''' % '\n'.join(gv_nodes + [''] + gv_edges))