New information has been added to this article since publication.
Refer to the Editor's Update below.

CLR Profiler

No Code Can Hide from the Profiling API in the .NET Framework 2.0

Jay Hilyard

This article is based on a prerelease version of the .NET Framework 2.0, formerly code-named "Whidbey." All information contained herein is subject to change.

This article discusses:

  • Looking into the .NET runtime
  • More detailed function tracing
  • Better thread and call stack information
  • The phase-out of in-process debugging
This article uses the following technologies:
The .NET Framework

Code download available at:CLRProfiler.exe(307 KB)

Contents

So What's New?
What's Gone?
Obtaining Thread Information
Stack 'Em Up
Enhanced Tracing
Describing and Decoding Function Arguments
Conclusion

Since the inception of .NET, the common language runtime (CLR) profiling API has been the mechanism to use to inspect what the runtime is doing under the covers. Many profilers simply report how much time is spent in a given routine, file, or class, but the profiling API is much more complete in the amount and types of data it makes available. Information about the application domains, assemblies, and classes that are loaded and used in a process, just-in-time (JIT) compiler notifications, memory usage tracking, tracing of events, exception tracking, managed to unmanaged code transitions, and the state of the runtime are all categories of information available to a profiler written for the .NET Framework 1.0 and 1.1 using the profiling API.

You will find a nicely enhanced profiling API in the .NET Framework 2.0 Beta 1. In this article, I will discuss why the changes were made and will describe how developers can take advantage of them.

If you have not worked with the profiling API before, let me explain its capabilities before I start discussing the new enhancements. First and foremost, using the profiling API requires an unmanaged COM server DLL that implements the ICorProfilerCallback interface. In addition, you need to set up two environment variables (COR_PROFILER and COR_ENABLE_PROFILING) to let the CLR know that it should load your DLL as a profiler. COR_PROFILER is set to the class ID (CLSID) of your COM server and COR_ENABLE_PROFILING is set to 1.

Once the CLR has created an instance of your profiler, the first notification you will receive on the ICorProfilerCallback interface is the Initialize notification. This gives you the opportunity to tell the CLR what types of notifications you would like to receive by calling ICorProfilerInfo::SetEventMask with a combination of flags that are defined by the COR_PRF_MONITOR enumeration. ICorProfilerInfo allows you to ask the CLR about the currently running program and act accordingly. The flags in COR_PRF_MONITOR are enabled at the very beginning of the program, though they can be turned on and off by calling SetEventMask again later in the program execution with any new flags you want.

Another thing you can do in the original profiling API is set up callbacks using the ICorProfilerInfo::SetEnterLeaveFunctionHooks call. This allows you to provide callback functions to the CLR that will be called every time a function is entered or exited, or when a tailcall occurs (these do not return). When the program terminates, you receive a shutdown notification from the CLR, at which point you have a chance to perform final cleanup of any resources in use. The original profiling API allowed many different ways of exploring your program as it ran, but as with all things, there was room for improvement.

For more background on the profiling API, see my overview of it in the January 2003 issue of MSDN®Magazine, available at Inspect and Optimize Your Program's Memory Usage with the .NET Profiler API, as well as Aleksandr Mikunov's article on MSIL code rewriting using the profiling API, available in the September 2003 issue of MSDN Magazine at Rewrite MSIL Code on the Fly with the .NET Framework Profiling API .

So What's New?

The profiling team at Microsoft listened to developers and added a number of cool capabilities in 2.0. New things you can do with the profiling API include tracking the name and application domain of a managed thread, obtaining function arguments and return values without using in-process debugging, inspecting these arguments for values, and getting a stack trace quickly.

Much of this functionality is exposed through two new interfaces: ICorProfilerCallback2 and ICorProfilerInfo2. The ICorProfilerCallback2 interface inherits from the original ICorProfilerCallback interface and adds a new method for tracking thread name changes as well as eight new methods for garbage collection notifications. This is discussed in the section on thread information a bit later in this article. The ICorProfilerInfo2 interface inherits from ICorProfilerInfo and adds 21 new methods for controlling interactions with the runtime.

