How does a .NET profiler work?

gabbersepp

Josef Biehler

Posted on May 2, 2020

How does a .NET profiler work?

Get the code for this tutorial

Introduction

In the last article we built a small runnable example. To start just use the template we created there. If you don't have it yet, visit the article and download the base profiler project.

Attention! Most of the functions that are used to query information from the profiler return a HRESULT. You should check if this value is 0 (error) or not. For the sake of simplicity I will omit all those checks because they make the code harder to understand. This counts for all following blog posts about this topic.

Profiler loading

As mentioned in the first post, the profiler is loaded along with the application. If the profiler is called, it is called by the application's thread. If multiple threads are running in your app and every thread triggers events, all those events arrive "at the same time" at your profiler in different threads. So you have to ensure that your profiler is threadsafe.

ICorProfilerCallback

Our profiler must implement that interface. Do you remember all those stubs we created? That are callbacks called by the CLR on certain events.

ICorProfilerInfo

The profiler should request an instance of type ICorProfilerInfo. This object is a bridge between the profiler and the profiled app. You can request the function name for a given function id for example. We will see some of it's power in the next articles.

Lifecycle

Well there are two functions that can somehow be identified as Lifecyclefunctions.

  • Initialize() is called after the CLR has initialized and loaded up the profiler. This is a very important point in time because it is the only place where you can setup the profiler.
  • Shutdown() is called when the application gets closed

Initialize

We will focus on Initialize because as already said, you must setup your profiler.
What you must do:

  • request an instance of type ICorProfilerInfo (or any newer version - you know, the numeric suffix)
  • set flags to tell the CLR which events you want to receive
  • maybe set some hooks (we will see this later)

Necessary steps:

  • add include <corprof.h>
  • add CComQIPtr<ICorProfilerInfo2> iCorProfilerInfo; variable to the outside of the class.
  • add pICorProfilerInfoUnk->QueryInterface(IID_ICorProfilerInfo2, (LPVOID*)&iCorProfilerInfo); to Initialize()

The include is required because it contains the definition of IID_ICorProfilerInfo2. The iCorProfilerInfo variable should not be an instance variable. This makes it much easier to handle. We need it later on to request metadata about the application.

Flags:

ICorProfilerInfo has a method SetEventMask(). You must pass a DWORD that reflects the events you want to be notified. A full list of all flags can be found here. In this example I choose COR_PRF_MONITOR_EXCEPTIONS because I want to monitor all exceptions that occur:

  iCorProfilerInfo->SetEventMask(COR_PRF_MONITOR_EXCEPTIONS);
Enter fullscreen mode Exit fullscreen mode

The function now looks like this:

CComQIPtr<ICorProfilerInfo2> iCorProfilerInfo;
HRESULT __stdcall ProfilerCallback::Initialize(IUnknown* pICorProfilerInfoUnk)
{
  pICorProfilerInfoUnk->QueryInterface(IID_ICorProfilerInfo2, (LPVOID*)&iCorProfilerInfo);
  iCorProfilerInfo->SetEventMask(COR_PRF_MONITOR_EXCEPTIONS);

  return S_OK;
}
Enter fullscreen mode Exit fullscreen mode

Depending on the flags you set you can use different callback methods (that are all those stubs we created in ProfilerCallback.cpp). In order to know which one you need, go the the documentation and search for COR_PRF_MONITOR_EXCEPTIONS:

ExceptionThrown Callback

We focus on the ExceptionThrown callback and fill it with some stupid logic:

HRESULT __stdcall ProfilerCallback::ExceptionThrown(ObjectID thrownObjectID)
{
  cout << "from profiler: \t\t\texception thrown\r\n";
  return S_OK;
}
Enter fullscreen mode Exit fullscreen mode

Adjust the Program.cs of the test app and throw an exception:

static void Main(string[] args)
{
  try
  {
    throw new Exception();
  }
  catch
  {

  }

  Console.Read();
}
Enter fullscreen mode Exit fullscreen mode

Now compile everything and execute start.bat:

Congratulations, you created your first profiler!

Threading

Let's see in which thread a profiler is executed. We keep the code from the sections above but adjust the ExceptionThrown callback a bit. I suggest to print the current thread id. This can be done by including <thread> and adjusting the message:

// ./code/DevToNetProfiler/DevToNetProfiler/ProfilerCallback.cpp#L31-L35

HRESULT __stdcall ProfilerCallback::ExceptionThrown(ObjectID thrownObjectID)
{
  cout << "from profiler: \t\t\texception thrown in thread " << this_thread::get_id() << "\r\n";
  return S_OK;
}
Enter fullscreen mode Exit fullscreen mode

Also you should print the thread id in your C# app:

// ./code/DevToNetProfiler/TestApp/Program.cs#L8-L21

static void Main(string[] args)
{
  try
  {
    throw new Exception();
  }
  catch
  {

  }

  Console.WriteLine($"from app:\tthread id: {AppDomain.GetCurrentThreadId()}");
  Console.Read();
}
Enter fullscreen mode Exit fullscreen mode

And this should be the result:

Both components have the same thread id.

Bitness

As always when dealing with native code, you must take the bitness into consideration. This is especially true if you want to use FunctionEnter/Leave callbacks.
The simple rule is: If the .NET app runs as 32Bit app, your profiler must be compiled as 32Bit profiler and vice-versa.

.NET and bitness

Read this article, if you want to read more about bitness in .NET and Visual Studio:

What happens if we ignore the bitness?

Let the profiler being compiled to 32Bit and set the TestApp to 64Bit. The message from the profiler is missing now:

Additionally we can find an error message in the windows event viewer:

Summary

This article showed the basics of a profiler. I think this is enough to get an insight. Do you miss some information that seems relevant to you? Let me know!

In the next articles I want to focus on some use cases. Stay tuned if you want to see some assembler code!


Found a typo?

As I am not a native English speaker, it is very likely that you will find an error. In this case, feel free to create a pull request here: https://github.com/gabbersepp/dev.to-posts . Also please open a PR for all other kind of errors.

Do not worry about merge conflicts. I will resolve them on my own.

💖 💪 🙅 🚩
gabbersepp
Josef Biehler

Posted on May 2, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related