Obtaining stack traces in bursted code

In our project, we often run into a scenario where, for debugging purposes, it is useful to know what code path is being used to call into a function that is called from many places. I’ve been getting around it by passing in a FixedString which is different for each call site, and logging that along with our diagnostic logs. However, this is tedious and labor-intensive, especially because I remove these logs at the end of a debugging session, and this kind of scenario happens often for us.

It would be convenient to be able to obtain a stacktrace on-demand in the form of a FixedString for debugging purposes, similar to C#'s Environment.StackTrace. Then, I don’t need to manually pass in a context string - I simply obtain a stacktrace, and I can surround that with conditional defines so that it is only enabled during our debugging scenario.

It seems that Unity is already doing this somehow because Leak detection can log full stacktraces, and when crashes occur, symbolicated stacktraces are logged to the Player.log if lib_burst_compiled.pdb is available. However, the internals of how this is handled is not available.

I tried looking into the various methods of obtaining a stacktrace in kernel32.dll. However, per the known issues, it appears that Burst does not support linking this dll: https://docs.unity3d.com/Packages/com.unity.burst@1.8/manual/docs/KnownIssues.html#known-issues-with-dllimport

Is there a good way to achieve what I am looking to do?

I had some time today to mess around with this and I have something working, but it isn’t especially useful because it doesn’t show all the stack frames I would expect.

I built a wrapper dll around code from the StackWalker library: GitHub - JochenKalmbach/StackWalker: Walking the callstack in windows applications

On the Unity side, the functions have the following signature, and are used from bursted code like this:

public unsafe static class BurstStacktrace
{
    [DllImport("GGStackTrace")]
    private static extern void* get_stack(ref int size);

    [DllImport("GGStackTrace")]
    private static extern void free_stack(void* stack);

    [BurstCompile]
    public static FixedString4096 GetStackTrace()
    {
        int size = 0;
        byte* stackPtr = (byte*)get_stack(ref size);

        FixedString4096 fixedStackStr = default;
        fixedStackStr.Append(stackPtr, size);

        free_stack(stackPtr);

        return fixedStackStr;
    }
}

However, I am only getting some of the stack frames that I want:

C:\Users\Ed\source\repos\GGStacktrace\GGStacktrace\StackWalker.cpp (1143): StackWalker::ShowCallstack
C:\Users\Ed\source\repos\GGStacktrace\GGStacktrace\GGStackTrace.cpp (28): get_stack
C:\UnityProjects\-\Assets\GG\GGUtil\Scripts\BurstStacktrace.cs (36): BurstStacktrace.PrintStackTrace
C:\UnityProjects\-\Assets\Scripts\Spells\Impacts\Systems\ImpactJobScheduler.cs (103): ImpactJobScheduler/JobStruct`1/Data<FireImpactSystem/ApplyFireJob>::ImpactJobScheduler.JobStruct`1.Data<FireImpactSystem.ApplyFireJob>.Execute
C:\UnityProjects\-\Assets\Scripts\Spells\Impacts\Systems\ImpactJobScheduler.cs (119): ImpactJobScheduler.JobStruct`1<FireImpactSystem.ApplyFireJob>.Execute

There are stack frames between the impact job scheduler and BurstStackTrace.PrintStackTrace that are missing, namely the job’s execute function and a few bursted function calls in between.

EDIT: After reading over the user manual again, I discovered the “Native Debug Mode Compilation” flag. Upon enabling this flag, I now receive the stacktraces I expect! Exciting. My guess is that the burst compiler is heavily inlining the intermediate function calls, causing those stack frames to get lost unless the debug flag is set.

2 Likes

I once searched for a solution, but only found this thread. Over time, I created a tool that solves this problem for me. Today, I’ve decided to share it as open source.

If anyone needs to save a function call location inside Burst ParallelJobs and doesn’t want to pass huge structures or worry about safety and race conditions, you can take a look at my solution:
Link to Topic (Unity Forum)

It’s very fast, and the variable that points to the call location or call chain is only 4 bytes.
The call chain is also visible in the inspector and the Entity inspector, and it’s fully clickable.

Note: Although, unlike DLL-based solutions, my implementation doesn’t save the entire call chain at once, it’s extremely fast, simple, and stable, even in a build (you can save your project’s call strings even in a standalone build), and it doesn’t create memory allocations.