[Editor's Update - 1/9/2006: The tables in the figures have been updated to reflect changes made for the release of the .NET Framework 2.0.]

Figure 1 New COR_PRF_MONITOR Flags

Flag Value
COR_PRF_ENABLE_FUNCTION_ARGS 0x02000000
COR_PRF_ENABLE_FUNCTION_RETVAL 0x04000000
COR_PRF_ENABLE_FRAME_INFO 0x08000000
COR_PRF_ENABLE_STACK_SNAPSHOT 0x10000000
COR_PRF_USE_PROFILE_IMAGES 0x20000000

However, all of these flags have also been added to the COR_PRF_IMMUTABLE enumeration value, which means that they can be set only with the ICorProfilerCallback::Initialize method. Any attempt to reset them at a later point by calling ICorProilerInfo::SetEventMask again will fail. COR_PRF_IMMUTABLE consists of the following COR_PRF_ MONITOR flags ORed together, as shown in Figure 2.

Figure 2 COR_PRF_IMMUTABLE Flags

COR_PRF_MONITOR_CODE_TRANSITIONS COR_PRF_MONITOR_REMOTING COR_PRF_MONITOR_REMOTING_COOKIE COR_PRF_MONITOR_REMOTING_ASYNC COR_PRF_MONITOR_GC COR_PRF_ENABLE_REJIT COR_PRF_ENABLE_INPROC_DEBUGGING COR_PRF_ENABLE_JIT_MAPS COR_PRF_DISABLE_OPTIMIZATIONS COR_PRF_DISABLE_INLINING COR_PRF_ENABLE_OBJECT_ALLOCATED COR_PRF_ENABLE_FUNCTION_ARGS COR_PRF_ENABLE_FUNCTION_RETVAL COR_PRF_ENABLE_FRAME_INFO COR_PRF_ENABLE_STACK_SNAPSHOT COR_PRF_USE_PROFILE_IMAGES

COR_PRF_ENABLE_FUNCTION_ARGS turns on two specific capabilities. The first is the ability to get function arguments in callbacks to FunctionEnter2 and FunctionTailcall2 (both new for the .NET Framework 2.0). The second is the ability to use the ICorProfilerInfo2::GetFunctionInfo2 function to get exact Class IDs for generic functions. If this flag is not used and you register for the FunctionEnter2 callback, the COR_PRF_FUNCTION_ARGUMENT_INFO* and COR_PRF_FRAME_INFO parameters to these functions will always be NULL. COR_PRF_ ENABLE_FUNCTION_RETVAL turns on the ability to track return values through the FunctionLeave2 callback via the COR_PRF_FUNCTION_ARGUMENT_RANGE* parameter. This parameter will always be NULL if this flag is not set in ICorProfilerInfo::SetEventMask.

Let's quickly touch on what is no longer available from the original profiling API. Then, we will explore how to use the powerful new functionality of Visual Studio®.

What's Gone?

The main feature being phased out with this release of the profiling API is the concept of in-process debugging. This was the original method used by profiler writers to examine values of function calls and return values as well as program state at run time. This was accomplished through four ICorProfilerInfo API methods (shown in Figure 3), and an interface called ICorDebug. ICorDebug is no longer used in conjunction with the profiling API.

Figure 3 Retired Profiling APIs

ICorProfilerInfo Interface Method Description
BeginInprocDebugging Indicates that a profiler wants to start using in-process debugging
GetInprocInspectionInterface Used to get an ICorDebug interface for full stack traces and other process information
GetInprocInspectionThisThread Used to get an ICorThread interface for thread-specific stack information
EndInprocDebugging Indicates that a profiler wants to stop using in-proc debugging

One of the downfalls of in-process debugging was its inability to correctly and reliably deliver method parameter and return values during FunctionEnter, FunctionLeave, and FunctionTailcall callbacks. This was due to the relatively complex nature of the interactions between ICorDebug and the JIT compiler in-process. Happily for us, the Microsoft profiling API team has replaced this with a better-performing version of the same functionality originally envisioned for ICorDebug.

Obtaining Thread Information

The flag previously needed to get threading information in a profiler was COR_PRF_MONITOR_THREADS, a value set during the call to SetEventMask. In the new version of the Profiling API, you can use the same flag, but you will get more information if you have implemented ICorProfilerCallback2. ThreadNameChanged is a new method on ICorProfilerCallback2:

HRESULT ICorProfilerCallback2::ThreadNameChanged( [in] ThreadID threadId, // ID of thread whose name was changed [in] ULONG cchName, // number of characters in new name [in] WCHAR name[]) // the new name of the thread

ThreadNameChanged is called when the friendly name is set on a .NET thread by assigning System.Threading.Thread.Name to a new value. Initially, .NET threads are not named, but if you are doing any sort of serious multithreading, I suggest naming them; this can prove an invaluable aid when debugging thread issues. It is much easier to manage threads named "GUI" and "Worker" than "0x98745837" and "0x22345873".

Another newly exposed piece of thread information is the AppDomain in which a thread is currently executing. This is retrieved with a call to ICorProfilerInfo2::GetThreadAppDomain:

HRESULT ICorProfilerInfo2::GetThreadAppDomain ( [in] ThreadID threadId, // ID of the thread whose AppDomain you require [out] AppDomainID *pAppDomainId) // AppDomainID for the requested thread

GetThreadAppDomain will allow you to get the ID for the current AppDomain associated with a given runtime thread. This may be able to help you solve bugs where threads are attempting to access AppDomain-specific resources when the thread is no longer in that AppDomain.

Stack 'Em Up

[**Editor's Update - 1/9/2006:**In order to use stack snapshots, ICorProfilerInfo2::SetEventMask must include the new COR_PRF_ENABLE_STATIC_SNAPSHOT flag.] To start the snapshot of the current thread's stack, call ICorProfilerInfo2::DoStackSnapshot, like so:

HRESULT ICorProfilerInfo2::DoStackSnapshot ( [in] ThreadID thread, // thread for which snapshot should be taken [in] StackSnapshotCallback *callback, // callback for each frame [in] ULONG32 infoFlags, // COR_PRF_SNAPSHOT_INFO flags to control // the degree of information for each snapshot clientData) // caller-specified data passed to each callback

[Editor's Update - 1/9/2006: Many of the ICorProfilerInfo2 methods described in this article, including DoStackSnapshot, were updated for the final release of the .NET Framework 2.0. Please see the documentation at ICorProfilerInfo2 for updated signatures and information.]

DoStackSnapshot will perform a stack snapshot of the current thread. During the snapshot, the callback function whose signature is defined by StackSnapshotCallback is called once per frame of the managed stack and once at the beginning of an unmanaged chain. A profiler can do unmanaged stack walking from this point if it is tracking unmanaged code in addition to managed code, or it can ignore the unmanaged stack markers and just trace the managed frames of the stack. The definition of the StackSnapshotCallback function looks like this:

HRESULT StackSnapshotCallback( [in] FunctionID funcId, // function ID for reported frame; 0 for unmanaged [in] UINT_PTR ip, // instruction pointer for next instruction in frame [in] COR_PRF_FRAME_INFO frameInfo, // opaque value used to get frame info [in] ULONG32 contextSize, // size of buffer in context parameter [in] BYTE [] context, // register context of the frame; can be NULL [in] void* clientData),// data passed via DoStackSnapshot call

StackSnapshotCallback allows the recipient to examine the frame of the stack that the snapshot is currently on. This includes getting info available via the COR_PRF_FRAME_INFO value, seeing the register information, and reading any supporting data passed in from the original caller of DoStackSnapshot in the clientData field.

The opaque value COR_PRF_FRAME_INFO is defined as shown in the following line of code:

typedef UINT_PTR COR_PRF_FRAME_INFO

COR_PRF_FRAME_INFO is used to represent a stack frame and act as a token for methods on ICorProfilerInfo2. This allows ICorProfilerInfo2 to retrieve parameter and return value information.

The COR_PRF_SNAPSHOT_INFO enumeration contains the values to pass as the infoFlags parameter value in DoStackSnapshot. These values are listed in Figure 4.

Figure 4 COR_PRF_SNAPSHOT_INFO Enumeration Values

Enumeration Value Description
COR_PRF_SNAPSHOT_DEFAULT This will return only the default set of stack information that is comprised of the FunctionID for the frame method, the IP (instruction pointer), and the COR_PRF_FRAME_INFO
COR_PRF_SNAPSHOT_ This will allow the unmanaged
REGISTER_CONTEXT register contexts for unmanaged frames to be passed in the context field of the StackSnapshotCallback

Using DoStackSnapshot every time you need a stack is fine as long as you don't need stacks too often. It also has the added bonus of making it relatively easy to interleave unmanaged stack frames; if you need full stacks of both managed and unmanaged code ranges, you must use DoStackSnapshot because it provides the unmanaged frame starting points. If you are only interested in managed stack walking, then you can use the enhanced tracing capabilities of the new FunctionEnter2, FunctionLeave2, and FunctionTailcall2 tracing functions. These three functions allow you to monitor the progression of your code and build a "shadow stack." If you often need stack traces, building a shadow stack is less expensive and, as an added benefit, it provides you with generic type parameter information that the DoStackSnapshot method will never give to a profiler.

Enhanced Tracing

Function tracing is a core part of any profiler, as it allows you to see the entry and exit for each function in the application. In the 1.0 and 1.1 versions of the Profiling API, this was accomplished by setting callback functions on the ICorProfilerInfo interface through the SetEnterLeaveFunctionHooks method. The method allowed a profiler to sign up for three different types of function-tracing callbacks: entering a function (FunctionEnter), leaving a function (FunctionLeave), and performing a tailcall (FunctionTailcall). A tailcall occurs when the last action of a method is a call to another method.

What's interesting here is that the stack never records the call to the first method, only the second. These callbacks would execute for every function when the type of call or return occurred in the CLR, so you could get a real picture of where your application was doing its work. Each callback was passed a function ID, so a profiler could identify which function was being entered or exited; this function ID can be resolved by using the metadata API that is also provided for unmanaged access.

This was adequate for profilers who simply cared whether a function was entered or exited. However, to find out which parameters the function was called with or what the return value was on exit, the profiler was supposed to use the in-process debugging interface. As I mentioned earlier in this article, in-process debugging was not optimal for this purpose as there was no way for a profiler to detect these values.

Now we come to .NET Framework 2.0, where the profiling API team has liberated us from the use of ICorDebug for in-process debugging. They did this by adding a new method to the ICorProfilerInfo2 interface called SetEnterLeaveFunctionHooks2 as shown in the following:

HRESULT ICorProfilerInfo2::SetEnterLeaveFunctionHooks2 ( // ptr to FunctionEnter2 callback [in] FunctionEnter2 *pFuncEnter, // ptr to FunctionLeave2 callback [in] FunctionLeave2 *pFuncLeave, // ptr to FunctionTailcall2 callback [in] FunctionTailcall2 *pFuncTailcall)

To make the callback occur, the COR_PRF_MONITOR flag COR_PRF_MONITOR_ENTERLEAVE must be set using ICorProfilerInfo2::SetEventMask. To get the new functionality of being able to inspect the arguments of a function, the profiler must set the new flag, COR_PRF_ENABLE_FUNCTION_ARGS. And to get the return values, COR_PRF_ENABLE_FUNCTION_ RETVAL must be set as well.

If you have done work with the profiling API in the past, you will have noticed that the calling convention for the original FunctionEnter, FunctionLeave, and FunctionTailcall are _declspec(naked). This means that the prologue and epilogue for the function are not set up for the profiler by the compiler. The profiler needed to do a bit of inline assembly language to set up the prologue at the beginning of the function and the epilogue at the end of the function. The new callbacks for FunctionEnter2, FunctionLeave2, and FunctionTailcall2 use the _stdcall calling convention where none of this is necessary.

[Editor's Update - 3/25/2006: This is incorrect. For the .NET Framework 2.0, the new callbacks for FunctionEnter2, FunctionLeave2, and FunctionTailcall2 should still in general be implemented as __declspec(naked).]

To register for the new function tracing callbacks, the profiling API provides the callback implementations for FunctionEnter2, FunctionLeave2, and FunctionTailcall2, as shown here:

typedef void FunctionEnter2 ( [in] FunctionID funcId, // FunctionID for the function being entered [in] COR_PRF_FRAME_INFO func, // opaque value only valid during callback [in] COR_PRF_FUNCTION_ARGUMENT_INFO *argumentInfo) // function args info

COR_PRF_ENABLE_FUNCTION_ARGS is not set in the event mask and will always be NULL.

FunctionEnter2 is called at the entering of almost every method (except tailcalls) during the execution of the program. This occurs after SetEnterLeaveFunctionHooks2 is called with the following callback pointer set:

typedef void FunctionLeave2 ( [in] FunctionID funcId, // FunctionID for function being left [in] COR_PRF_FRAME_INFO func, // opaque value only valid during callback [in] COR_PRF_FUNCTION_ARGUMENT_RANGE *retvalRange) // info about return // values from function

COR_PRF_ENABLE_FUNCTION_RETVAL is not set in the event mask and will always be NULL.

FunctionLeave2 is called for the exiting of every method during the execution of the app. Again, this occurs after SetEnterLeaveFunctionHooks2 is called with this callback pointer set:

typedef void FunctionTailcall2 ( [in] FunctionID funcId, // FunctionID for the function [in] COR_PRF_FRAME_INFO func) // opaque value only valid during callback

Describing and Decoding Function Arguments

Now that you have callbacks for the function at the point it is being entered and exited, you can examine arguments and return values. The Profiling API exposes these values as regions in memory. You can use a function's metadata token to get the types of the parameters you are dealing with, which will allow you to decode them. The .NET Framework keeps track of items by tagging them with values from the CorElementType enumeration. Figure 5 lists all of these values (entries in red are new enumeration values added in the .NET Framework 2.0). CorElementTypes with a value above 0x22 (ELEMENT_TYPE_ MAX) are special modifiers used to describe types in different circumstances. In the arguments and return values, you will see the basic .NET types, represented by the CorElementType enumeration. These are shown in Figure 6.

Figure 6 Argument and Return Value Common Types

ELEMENT_TYPE Representation
ELEMENT_TYPE <= R8, I, U Primitive values
ELEMENT_TYPE_VALUETYPE Use the ICorProfilerInfo2::GetClassLayout method to resolve layout
ELEMENT_TYPE_ (CLASS, STRING, OBJECT, ARRAY, GENERICINST, SZARRAY) Object ID (pointer into garbage collector heap)
ELEMENT_TYPE_BYREF Managed pointer (to the stack or garbage collector heap)
ELEMENT_TYPE_PTR Unmanaged pointer (not movable by the garbage collector)
ELEMENT_TYPE_FNPTR Pointer-sized opaque value
ELEMENT_TYPE_TYPEDBYREF Managed pointer, followed by a pointer-sized opaque value

Figure 5 CorElementType Enumeration, Values, and Description

Enumeration Value Value (Hex) Description
ELEMENT_TYPE_END 0x0 Undefined constant
ELEMENT_TYPE_VOID 0x1 Void return type
ELEMENT_TYPE_BOOLEAN 0x2 Boolean value
ELEMENT_TYPE_CHAR 0x3 WCHAR
ELEMENT_TYPE_I1 0x4 Short
ELEMENT_TYPE_U1 0x5 Unsigned short
ELEMENT_TYPE_I2 0x6 Int
ELEMENT_TYPE_U2 0x7 Unsigned int
ELEMENT_TYPE_I4 0x8 Long
ELEMENT_TYPE_U4 0x9 Unsigned long
ELEMENT_TYPE_I8 0xA __int64
ELEMENT_TYPE_U8 0xB Unsigned __int64
ELEMENT_TYPE_R4 0xC Float
ELEMENT_TYPE_R8 0xD Double
ELEMENT_TYPE_STRING 0xE String object
ELEMENT_TYPE_PTR 0xF Unmanaged pointer
ELEMENT_TYPE_BYREF 0x10 Managed pointer
ELEMENT_TYPE_VALUETYPE 0x11 Value type
ELEMENT_TYPE_CLASS 0x12 Specific class type
ELEMENT_TYPE_VAR 0x13 A generic class type variable (for example, MyClass <T> and <T> gets this CorElementType)
ELEMENT_TYPE_ARRAY 0x14 Multidimensional array
ELEMENT_TYPE_GENERICINST 0x15 Generic instantiated type
ELEMENT_TYPE_TYPEDBYREF 0x16 Managed pointer, followed by an ELEMENT_TYPE_FNPTR
ELEMENT_TYPE_I 0x18 Platform-independent int
ELEMENT_TYPE_U 0x19 Platform-independent unsigned int
ELEMENT_TYPE_FNPTR 0x1B Function pointer with the complete signature and calling convention
ELEMENT_TYPE_OBJECT 0x1C System.Object
ELEMENT_TYPE_SZARRAY 0x1D Single-dimensional array
ELEMENT_TYPE_MVAR 0x1E Generic method type variable (for example, void MyMethod<T> () and <T> gets this CorElementType)
ELEMENT_TYPE_CMOD_REQD 0x1F Binding flag for required C modifier
ELEMENT_TYPE_CMOD_OPT 0x20 Binding flag for optional C modifier
ELEMENT_TYPE_INTERNAL 0x21 Internally generated signature
ELEMENT_TYPE_MAX 0x22 Maximum of base types, all past this point are modifiers to the preceding base types
ELEMENT_TYPE_MODIFIER 0x40 Modifier flag
ELEMENT_TYPE_SENTINEL 0x01 | 0x40 Indicates the last argument in a varargs function
ELEMENT_TYPE_PINNED 0x05 | 0x40 This is pinned
ELEMENT_TYPE_R4_HFA 0x06 | 0x40 Internal CLR usage
ELEMENT_TYPE_R8_HFA 0x07 | 0x40 Internal CLR usage

The arguments for a FunctionEnter2 call or a FunctionTailcall2 call are passed in the COR_PRF_FUNCTION_ARGUMENT_ INFO* argumentInfo parameter. COR_PRF_FUNCTION_ ARGUMENT_INFO is a struct that's defined like this:

typedef struct _COR_PRF_FUNCTION_ARGUMENT_INFO { ULONG numRanges; // number of argument ranges ULONG totalArgumentSize; // total size of arguments COR_PRF_FUNCTION_ARGUMENT_RANGE ranges[ 1 ]; // array of ranges } COR_PRF_FUNCTION_ARGUMENT_INFO;

The ranges field is defined as a set of structures defined as COR_PRF_FUNCTION_ARGUMENT_RANGE and shown here:

typedef struct _COR_PRF_FUNCTION_ARGUMENT_RANGE { UINT_PTR startAddress; // starting memory address of first argument ULONG length; // length of this range of memory } COR_PRF_FUNCTION_ARGUMENT_RANGE;

If a profiler wants to reference any of this information later, it needs to copy it, since these structures will not be valid once you leave the FunctionEnter2 callback. COR_PRF_FUNCTION_ARGUMENT_INFO is essentially a map to the function argument values (for Value Types) or Object IDs (for Reference Types). If the arguments are contiguous in memory in left-to-right order, then one COR_PRF_FUNCTION_ARGUMENT_RANGE structure will be used to represent them. This range is pulled apart by looking at the function signature which can be retrieved from the metadata using the unmanaged metadata API, and then using that knowledge to walk along the memory range and get each argument's value or Object ID.

For a FunctionLeave2 call, only the return values are provided in a range specified by a COR_PRF_FUNCTION_ARGUMENT_RANGE structure. For items such as ref and out parameters, you need to save the memory address for the argument in the call to FunctionEnter2 and then check it in the Leave callback.

Now that you have the values and/or object IDs for the arguments and return values, what do you do with them? It's easy to print out a simple integer value, but what if the value is an object ID? Help is needed to decode what type of object you are dealing with in order to access the values.

A whole new set of functions was added to the ICorProfilerInfo2 interface to help you retrieve the information for these parameters and more. This set includes the following new functions added to the ICorProfilerInfo2 interface:

  • GetFunctionInfo2
  • GetStringLayout
  • GetClassLayout
  • GetClassIDInfo2
  • GetClassFromTokenAndTypeArgs
  • GetFunctionFromTokenAndTypeArgs
  • GetArrayObjectInfo
  • GetBoxClassLayout

Let's start by figuring out some things about the function you are being called back about. In the previous versions of the profiling API, there was a function called ICorProfilerInfo::GetFunctionInfo. GetFunctionInfo let you use the Function ID passed in to the callback to determine a function's class and module; it even got the function's metadata token. In the grand tradition of API versioning, Microsoft now gives us the fabulously exciting method name ICorProfilerInfo2::GetFunctionInfo2.

What new and wondrous things can now be accessed? Well let's take a look at the definition for GetFunctionInfo2:

HRESULT GetFunctionInfo2 ( [in] COR_PRF_FRAME_INFO frameInfo, // frame information for callback [out] ClassID *pClassId, // class ID for function; can be NULL [out] ModuleID *pModuleId, // module ID for function; can be NULL [out] mdToken *pToken, // metadata token for function; can be NULL [in] ULONG32 cTypeArgs, // number of elements in the type arg array [out] ULONG32 *pcTypeArgs, // number of elements needed to hold type // arguments to the function; can be NULL [out] ClassID typeArgs[]) // caller-allocated buffer to accept the // the function's type arguments; can be NULL

If you are trying to examine different parts of the function signature, GetFunctionInfo2 allows you to get the class the function is part of, the module it belongs to, and the metadata token. (The latter can be used to find all sorts of information with the metadata API, but that's a topic for another time.)

You now have the capability to look at the type parameters on a generic function. All of the arguments with TypeArgs in the name help to describe the classes of the type arguments for this instantiation of this function. For example, consider a generic function defined like this:

public class Printer { public void Print <T>(T item) { // do some printing generically } }

You can find out the type of T when this function was instantiated at run time for this callback. That's not all, though. Due to the way the CLR stores different types of items that can be arguments (strings, arrays, boxed items, and so on), a profiler needs a few helper methods to pluck the appropriate value from the correct place in memory. This is because the layout in memory of this data is not guaranteed by the CLR, since it could change from platform to platform and processor architecture to processor architecture.

The ICorProfilerInfo2::GetStringLayout function is provided to analyze string information:

HRESULT GetStringLayout ( [out] ULONG *pBufferLengthOffset, // offset to DWORD containing buffer len [out] ULONG *pStringLengthOffset, // offset to DWORD containing string len [out] ULONG *pBufferOffset) // offset to the raw string buffer

Since all strings in .NET are Unicode-based, the buffer for the string actually contains wide characters (WCHAR). This must be taken into account when you process the string after retrieving it.

Not all parameters are value types. If the profiler is to make sense of reference type parameters, it needs to walk the layout of a class or structure in memory to find the individual member values. To help in this value quest, find the ICorProfilerInfo2::GetClassLayout method, as shown here:

HRESULT GetClassLayout ( [in] ClassID classID, // class ID for which the layout is requested [in, out] COR_FIELD_OFFSET rFieldOffset[], // caller allocated layout // of fields in the class [in] ULONG cFieldOffset, // number of COR_FIELD_OFFSET elements in // the caller allocated buffer in rFieldOffset [out] ULONG *pcFieldOffset, // actual number of COR_FIELD_OFFSET elements [out] ULONG *pulClassSize); // size in bytes of the class

There is one COR_FIELD_OFFSET structure for each field in the class, with COR_FIELD_OFFSET defined as:

typedef struct _COR_FIELD_OFFSET { mdFieldDef ridOfField; // fieldDef metadata token for this field ULONG ulOffset; // offset of field from beginning of object } COR_FIELD_OFFSET;

To get at the field values using the information returned from GetClassLayout, you need to add the ulOffset field's COR_FIELD_OFFSET value to the object ID of the field. You will then have the address in memory where this field's value resides. Note that fields that are reference types have values that can be used as object IDs for further inspection using GetClassLayout.

To get information about the parent module that a class belongs to, as well as the metadata token for the class, a profiler used to call ICorProfilerInfo::GetClassIDInfo. Now, to support generics properly, it calls ICorProfilerInfo2::GetClassIDInfo2:

HRESULT GetClassIDInfo2( [in] ClassID classId, // class ID for which information is required [out] ModuleID *pModuleId, // module to which this class belongs [out] mdTypeDef *pTypeDefToken, // metadata token for the class [in] ULONG32 cNumTypeArgs, // number of ClassID slots in typeArgs [out] ULONG32 *pcNumTypeArgs, // actual number of type arguments [out] ClassID typeArgs[]).. // type argument Class IDs for this class

GetClassIDInfo2 generalizes GetClassIDInfo for all types, generic and non-generic, as demonstrated by the ability to get the type parameters for a class. If you call GetClassIDInfo with zero specified for the cNumTypeArgs, but provide a pointer to a ULONG32 to return the correct number in, you can figure out how much space you need. Then, you can allocate the array of class IDs to pass in typeArgs and make the actual call with the correctly-sized array.

Now that you know how to get the metadata token and type arguments for a class from a class ID, it only seems fair to show you how to round-trip and get a class ID for a metadata token and some type arguments. This is just what the GetClassFromTokenAndTypeArgs is used for, as shown in this code:

HRESULT GetClassFromTokenAndTypeArgs( [in] ModuleID moduleID, // module to which the class belongs [in] mdTypeDef typeDef, // metadata token for the class [in] ULONG32 cTypeArgs, // number of ClassID slots allocated in typeArgs [in, size_is(cTypeArgs)] ClassID typeArgs[], // argument Class IDs [out] ClassID* pClassID) // pointer which receives the Class ID

To do the same thing for a function, let's turn again to the longer-named methods on the ICorProfilerInfo2 interface and find GetFunctionFromTokenAndTypeArgs:

HRESULT GetFunctionFromTokenAndTypeArgs( [in] ModuleID moduleID, // module to which function belongs [in] mdTypeDef funcDef, // metadata token for the function [in] ClassID classId, // class ID for the class containing the function [in] ULONG32 cTypeArgs, // number of slots in typeArgs [in, size_is(cTypeArgs)] ClassID typeArgs[], // argument Class IDs [out] FunctionID* pFunctionID) // pointer which receives the Function ID

There you have it—a way to go back and forth with both class IDs and function IDs.

Figure 7 .NET Arrays

Figure 7** .NET Arrays **

What else can the new profiling API do? Well, it helps a profiler discover how to access elements in various types of arrays. Three types of arrays are available in the .NET Framework: single-dimensional arrays, traditional multidimensional arrays, and jagged arrays (a multidimensional array where all rows can have a different number of columns). Single-dimensional and multidimensional are the actual array constructs in the CLR, and jagged arrays are implemented using single-dimensional arrays of single-dimensional arrays. See Figure 7 for a graphic representation of these array types.

To assist a profiler in distinguishing the array types and accessing the correct element values, the profiling API provides two functions: ICorProfilerInfo::IsArrayClass and ICorProfilerInfo2::GetArrayObjectInfo. GetArrayObjectInfo helps a profiler determine the structural information about an array, such as the total number of elements and the dimension size and lower bound. IsArrayClass aids a profiler in identifying the type of the array, its elements, and the array rank (number of dimensions). First, let's look at IsArrayClass:

HRESULT IsArrayClass ( [in] ClassID classID, // class ID for class in which we're interested [out] CorElementType *pBaseElemType, // describes array element type [out] ClassID *pBaseClassId, // class ID for array element type [out] ULONG *pcRank) // number of array dimensions, known as rank

CorElementType is one of the basic enumerations in the unmanaged APIs for .NET that represents all of the types of things that can be encountered. It is similar to the VARIANT VARTYPE definitions from COM, but is not an exact match because .NET specifications differ from COM. Figure 5 shows CorElementType and its values. In IsArrayClass, CorElementType is used for the pBaseElemType parameter.

The first parameter is classId, which is the class we are examining to see if it is an array. The second parameter is pBaseElemType, which indicates the CorElementType of the elements in the array. Next is the pBaseClassId parameter, which gives us the actual class ID for all of the elements in the array. To determine the number of dimensions in our array, we look at the pcRank parameter, which allows us to see how the array is constructed.

Now that we know how many dimensions we are dealing with (from the pcRank parameter), we can use this information to help us set up our parameters for GetArrayObjectInfo. GetArrayObjectInfo looks like this:

HRESULT GetArrayObjectInfo( [in] ObjectID objectId, // object ID for array to examine [in] ULONG32 cDimensionSizes, // length of sizes and lower bounds arrays [out, size_is(cDimensionSizes), length_is(cDimensionSizes)] ULONG32 pDimensionSizes[], // sizes of each requested dimension [out, size_is(cDimensionSizes), length_is(cDimensionSizes)] int pDimensionLowerBounds[], // lower bound of each requested dimension [out] BYTE **ppData); // pointer to the raw buffer of array data

Once GetArrayObjectInfo is called, the profiler has the size of each dimension (pDimensionSizes), the lower bound of each dimension (pDimensionLowerBounds), and the offset in memory where the array values start (ppData). With this information, and using the element type from IsArrayClass, the profiler can now walk along the array and examine the values for each element. Once each value is found, it can be examined using the techniques discussed earlier to determine if more drilling is necessary, or if the value is in fact right at the element location.

The final piece of layout information that a profiler needs to decode the parameter and return value information is how to handle boxed values. A value is boxed when a value type is passed to a parameter that requires a reference type. These values are treated as a reference type, where the object reference points to a box on the GC heap which contains the value type. To see where the actual values are located within the box, we use the ICorProfilerInfo2::GetBoxClassInfo method:

HRESULT GetBoxClassLayout( [in] ClassID classId, // class ID for the boxed type [out] ULONG32 *pBufferOffset) // offset to the value in the box

GetBoxClassLayout returns information about how to find the value for a boxed type. If you pass a Class ID for a class that cannot be boxed, you will get a failed HRESULT return code. The offset received for boxed items in the pBufferOffset parameter is offset from the Object ID of the boxed item.

As you can see, examining the values of a running program is a complex activity. But the new Profiling APIs let you do just that.

Conclusion

The Profiling API has been updated substantially to help you deal with generics, get stack traces, see new information about threads, and more closely examine the parameter and return value data. C# and managed code has a lot going for it, but every once in a while, you might want to dust off those C++ skills and get under the hood for a look around.

[Editor's Update - 1/9/2006:

In the final release of the Profiling API for the .NET Framework 2.0, several new items were added. Primarily, these enable inspection in four main areas: static variable evaluation, garbage collection information, exception tracing, and the enumeration of frozen objects.

Static variables are scoped in four ways: RVA, AppDomain, Thread, and Context. By default static variables are scoped by AppDomain, but RVA statics can be created in C++ and MSIL, Thread statics are created using the System.ThreadStaticAttribute, and Context statics are created using the System.ContextStartAttribute. In order to determine which scope a static variable is in, use the ICorProfilerInfo2::GetStaticFieldInfo method to get back a COR_PRF_STATIC_TYPE value that tells in what scope the static resides. Once the scope has been determined, call GetRVAStaticAddress, GetAppDomainStaticAddress, GetThreadStaticAddress, or GetContextStaticAddress to get the address of the value for the static variable.

Methods that deal with gathering garbage collection information have been added to ICorProfilerInfo2 (new callbacks have been added to the ICorProfilerCallback2 interface). For ICorProfilerInfo2, GetGenerationBounds allows the profiler to determine the location in memory of the garbage collector's generations, and GetObjectGeneration allows the profiler to determine the generation and location in the garbage collector for a particular object. The COR_PRF_GC_GENERATION_RANGE structure describes how the generation is laid out in meory. New callbacks on ICorProfilerCallback2 for garbage collection include GarbageCollectionStarted, GarbageCollectionFinished, SurvivingReferences, FinalizeableObjectQueued, RootReferences2, HandleCreated, and HandleDestroyed.

Exception tracing, through the ICorProfilerInfo2::GetNotifiedExceptionClauseInfo and ICorProfilerInfo2::GetCodeInfo2 methods, allows the profiler to determine the IL address for the exception block that handled the exception. The COR_PRF_EX_CLAUSE_INFO value returned from GetNotifiedExceptionClauseInfo provides the information to pass into GetCodeInfo2, which can be used to resolve the location of the given function that scopes the IL address.

Finally, frozen objects are generated at NGEN time. See the System.Runtime.CompilerServices.StringFreezingAttribute documentation in MSDN for an example of how to set up a frozen object. In order to find and inspect them, a profiler uses the ICorProfilerInfo2::EnumModuleFrozenObjects method and the corresponding ICorProfilerObjectEnum interface. ]

Jay Hilyard is a software engineer on the New Product Development team with Newmarket International and co-author of the C# Cookbook (2nd edition) from O'Reilly. When Jay is not busy spending time with his family or watching the Patriots, he can be reached at hilyard@comcast.net.