Software7

Personal Developer Notebook

Access to Mono’s Low Level Profiling Functions in Unity

Unity has already quite useful profiling functions, but certain information are still difficult to gather. I once had to determine the cause of a memory leak problem in a Unity project not written by me. That was kind of hard to find. Should I encounter such a problem again it would be nice to have a tool-based approach.

Inspired by a recent twitter discussion it occurred to me that one could possibly hook into the low level profile interface of Mono.

One can take a look at Mono’s profiler header here:

profiler.h

The code (and documentation^^) for the profiler can be found here:

profiler.c

Probably the only way to determine, if that’s feasible at all, is to quickly create a minimal plugin.

So can we call these methods in the Mono runtime of Unity?

Here’s a quick and dirty attempt:

#define WIN32_LEAN_AND_MEAN
#define EXPORT_API __declspec(dllexport)

#include <stdio.h>
#include <Windows.h>

//
//  MonoProfilePlugin.c
//
//  Created by stephan / Software 7 GmbH on 21.05.13 - 17:14
//  Only a quick & dirty hack
//  use at your own risk
//

static HMODULE monoLibraryHandle;

//did I already mention that it's a quick and dirty hack? 
struct _MonoMethod {
    short flags;  
    short iflags; 
    int token;
    char *klassDummy;
    char *signatureDummy;
    const char *name;
    //some fields were omitted
};

typedef struct _MonoMethod MonoMethod;
        
typedef enum {
    MONO_PROFILE_NONE = 0,
    MONO_PROFILE_APPDOMAIN_EVENTS = 1 << 0,
    MONO_PROFILE_ASSEMBLY_EVENTS  = 1 << 1,
    MONO_PROFILE_MODULE_EVENTS    = 1 << 2,
    MONO_PROFILE_CLASS_EVENTS     = 1 << 3,
    MONO_PROFILE_JIT_COMPILATION  = 1 << 4,
    MONO_PROFILE_INLINING         = 1 << 5,
    MONO_PROFILE_EXCEPTIONS       = 1 << 6,
    MONO_PROFILE_ALLOCATIONS      = 1 << 7,
    MONO_PROFILE_GC               = 1 << 8,
    MONO_PROFILE_THREADS          = 1 << 9,
    MONO_PROFILE_REMOTING         = 1 << 10,
    MONO_PROFILE_TRANSITIONS      = 1 << 11,
    MONO_PROFILE_ENTER_LEAVE      = 1 << 12,
    MONO_PROFILE_COVERAGE         = 1 << 13,
    MONO_PROFILE_INS_COVERAGE     = 1 << 14,
    MONO_PROFILE_STATISTICAL      = 1 << 15,
    MONO_PROFILE_METHOD_EVENTS    = 1 << 16,
    MONO_PROFILE_MONITOR_EVENTS   = 1 << 17,
    MONO_PROFILE_IOMAP_EVENTS     = 1 << 18, 
    MONO_PROFILE_GC_MOVES         = 1 << 19,
    MONO_PROFILE_GC_ROOTS         = 1 << 20
} MonoProfileFlags;


typedef enum
{
    MONO_GC_EVENT_START,
    MONO_GC_EVENT_MARK_START,
    MONO_GC_EVENT_MARK_END,
    MONO_GC_EVENT_RECLAIM_START,
    MONO_GC_EVENT_RECLAIM_END,
    MONO_GC_EVENT_END,
    MONO_GC_EVENT_PRE_STOP_WORLD,
    MONO_GC_EVENT_POST_STOP_WORLD,
    MONO_GC_EVENT_PRE_START_WORLD,
    MONO_GC_EVENT_POST_START_WORLD
} MonoGCEvent;

struct _MonoProfiler {
    char dummy[10];
};
        
typedef struct _MonoProfiler MonoProfiler;

static FILE *fp;
static MonoProfiler _monoProfiler;
static BOOLEAN gc_eventCalledAtLeastOnce = FALSE;
static BOOLEAN gc_resizeCalledAtLeastOnce = FALSE;
static BOOLEAN sample_method_enterCalledAtLeastOnce = FALSE;
static BOOLEAN sample_method_leaveCalledAtLeastOnce = FALSE;

typedef void (*MonoProfileFunc) (MonoProfiler *prof);
typedef void (*MonoProfileMethodFunc) (MonoProfiler *prof, MonoMethod* method);
typedef void (*MonoProfileGCFunc) (MonoProfiler *prof, MonoGCEvent event, int generation);
typedef void (*MonoProfileGCResizeFunc)   (MonoProfiler *prof, __int64 new_size);

typedef void (*mono_profiler_installFunc) (MonoProfiler *prof, MonoProfileFunc shutdown_callback);
typedef void (*mono_profiler_install_enter_leaveFunc) (MonoProfileMethodFunc enterCallback, MonoProfileMethodFunc leaveCallback);
typedef void (*mono_profiler_set_eventsFunc) (MonoProfileFlags events);
typedef void (*mono_profiler_install_gcFunc) (MonoProfileGCFunc callback, MonoProfileGCResizeFunc heap_resize_callback);

