Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to build a graph of specific function calls?

I have a project where I want to dynamically build a graph of specific function calls. For example if I have 2 template classes, A and B, where A have a tracked method (saved as graph node) and B has 3 methods (non-tracked method, tracked method and a tracked method which calls A's tracked method), then I want to be able to only register the tracked method calls into the graph object as nodes. The graph object could be a singleton.

template <class TA>
class A
{
public:
    void runTracked()
    {
        // do stuff
    }
};

template <class TB>
class B
{
public:
    void runNonTracked()
    {
        // do stuff
    }

    void runTracked()
    {
        // do stuff
    }

    void callATracked()
    {
        auto a = A<TB>();
        a.runTracked();
        // do stuff
    }
};

void root()
{
    auto b1 = B<int>();
    auto b2 = B<double>();
    b1.runTracked();
    b2.runNonTracked();
    b2.callATracked();
    
}

int main()
{
    auto b = B<int>();
    b.runTracked()
    root();
    return 0;
}

This should output a similar graph object to the below:

root()
\-- B<int>::runTracked()
\-- B<double>::callATracked()
    \-- A<double>::runTracked()

The tracked functions should be adjustable. If the root would be adjustable (as in the above example) that would be the best. Is there an easy way to achieve this?

I was thinking about introducing a macro for the tracked functionalities and a Singleton graph object which would register the tracked functions as nodes. However, I'm not sure how to determine which is the last tracked function in the callstack, or (from the graphs perspective) which graph node should be the parent when I want to add a new node.

like image 200
Gábor Pálovics Avatar asked Aug 13 '21 08:08

Gábor Pálovics


People also ask

What is a function call graph?

Function call graphs, or call trees, are a standard tool often found in reverse engineering tools, code browsers, IDEs, and even file editors. They provide fundamental information about the control flow dependencies for the functions you're examining in your tool.

How do you extract a call from a graph?

On the bottom right, select the "Call graph" tab. This shows an interactive call graph that correlates to performance metrics in other windows as you click the functions. To export the graph, right click it and select "Export Graph".

What is a call graph in C?

A call graph (also known as a call multigraph) is a control-flow graph, which represents calling relationships between subroutines in a computer program. Each node represents a procedure and each edge (f, g) indicates that procedure f calls procedure g. Thus, a cycle in the graph indicates recursive procedure calls.


1 Answers

In general, you have 2 strategies:

  1. Instrument your application with some sort of logging/tracing framework, and then try to replicate some sort of tracing mixin-like functionality to apply global/local tracing depending on which parts of code you apply the mixins.

  2. Recompile your code with some sort of tracing instrumentation feature enabled for your compiler or runtime, and then use the associated tracing compiler/runtime-specific tools/frameworks to transform/sift through the data.

For 1, this will require you to manually insert more code or something like _penter/_pexit for MSVC manually or create some sort of ScopedLogger that would (hopefully!) log async to some external file/stream/process. This is not necessarily a bad thing, as having a separate process control the trace tracking would probably be better in the case where the traced process crashes. Regardless, you'd probably have to refactor your code since C++ does not have great first-class support for metaprogramming to refactor/instrument code at a module/global level. However, this is not an uncommon pattern anyways for larger applications; for example, AWS X-Ray is an example of a commercial tracing service (though, typically, I believe it fits the use case of tracing network calls and RPC calls rather than in-process function calls).

For 2, you can try something like utrace or something compiler-specific: MSVC has various tools like Performance Explorer, LLVM has XRay, GCC has gprof. You essentially compile in a sort of "debug++" mode or there is some special OS/hardware/compiler magic to automatically insert tracing instructions or markers that help the runtime trace your desired code. These tracing-enabled programs/runtimes typically emit to some sort of unique tracing format that must then be read by a unique tracing format reader.

Finally, to dynamically build the graph in memory is a a similar story. Like the tracing strategies above, there are a variety of application and runtime-level libraries to help trace your code that you can interact with programmatically. Even the simplest version of creating ScopedTracer objects that log to a tracing file can then be fitted with a consumer thread that owns and updates the trace graph with whatever desired latency and data durability requirements you have.

like image 143
CinchBlue Avatar answered Oct 19 '22 09:10

CinchBlue