Create Your Own CLR Profiler in C#

Before we go any further, I’d like to clarify something: It is not feasible to create a high-performance live CLR profiler in C#, because of the way the core profiling API and the CLR works. It’s unmanaged C++ all the way down. If you are interested in knowing more about how to build one, here are two excellent sources:

However, it is feasible to build a CLR profiler in C# that can be used during development and testing for things such as memory and code analysis on servers where Visual Studio is not available.

That’s all been made possible because of the excellent work done by the Microsoft Research team that built PEX (and Moles) a couple of years ago. In order for PEX to work its magic — and if you don’t know what that is, you should follow the link above and also check out the Code Digger VS Extension — the team needed to be able to instrument every single bit of code produced by the C# compiler. And to do that, they implemented the C++ profiling interfaces and wrapped it all up in the publicly available Microsoft.ExtendedReflection.dll.

The Microsoft.ExtendedReflection.dll was primarily for PEX to use, but a little bit of documentation was released along with a few samples. In order to get to that, you will have to download and install the original PEX and then go to this folder:

C:\Program Files\Microsoft Pex\Documentation\pex.samples\samples\ExtendedReflection

The “Tracing” sample contains a ready-made implementation of a CLR profiler in C# that you can extend to fit your purposes. Here are a few code snippets from the initialization:

var application = args[0]; // For instance, ConsoleApplication1.exe

...

var startInfo = new ProcessStartInfo(application, null);
startInfo.UseShellExecute = false;
var env = new StringDictionary();
SetMonitoringEnvironment(userAssembly, userType, env);
foreach(DictionaryEntry de in env)
{
  startInfo.EnvironmentVariables[(string)de.Key] = (string)de.Value;
}

using (var process = Process.Start(startInfo))
{
  process.WaitForExit();
  exitCode = process.ExitCode;
}

...

ControllerSetUp.SetMonitoringEnvironmentVariables(
	environmentVariables,
	MonitorInstrumentationFlags.EnterLeaveMethod |
	MonitorInstrumentationFlags.UnwindMethod,// |
	//MonitorInstrumentationFlags.MethodArguments |
	//MonitorInstrumentationFlags.MethodReturnArguments,
	false,
	userAssembly.Location,
	userType.FullName,
	new string[] { Metadata<Program>.Assembly.Location }, // substitutions assemblies
	new string[] { typeof(System.Diagnostics.Debug).FullName }, // types to monitor
	null, // types to exclude to monitor
	new string[] { "ConsoleApplication1" }, // namespaces to monitor
	null, // namespace to exclude to monitor
	new string[] { "ConsoleApplication1" }, // assemblies to monitor
	new string[] {
		Metadata<Object>.Assembly.ShortName,
		Metadata<_ThreadContext>.Assembly.ShortName,
		userAssembly.ShortName },
	null, // types to project
	null, null, null, null, null,
	@"c:\pex\bin\debuginstr.txt", // log file name
	false, // crash on failure
	null, // target clr version
	true, // protect all .cctors
	false, // disable mscorlib supressions
	ProfilerInteraction.Fail, // allow loading external profiler
	null // additional instrument attribute full name
	);

From there, you can implement any or all of the callbacks defined in the documentation. For instance:

public override EnterMethodFlags EnterMethod(Method method)
{
  // Do whatever you want here.

  sw.WriteLine("level {1} <enter> {0}", method.FullName, level);
  this.indent(+1);

  return EnterMethodFlags.NeedArguments;
}

That’s pretty clever. So now you can for instance start counting which methods get called the most across any namespace you want to monitor — which by the way is one of the typical performance killers for developers who are fond of Linq without actually understanding how it works under the covers.

Have fun profiling!