Search code examples
c++c++11templatestraceruntime-configuration

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.


Solution

  • 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.

    Edit: If you would like, OpenTelemetry/Jaeger may be a good place to start visualizing traces once you have extracted the data (and you can also report directly to it if you want), although it prefers a tree presentation format: Jaeger documentation for Trace Detail View