Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create a call graph for a file with clang

Is there a way to create a call graph with clang that can reasonably fit on a page?

i.e. given:

#include<iostream>
using namespace std;
int main()
{
    int a;
    cin>>a;
    cout<<a;
    cout<<a;
    return 0;
}

I current get enter image description here

by using:

$ clang++ main.cpp -S -emit-llvm -o - |
opt -analyze -std-link-opts -dot-callgraph
$ cat callgraph.dot | c++filt |
sed 's,>,\\>,g; s,-\\>,->,g; s,<,\\<,g' |
gawk '/external node/{id=$1}$1!=id' | dot -Tpng -ocallgraph.png

(which seems like a lot of effort to do something that I would not have expected to be so difficult). I would like to get something that is a bit more reasonable on the horizontal axis. Unflatten seems not to have any impact (at least on this file, on other files it seems to have minimal effect).

Is there a way to ensure that the png file that is generated can comfortably fit on a page (any standard size)?

Note: Code for the above taken from Generate calling graph for C++ code

Update: Setting page="8.5,11" gives the following:

enter image description here

like image 800
soandos Avatar asked Mar 17 '13 08:03

soandos


1 Answers

I think the first thing to do would be to set the direction of the graph from the default bottom-to-top ranking to left-to-right, by inserting:

rankdir=LR;

... near the top of the .dot file, after the first {. That should orient the graph left-to-right and thereby make it much more compact for a case like this that has long node labels. Exactly how this could be done would depend on the format of callgraph.dot but, assuming it looks something like this:

digraph G {
    node [shape=rectangle];
    ...

... then something like:

sed 's/digraph G {/digraph G { \n rankdir=LR;/'

... would do the job.

Another approach that I've taken in the past is to insert dummy nodes into edges to reduce the number of nodes that have the same rank (and would therefore be drawn in the same row (with rankdir=TB, which is the default) or column (with rankdir=LR). This is straightforward to do when writing .dot files by hand but harder to script.

If you wanted to script inserting extra nodes in some edges to spread nodes that would normally be at the same rank over several ranks, you could do this by running dot -Tplain to output a plain text file* that includes (among other things) a list of nodes with the X and Y co-ordinates of the center of each node. Then, you could use gawk to read that list, find any large group of nodes with the same X co-ordinate (if rankdir=TB) or Y co-ordinate (if rankdir=LR) and then process the original .dot file to insert an extra blank node before (say) half of the nodes in that group so that the group was spread over two ranks rather than one. I haven't had occasion to do this myself, though.

*See Emden Gansner, Eleftherios Koutsofios and Stephen North (2006) Drawing graphs with dot, Appendix B.

EDIT: How to automatically insert extra nodes.

Given a .dot file test1.dot as follows:

digraph G {
    n1 -> n20 
    n1 -> n21 
    n1 -> n22 
    n20 -> n3 
    n21 -> n3 
    n22 -> n3
}

... which produces the graph shown.

enter image description here

... running dot -Tplain test1.dot >test1.plain gives the file test1.plain:

graph 1 2.75 2.5
node n1 1.375 2.25 0.75 0.5 n1 solid ellipse black lightgrey
node n20 0.375 1.25 0.75 0.5 n20 solid ellipse black lightgrey
node n21 1.375 1.25 0.75 0.5 n21 solid ellipse black lightgrey
node n22 2.375 1.25 0.75 0.5 n22 solid ellipse black lightgrey
node n3 1.375 0.25 0.75 0.5 n3 solid ellipse black lightgrey
edge n1 n20 4 1.1726 2.0394 1.0313 1.9019 0.83995 1.7159 0.68013 1.5605 solid black
edge n1 n21 4 1.375 1.9958 1.375 1.8886 1.375 1.7599 1.375 1.6405 solid black
edge n1 n22 4 1.5774 2.0394 1.7187 1.9019 1.9101 1.7159 2.0699 1.5605 solid black
edge n20 n3 4 0.57736 1.0394 0.71875 0.90191 0.91005 0.71592 1.0699 0.56054 solid black
edge n21 n3 4 1.375 0.99579 1.375 0.88865 1.375 0.7599 1.375 0.64045 solid black
edge n22 n3 4 2.1726 1.0394 2.0313 0.90191 1.8399 0.71592 1.6801 0.56054 solid black
stop

Thus, we can now process the two files together. I'll use Python for this because it is a bit easier to do it in Python than in Awk. For the sake of this example, I've limited the number of nodes in a rank to 2 and I've used the rank as defined by the default bottom-to-top ordering rather than the left-to-right ordering that I've suggested above. I don't know exactly what sort of .dot file is being output by clang, so there may be a need to modify this example a bit to take that into account.

import sys,re;

plain = open(sys.argv[2])
nodesInRank = {}
for line in plain:
    x = line.split()
    rankloc = 3   # rank is in the y column for the vertical case. 
                  # Change this to rankloc = 2 for the horizontal case
    if len(x) > 0 and x[0] == "node":
        nodesInRank[x[rankloc]] = nodesInRank.get(x[rankloc],[]) + [x[1]]

maxNodesInRank = 2
dummies = set()
for n in nodesInRank.values():
    if len(n) > maxNodesInRank:
        dummies = dummies | set(n[:len(n)//2])

dot = open(sys.argv[1])
for line in dot:
    line = line.rstrip()
    line2 = ""
    for d in dummies:
        m = "-> +%s" % (d)
        if re.search(m,line):
            line = re.sub(m,"-> dummy_%s [dir = none]\n dummy_%s -> %s" % (d,d,d),line)
            line2 = '\tdummy_%s [shape=none, width=0, height=0, label=""];' % (d)
    print (line)
    if len(line2) > 0:
        print (line2)

Given this Python script, which I've called breakrank.py, I can now run it as:

python breakrank.py test1.dot test1.plain >test_dummy.dot

... which puts the following in test_dummy.dot:

digraph G {
    n1 -> dummy_n20 [dir = none]
 dummy_n20 -> n20
    dummy_n20 [shape=none, width=0, height=0, label=""];
    n1 -> n21
    n1 -> n22
    n20 -> n3
    n21 -> n3
    n22 -> n3
}

If we run this through dot, we now get:

enter image description here

... which gives us what we want, I think.

like image 122
Simon Avatar answered Sep 28 '22 08:09

Simon