Profiling and Runtime Notification IDs
Runtime notifications supply an ID for reported classes, threads, application domains, and so on. These IDs can be used to query the common language runtime (CLR) for more information. Each ID is the address of a block in memory that describes the item. However, IDs should be treated as opaque handles by profilers. If an invalid ID is used in a call to a profiling function, the results are undefined. Most likely, the result will be an access violation. The profiler has to ensure that the IDs being used are valid. The profiling API does not perform any kind of validation, because that would create overhead and would slow down the execution of the application considerably.
Characteristics of Profiling IDs
The following sections describe the characteristics of IDs in the profiling API.
Uniqueness
A ProcessID is unique system-wide for the lifetime of the process. All other IDs are unique process-wide for the lifetime of the ID.
Hierarchy and Containment
IDs are arranged in a hierarchy that mirrors the hierarchy in the process. Processes contain application domains, which contain assemblies, which contain modules, which contain classes, which contain functions. Threads are contained within processes and may move from application domain to application domain. Objects are mostly contained within application domains, and a very few objects may be members of multiple application domains at the same time. Contexts are contained within processes.
Lifetime and Stability
When a given process, application domain, assembly, thread, or object is destroyed, freed, or released, or it ends, the ID that is associated with it becomes invalid. When a given ID becomes invalid, all the IDs that it contains also become invalid. For example, when an application domain is unloaded, its AppDomainID becomes invalid. The AssemblyIDs, ModuleIDs, ClassIDs, and FunctionIDs corresponding to the assemblies, modules, classes, and functions within the application domain also become invalid at the same time.
The lifetime and stability of specific IDs are as follows:
ProcessID: Alive and stable from the call to the ICorProfilerCallback::Initialize method until the return from the ICorProfilerCallback::Shutdown method.
AppDomainID: Alive and stable from the call to the ICorProfilerCallback::AppDomainCreationFinished method until the return from the ICorProfilerCallback::AppDomainShutdownStarted method.
AssemblyID, ModuleID, ClassID: Alive and stable from the call to the LoadFinished method for the ID until the return from the UnloadStarted method for the ID.
FunctionID: Alive and stable from the call to the ICorProfilerCallback::JITCompilationFinished or ICorProfilerCallback::JITCachedFunctionSearchFinished method until the destruction of the containing ClassID.
ThreadID: Alive and stable from the call to the ICorProfilerCallback::ThreadCreated method until the return from the ICorProfilerCallback::ThreadDestroyed method.
ObjectID: Alive starting with the call to the ICorProfilerCallback::ObjectAllocated method. Eligible to change or die with each garbage collection.
GCHandleID: Alive from the call to the ICorProfilerCallback2::HandleCreated method until the return from the ICorProfilerCallback2::HandleDestroyed method.
In addition, any ID that returns from a profiling function will be alive at the time it is returned.
Application Domain Affinity
Each user-created application domain in a process has an AppDomainID. The default domain and a special pseudo-domain that is used for holding domain-neutral assemblies also have AppDomainIDs.
Assemblies, modules, classes, functions, and garbage collector handles have application domain affinity. This means that if an assembly is loaded into multiple application domains, the assembly and all its modules, classes, functions, and garbage collector handles will have a different ID in each application domain, and operations on each ID will take effect only in the associated application domain. Domain-neutral assemblies appear in the special pseudo-domain mentioned earlier.
Special Notes
All IDs except ObjectID should be treated as opaque values. Most IDs are fairly self-explanatory. The following IDs are worth explaining in more detail:
ClassIDs represent classes. In the case of generic classes, they represent fully instantiated types. List<int>, List<char>, List<object>, and List<string> each have their own ClassID. List<T> is an uninstantiated type and has no ClassID. Dictionary<string,V> is a partially instantiated type and has no ClassID.
FunctionIDs represent native code for a function. In the case of generic functions (or functions on generic classes), multiple native code instantiations, and therefore multiple FunctionIDs, might exist for a given function. Native code instantiations may be shared between different types (for example, List<string> and List<object> share code), so a FunctionID may belong to more than one ClassID.
ObjectIDs represent garbage-collected objects. An ObjectID is the current address of an object at the time the profiler receives the ObjectID, and may change with each garbage collection. Therefore, an ObjectID value is valid only between the time it is received and the time the next garbage collection starts. The CLR also supplies notifications for a profiler to update its internal object-tracking maps so that the profiler can maintain a valid ObjectID across garbage collections.
GCHandleIDs represent entries in the garbage collection's handle table. GCHandleIDs, unlike ObjectIDs, are opaque values. Garbage collection handles are created by the CLR itself in some cases, or they can be created by using the GCHandle structure. (Note that the GCHandle structure only represents the handle; the handle is not contained in the structure.)
ThreadIDs represent managed threads. If a host supports execution in fiber mode, a managed thread may exist on different operating system threads, depending on when it is examined.
Note
Profiling of fiber-mode applications is not supported in the .NET Framework version 2.0.