In Part I we saw how CPU profilers work, in Part II we learned how to measure execution time of programs using CPU time and Wall time and in Part III we learned how to use Ruby's TracePoint API to trace method calls and returns in Ruby VM. We also started building our DiyProf gem. It's recommended to read those first before you continue. In this part we're going to create a simple call graph visualization using the data we collected using the TracePoint API.
Part IV. Call Graphs
In Part III, we used Ruby's TracePoint API to print method call and return traces. It'd be more useful if we can parse these traces and create a call graph out of it. A call graph is basically a visual representation of the method calls, in the form of a tree. It looks like this :
This graph tells us that outer_method
calls 3 methods from inside it:
inner_method{1,2,3}
and inner_method_3
calls another method named
inner_most_method
.
Graphviz and the DOT language
Graphviz is a graph visualization tool which can take a graph description in text format and generate diagrams as PDF and other formats. For describing graphs in text format, Graphviz uses a language called DOT, which is a simple declarative way of describing nodes and connections between them.
Before proceeding, make sure you've installed graphviz and the dot
command
is available in your $PATH.
Follow their official documentation for
installation steps.
Now let's take an example.
digraph graph_name {
a -> b -> c;
b -> d;
}
Save the above lines as sample.dot and run the following command:
The above command will generate a directed graph that looks like this:
There's another way to represent graphs, by separating nodes and connections like so:
digraph graph_name {
<nodes>
<connections>
}
So the following DOT notation represents the same graph:
digraph graph_name {
a;
b;
c;
d;
a -> b;
b -> c;
b -> d;
}
It's also possible to add additional labels to nodes and connections.
digraph graph_name {
a [label="a \n foo: bar"];
b [label="b \n foo: baz"]
a -> b [label="xyz"];
}
Above DOT code represents the following graph:
Call Graph with Graphviz
We're going to create a reporter class for our DiyProf gem which will keep collecting all method calls and create a DOT file that represents the call graph.
We'll add a record
method under the DotReporter class which pushes method infos into a stack
when calls happen and pop them out of the stack when the methods return.
When the method info is popped from the stack, the time it spent in the stack
is recorded(which is the execution time) and it adds the method's call info(method name and time)
to a call tree. See the code below:
add_method_to_call_tree
adds nodes and connections for the added method.
Here's the code that does that:
Now that we have the nodes and connections in @methods
and @calls
,
it'll be trivial to generate a DOT file that looks like this:
digraph G {
<graph_nodes>
<graph_links>
}
Here's the code that will walk through @methods
and @calls
and prepare
such a DOT file:
Check out the lib/diy_prof/dot_reporter.rb
file on the github repo
to see how all of this will fit together. We also need some changes in the
Tracer class we wrote in Part III to make use of a custom reporter class:
Now that our tracer is able to generate a DOT formatted report, we can use it on our sample program we wrote in the previous parts:
We should get a call graph that looks like this:
Congratulations if you got this far! Check out the github repo which has the complete code if you got stuck somewhere.
Bonus!
Wouldn't it be cool if we could vary the size of the nodes based on self or total
time of the method?
The DOT syntax accepts a bunch of attributes for nodes and edges.
Looks like we can use size
and fontsize
attributes to vary the size of the
nodes and the text inside the nodes. I'll leave it to you to think of how to use
these to make the most expensive methods stand out in the call graph.
Recap
We've learned about Graphviz and the DOT language and how to use them to create call graph diagrams out of the data collected by Ruby Tracepoint API. In Part V, we'll start building a sampling profiler for Ruby, in particular we'll learn some basics about interrupts and signal handling in C. Thanks for reading! If you would like to get updates about subsequent blog posts in this DIY CPU profiler series, do follows us on twitter @codemancershq.