static mono_profiler_installFunc mono_profiler_install;
static mono_profiler_install_enter_leaveFunc mono_profiler_install_enter_leave;
static mono_profiler_set_eventsFunc mono_profiler_set_events;
static mono_profiler_install_gcFunc mono_profiler_install_gc;
    
static void debug_out(const char *txt)
{
    if(fp != NULL) {
        fprintf(fp, "%s\n", txt);
        fflush(fp);
    }
}

static void profiler_shutdown (MonoProfiler *prof)
{
    debug_out("profiler_shutdown");        
}

static void gc_event(MonoProfiler *profiler, MonoGCEvent event, int generation)
{
    if(!gc_eventCalledAtLeastOnce) {
        debug_out("gc_event");
        gc_eventCalledAtLeastOnce = TRUE;
    }

}

static void gc_resize (MonoProfiler *profiler, __int64 new_size)
{
    if(!gc_resizeCalledAtLeastOnce) {
        debug_out("gc_resize");
        gc_resizeCalledAtLeastOnce = TRUE;
    }
}

static void sample_method_enter (MonoProfiler *prof, MonoMethod *method)
{
    if(!sample_method_enterCalledAtLeastOnce) {
        debug_out("sample_method_enter");
        debug_out(method->name);
        sample_method_enterCalledAtLeastOnce = TRUE;
    }
}
        
static void sample_method_leave (MonoProfiler *prof, MonoMethod *method)
{
    if(!sample_method_leaveCalledAtLeastOnce) {
        debug_out("sample_method_leave");
        debug_out(method->name);
        sample_method_leaveCalledAtLeastOnce = TRUE;
    }        
}

void EXPORT_API InitializeProfiler()
{
    fopen_s(&fp, "C:\\Users\\stephan\\tmp\\mytestlog.txt", "w");
        
    debug_out("InitializeProfiler");
        
    monoLibraryHandle = LoadLibrary(L"C:\\Program Files (x86)\\Unity\\Editor\\Data\\Mono\\EmbedRuntime\\mono.dll");
    if(monoLibraryHandle) { 
        mono_profiler_install = (mono_profiler_installFunc)GetProcAddress(monoLibraryHandle, "mono_profiler_install");
        mono_profiler_install_gc = (mono_profiler_install_gcFunc)GetProcAddress(monoLibraryHandle, "mono_profiler_install_gc");
        mono_profiler_install_enter_leave = (mono_profiler_install_enter_leaveFunc)GetProcAddress(monoLibraryHandle, "mono_profiler_install_enter_leave");
        mono_profiler_set_events = (mono_profiler_set_eventsFunc)GetProcAddress(monoLibraryHandle, "mono_profiler_set_events");
        debug_out("after GetProcAddress");

        mono_profiler_install (&_monoProfiler, profiler_shutdown);
        debug_out("after mono_profiler_install");

        mono_profiler_install_gc (gc_event, gc_resize);
        debug_out("after mono_profiler_install_gc");
        
        mono_profiler_install_enter_leave (sample_method_enter, sample_method_leave);
        debug_out("after mono_profiler_install_enter_leave");

        mono_profiler_set_events((MonoProfileFlags)(MONO_PROFILE_ENTER_LEAVE | MONO_PROFILE_GC));
        debug_out("after mono_profiler_set_events");
    }
    else {
        debug_out("Loading mono.dll failed!");          
    }
}

Calling the plugin from Unity:

using UnityEngine;
using System.Collections;
using System.Runtime.InteropServices;

public class MyTest : MonoBehaviour
{
    [DllImport ("MonoProfilePlugin")]
    private static extern void InitializeProfiler();

    void Start ()
    {
        InitializeProfiler();
        //we need to call a method to see whether sample_method_enter/-leave works
        Debug.Log(AddIntegers(1, 2));
    }

    void Update ()
    {
    }

    private int AddIntegers(int a, int b)
    {
        return a + b;
    }
}

Now it gets interesting.

What will we find in mytestlog.txt?

This is the result:

InitializeProfiler
after GetProcAddress
after mono_profiler_install
after mono_profiler_install_gc
after mono_profiler_install_enter_leave
after mono_profiler_set_events
sample_method_enter
AddIntegers
sample_method_leave
AddIntegers
gc_event
profiler_shutdown

Wooh. This means it works. We successfully hooked into Mono’s profiling.

Next step would be to collect all the allocation data together with the corresponding stack traces and add some reporting.

Unfortunately currently I don’t have time for this. Perhaps I can later work on it. Or perhaps someone wants to develop this further? (HINT, HINT, HINT) 🙂