How to build a graph of specific function calls?
Asked Answered
J

1

6

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.

Jambeau answered 13/8, 2021 at 8:29 Comment(15)
To clarify: You want to build a graph of function calls? Something like a (filtered) tree view of “stack traces”?Sanderling
@Sanderling exactly what you are saying, but it's not just the graphics I want as an output, but rather the graph object.Steeplechase
So you want a dynamic graph object. What I don't understand, and what cannot be deduced looking on the “output”, is the relation between a call stack (which is actually a stack) and a tree graph. I guess that you want a graph (that's a tree) of all the (tracked) calls that are caused by a specific call (I think root() in your case)?Sanderling
Yes, but only include specific elements.Steeplechase
I think a look into Log4j could help, this allows dynamic configuration of logging activity. Even if this is not what you want, the concept there could be help you to understand your problem. BTW, is using templates part of the problem or part of the solution?Sanderling
only include specific elements -- that's what you mean by adjustable. The code you show does not switch at runtime between tracked and untracked calls.Sanderling
Another question: how dynamic should the configuration be? Should it be done at runtime, maybe via random?Sanderling
Please check if the changes I made to title, text, and tags meet your intent. Maybe you should try to further explain the terms adjustable and dynamic.Sanderling
@Wolf, 1. "BTW, is using templates part of the problem or part of the solution?" Part of the problem. It's necessary for the solution to work with template classes/functions properly. 2. "only include specific elements -- that's what you mean by adjustable." Indeed. 3. "how dynamic should the configuration be? Should it be done at runtime, maybe via random?" I'm not sure if I understand this question.Steeplechase
I wrote random, but this could have been input as well. This is to understand what you mean by dynamic: should the tracking status really be adjusted at runtime?Sanderling
I'm not sure if I understand your question correctly. What do you mean by tracking status?Steeplechase
By "tracking status" I meant the configuration value that controls whether a specific function should be tracked or not. Should this value be changeable at runtime or is there a fresh built needed. If the value can be adjusted at runtime: is it read just once, at the beginning of the session (maybe from a config file), or should the value be adjustable even between calls?Sanderling
No, the config shouldn't be modified during a run. The config file solution sounds appropriate.Steeplechase
Here is another issue: Did you think about cases where a parent node (caller) is hidden when a child now (called) should show up? A regular tree would not allow showing branches/leaves in hidden branches.Sanderling
You might be interested in en.cppreference.com/w/cpp/utility/basic_stacktrace, but only if you have access to a prototype C++23 compiler... Still good to know it's coming.Dziggetai
E
2

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

Euton answered 30/8, 2021 at 9:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.