]>
git.proxmox.com Git - mirror_frr.git/blob - python/callgraph-dot.py
1 # callgraph json to graphviz generator for FRR
3 # Copyright (C) 2020 David Lamparter for NetDEF, Inc.
5 # This program is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by the Free
7 # Software Foundation; either version 2 of the License, or (at your option)
10 # This program is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15 # You should have received a copy of the GNU General Public License along
16 # with this program; see the file COPYING; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
24 class FunctionNode(object):
27 def __init__(self
, name
):
29 FunctionNode
.funcs
[name
] = self
39 return '<"%s()" rank=%r>' % (self
.name
, self
.rank
)
41 def define(self
, attrs
):
43 self
.defs
.append((attrs
["filename"], attrs
["line"]))
46 def add_call(self
, called
, attrs
):
47 return CallEdge(self
, called
, attrs
)
57 def unlink(self
, other
):
58 self
.out
= list([edge
for edge
in self
.out
if edge
.o
!= other
])
59 other
.inb
= list([edge
for edge
in other
.inb
if edge
.i
!= other
])
64 return cls
.funcs
[name
]
65 return FunctionNode(name
)
68 class CallEdge(object):
69 def __init__(self
, i
, o
, attrs
):
72 self
.is_external
= attrs
["is_external"]
79 return '<"%s()" -> "%s()">' % (self
.i
.name
, self
.o
.name
)
84 return n
.split(".", 1)[0]
88 def calc_rank(queue
, direction
):
93 elem
= lambda x
: x
.calls()
96 elem
= lambda x
: x
.calld()
101 while len(nextq
) > 0 and cont
:
105 # sys.stderr.write('rank %d\n' % currank)
115 for other
in elem(node
):
118 if other
.rank
is None:
121 rank
= aggr(rank
, other
.rank
+ direction
)
136 class NodeGroup(set):
137 def __init__(self
, members
):
138 super().__init
__(members
)
141 def __init__(self
, graph
, fn
):
151 return '<Graph.Node "%s()"/%d>' % (self
._fn
.name
, len(self
._fns
))
154 return hash(self
._fn
.name
)
157 for called
in self
._fn
.calls():
158 if called
.name
== self
._fn
.name
:
160 if called
.name
in self
._graph
:
161 self
._calls
.add(self
._graph
[called
.name
])
162 self
._graph
[called
.name
]._calld
.add(self
)
164 def unlink(self
, other
):
165 self
._calls
.remove(other
)
166 other
._calld
.remove(self
)
178 def group(self
, members
):
179 assert self
in members
182 for g
in [m
._group
for m
in members
]:
191 if len(pregroups
) == 0:
192 group
= self
._graph
.NodeGroup(members
)
193 self
._graph
._groups
.append(group
)
194 elif len(pregroups
) == 1:
199 self
._graph
._groups
.remove(g
)
200 group
= self
._graph
.NodeGroup(members
)
201 self
._graph
._groups
.append(group
)
207 def merge(self
, other
):
208 self
._fns
.extend(other
._fns
)
209 self
._calls
= (self
._calls | other
._calls
) - {self
, other
}
210 self
._calld
= (self
._calld | other
._calld
) - {self
, other
}
211 for c
in other
._calls
:
214 c
._calld
.remove(other
)
216 for c
in other
._calld
:
219 c
._calls
.remove(other
)
221 del self
._graph
[other
._fn
.name
]
223 def __init__(self
, funcs
):
227 self
[fn
.name
] = self
.Node(self
, fn
)
228 for node
in self
.values():
233 nodes
= list(self
.values())
239 evalset
= set(node
.calls())
242 while prevevalset
!= evalset
:
243 prevevalset
= evalset
246 for evnode
in prevevalset
:
247 inbound
= set(evnode
.calld())
248 if inbound
<= candidates
:
249 candidates
.add(evnode
)
250 evalset |
= set(evnode
.calls()) - candidates
254 # if len(candidates) > 1:
255 # for candidate in candidates:
256 # if candidate != node:
257 # #node.merge(candidate)
258 # if candidate in nodes:
259 # nodes.remove(candidate)
260 node
.group(candidates
)
262 for candidate
in candidates
:
263 if candidate
in nodes
:
264 nodes
.remove(candidate
)
266 def calc_subgraphs(self
):
267 nodes
= list(self
.values())
272 self
._linear
_nodes
= []
275 sys
.stderr
.write("%d\n" % len(nodes
))
283 for calls
in now
.calls():
284 if calls
in down
[node
]:
293 for calld
in now
.calld():
294 if calld
in up
[node
]:
298 common
= up
[node
] & down
[node
]
301 self
._linear
_nodes
.append(node
)
305 self
._subgraphs
.append(sg
)
310 return self
._subgraphs
, self
._linear
_nodes
313 with
open(sys
.argv
[1], "r") as fd
:
318 ("lsp_processq_add", "work_queue_add"): [
321 "lsp_processq_complete",
324 ("mq_add_handler", "work_queue_add"): ["meta_queue_process",],
325 ("meta_queue_process", "work_queue_add"): ["meta_queue_process",],
326 # bgpd - label pool WQ
327 ("bgp_lp_get", "work_queue_add"): ["lp_cbq_docallback",],
328 ("bgp_lp_event_chunk", "work_queue_add"): ["lp_cbq_docallback",],
329 ("bgp_lp_event_zebra_up", "work_queue_add"): ["lp_cbq_docallback",],
331 ("bgp_process", "work_queue_add"): ["bgp_process_wq", "bgp_processq_del",],
332 ("bgp_add_eoiu_mark", "work_queue_add"): ["bgp_process_wq", "bgp_processq_del",],
334 ("bgp_clear_route_table", "work_queue_add"): [
335 "bgp_clear_route_node",
336 "bgp_clear_node_queue_del",
337 "bgp_clear_node_complete",
340 ("rfapi_close", "work_queue_add"): ["rfapi_deferred_close_workfunc",],
341 ("rfapiRibUpdatePendingNode", "work_queue_add"): [
342 "rfapiRibDoQueuedCallback",
343 "rfapiRibQueueItemDelete",
348 for func
, fdata
in data
["functions"].items():
349 func
= nameclean(func
)
350 fnode
= FunctionNode
.get(func
).define(fdata
)
352 for call
in fdata
["calls"]:
353 if call
.get("type") in [None, "unnamed", "thread_sched"]:
354 if call
.get("target") is None:
356 tgt
= nameclean(call
["target"])
357 fnode
.add_call(FunctionNode
.get(tgt
), call
)
358 for fptr
in call
.get("funcptrs", []):
359 fnode
.add_call(FunctionNode
.get(nameclean(fptr
)), call
)
360 if tgt
== "work_queue_add":
361 if (func
, tgt
) not in extra_info
:
363 "%s:%d:%s(): work_queue_add() not handled\n"
364 % (call
["filename"], call
["line"], func
)
368 attrs
.update({"is_external": False, "type": "workqueue"})
369 for dst
in extra_info
[func
, tgt
]:
370 fnode
.add_call(FunctionNode
.get(dst
), call
)
371 elif call
["type"] == "install_element":
372 vty_node
= FunctionNode
.get("VTY_NODE_%d" % call
["vty_node"])
373 vty_node
.add_call(FunctionNode
.get(nameclean(call
["target"])), call
)
374 elif call
["type"] == "hook":
375 # TODO: edges for hooks from data['hooks']
378 n
= FunctionNode
.funcs
380 # fix some very low end functions cycling back very far to the top
382 n
["peer_free"].unlink(n
["bgp_timer_set"])
383 n
["peer_free"].unlink(n
["bgp_addpath_set_peer_type"])
384 if "bgp_path_info_extra_free" in n
:
385 n
["bgp_path_info_extra_free"].rank
= 0
388 n
["zlog_ref"].rank
= 0
389 if "mt_checkalloc" in n
:
390 n
["mt_checkalloc"].rank
= 0
392 queue
= list(FunctionNode
.funcs
.values())
393 queue
= calc_rank(queue
, 1)
394 queue
= calc_rank(queue
, -1)
396 sys
.stderr
.write("%d functions in cyclic set\n" % len(queue
))
404 sys
.stderr
.write("%d groups after automerge\n" % len(graph
._groups
))
408 return n
.startswith("rfapi") or n
.startswith("vnc") or ("_vnc_" in n
)
411 _vncstyle
= ',fillcolor="#ffffcc",style=filled'
412 cyclic_set_names
= set([fn
.name
for fn
in graph
.values()])
414 for i
, group
in enumerate(graph
._groups
):
417 gv_nodes
.append("\tsubgraph cluster_%d {" % i
)
418 gv_nodes
.append("\t\tcolor=blue;")
420 has_cycle_callers
= set(gn
.calld()) - group
422 set([edge
.i
.name
for edge
in gn
._fn
.inb
]) - cyclic_set_names
429 if has_cycle_callers
:
430 style
+= ",color=blue,penwidth=3"
432 style
+= ',fillcolor="#ffeebb",style=filled'
433 etext
+= '<br/><font point-size="10">(%d other callers)</font>' % (
438 '\t\t"%s" [shape=box,label=<%s%s>%s];'
439 % (gn
.name
, "<br/>".join([fn
.name
for fn
in gn
._fns
]), etext
, style
)
441 gv_nodes
.append("\t}")
445 set([edge
.i
.name
for edge
in gn
._fn
.inb
]) - cyclic_set_names
453 style
+= ',fillcolor="#ffeebb",style=filled'
454 etext
+= '<br/><font point-size="10">(%d other callers)</font>' % (
458 '\t"%s" [shape=box,label=<%s%s>%s];'
459 % (gn
.name
, "<br/>".join([fn
.name
for fn
in gn
._fns
]), etext
, style
)
463 for gn
in graph
.values():
464 for calls
in gn
.calls():
465 if gn
._group
== calls
._group
:
467 '\t"%s" -> "%s" [color="#55aa55",style=dashed];' % (gn
.name
, calls
.name
)
472 if len(nn
._group
) > 1:
473 return "cluster_%d" % nn
._group
.num
477 tup
= xname(gn
), calls
.name
478 if tup
[0] != tup
[1] and tup
not in edges
:
479 gv_edges
.append('\t"%s" -> "%s" [weight=0.0,w=0.0,color=blue];' % tup
)
482 with
open(sys
.argv
[2], "w") as fd
:
485 node [fontsize=13,fontname="Fira Sans"];
488 % "\n".join(gv_nodes
+ [""] + gv_edges
)