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
|

Contents
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 th