CLR Diagnostics with ClrMD and ScriptCS REPL - ScriptCS.ClrDiagnostics

WinDbg+SOS  has been used by .NET developers for years. It is a very powerful profiling/analysis tool that unfortunately is quite hard to use and exposes native-only API. By releasing ClrMD  library Microsoft makes CLR heap memory inspection accessible to regular C# developers and enables them to write customized profiling tools. I have created a simple ScriptCS script pack that allows for interactive debugging under REPL. Now we can use both C# and some of SOS features under the same console :). More after the break.

Microsoft.Diagnostics.Runtime

Not that long ago .NET Runtime team announced the beta release of Microsoft.Diagnostics.Runtime component (aka ClrMD).
The package delivers managed API for .NET process and crash dump inspection which is similar to SOS Debugging Extensions. If you are a seasoned Windows programmer you probably have been using WinDbg a couple of times, for example to track and identify logical memory leaks in your applications.
ClrMD simply brings some of that capabilities as an API. That is a big deal for a couple of reasons:

  • Makes memory profiling and process analysis much easier for regular C#/.NET developers,
  • Allows people and businesses to write custom diagnostic tools tailored for their needs.

Remember that ClrMD is still in beta phase and also that attaching and debugging is an invasive process (don't run it on production servers).

For more in depth introduction to the topic please read the original blog post

ScriptCS and REPL

ScriptCS is a cool project started by @gblock and inspired by @filip_woj that uses Roslyn and NuGet to make C# scripting easy (no .csprojs required). It is getting a lot of traction in the community recently - it is definitely one of my favourite initiatives in the C# world, putting Roslyn to a great use.

Just recently Glenn added initial REPL (Read-eval-print loop) support to the project meaning that now you can use C# in an interactive shell, kind of like a JavaScript console.

ClrMD + ScriptCS = ScriptCS.ClrDiagnostics

Based on the above I've created a script pack that brings ClrMD into ScriptCS and is aimed to provide interactive CLR diagnostics environment under ScriptCS REPL.
It is available on GitHub and NuGet as ScriptCS.ClrDiagnostics

Here is how you can use it:

  • Install ScriptCS using nightly builds (this is needed due to a bug in current version that prevents from installing prerelease packages) cinst scriptcs -pre -source https://www.myget.org/F/scriptcsnightly/ Alternatively you can build from source
  • Install ScriptCS.ClrDiagnostics: scriptcs -install ScriptCs.ClrDiagnostics -pre
  • Launch ScriptCS in REPL mode (you may need to get latest version for that): scriptcs.exe
  • You should be able to load the script pack and attach to a process like this:
// Load ClrDiag object
> var c = Require<ClrDiag>();

// Attach to process 
> c.Attach(6152)
Attaching to process PID=6152
Using CLR Version=v4.0.30319.18033 DACFileName=mscordacwks_amd64_Amd64_4.0.30319.18033.dll
Succesfully attached to process PID=6152 Name=WpfApplication2

You can also use the process name:

c.Attach("MyApplication")

Check current state

> c.IsAttached 
True         

> c.Process.WorkingSet64
51265536

ClrMD API is available to use, eg. ClrRuntime

> c.Clr.Threads.Count
2

Script pack also provides some helpers that should make analysis easier:

> c.PrintTypes("System.");
Total size               Count   Name
167.82KB                 398     System.Object[]
143.29KB                 2404    System.String
126.98KB                 2385    System.Delegate[]
126.98KB                 2385    System.Delegate[]

PrintTypes() will simply output all the types on managed heaps sorted by total memory consumed, it lets you optionally filter by type name and limit returned result set.

You can also display each thread's call stack using PrintStackTrace():

> c.PrintStackTrace()                                                                                                                                     
Stacktrace for ThreadId=3200                                                                                                                              
           0       A9E9B8 InlinedCallFrame                                                                                                                
           0       A9E9B8 InlinedCallFrame                                                                                                                
 7F92AF92093       A9E990 DomainBoundILStubClass.IL_STUB_PInvoke(System.Windows.Interop.MSG ByRef, System.Runtime.InteropServices.HandleRef, Int32, Int32)
 7F92AF87640       A9EA60 System.Windows.Threading.Dispatcher.GetMessage(System.Windows.Interop.MSG ByRef, IntPtr, Int32, Int32)                          
 7F92AF85E9E       A9EB20 System.Windows.Threading.Dispatcher.PushFrameImpl(System.Windows.Threading.DispatcherFrame)                                     
 7F9272172DA       A9EBC0 System.Windows.Application.RunInternal(System.Windows.Window)                                                                   
 7F927216BD7       A9EC60 System.Windows.Application.Run()                                                                                                
 7F8E3700107       A9ECA0 WpfApplication2.App.Main()

Or for one thread using index:

> c.PrintStackTrace(0)                                                                                                                                     
Stacktrace for ThreadId=3200                                                                                                                              
           0       A9E9B8 InlinedCallFrame                                                                                                                
           0       A9E9B8 InlinedCallFrame                                                                                                                
 7F92AF92093       A9E990 DomainBoundILStubClass.IL_STUB_PInvoke(System.Windows.Interop.MSG ByRef, System.Runtime.InteropServices.HandleRef, Int32, Int32)

After you are done, detach from the process like this:

> c.Detach()                                                    
Successfully detached from process PID=6152 Name=WpfApplication2

Troubleshooting

ScriptCs.ClrDiagnostics lets you also specify DAC file (mscordacwks.dll) location while attaching:

> c.Attach(6152, @"C:\Windows\Microsoft.NET\Framework64\v4.0.30319\mscordacwks.dll");

If you encounter an error with StackOverflow being thrown in REPL console - it is caused by jsv serialization recently added to ScriptCS. You may need to wait for the fix or manually apply a simple workaround (use simple .ToString() in place of object serialization to jsv).

Summary

The project has been created as an experiment, but it shows how great capabilities ScriptCS REPL has.

Oh, and you can also do that:

> c.Play().ImperialMarch();
comments powered by Disqus