Profiler Catch-up

A profiler that loads when an application starts up can optionally determine the entire history of that application. The profiler can request the appropriate callback events to learn about the classes and modules that are loaded, functions that are just-in-time (JIT) compiled, objects that have been allocated, and so on. However, a profiler that loads by attaching to an already running process does not know what has already occurred; it must catch up with the current state of the application and the associated garbage collection heap.

There are two fundamental ways in which the profiler can catch up with the current state of an application:

  • Lazy catch-up. When the profiler encounters new IDs, it queries for information about those IDs, instead of assuming that it has a full cache that is built as the IDs are created.

  • Enumeration. For certain kinds of IDs, the profiler can request (at attach time) a complete list of the currently active IDs and query for information about them.

In lazy catch-up, the profiler obtains information about IDs as it encounters them. For example, if the sampling profiler encounters an instruction pointer (IP) in a FunctionID that has not been seen before, it looks up information about that function, instead of assuming that it has a complete cache of functions from JIT-compilation time. If that FunctionID resides in a module that the profiler has not seen before, it looks up information about that ModuleID at that point, instead of assuming that there is a complete cache of all modules. Many profilers are already doing something similar to this if they support sampling against regular native image format (NGEN) functions, because there are no JIT notifications for NGEN images.

Starting with the .NET Framework 4, the profiling API provides enumerator methods for module and JIT-compiled function IDs: 

These methods return a standard enumerator that lets you iterate through all the currently loaded IDs of that type. Note that EnumJITedFunctions enumerates only FunctionIDs for which you would receive ICorProfilerCallback::JITCompilationStarted and ICorProfilerCallback::JITCompilationFinished events, and will not include FunctionIDs from NGEN modules.

These enumerators provide a snapshot of the IDs while the process is active and running. This means that there are two race conditions the profiler can encounter:

  • The first race condition is caused by enumerating too soon, which can result in holes and allow IDs to be missed.

  • The second race condition is caused by IDs loading or unloading while the profiler enumerates through those IDs.

This following sections describe how a profiler can avoid missed IDs. For a detailed description of both race conditions, see the Profiler Attach, Part 2 entry in the CLR Profiling API blog.

To avoid the race condition that causes holes, the profiler should call enumeration methods only after events have been enabled. Because events are enabled at some point after the ICorProfilerCallback3::InitializeForAttach method returns, the best place for the profiler to call the enumeration methods is inside its implementation of the ICorProfilerCallback3::ProfilerAttachComplete method, which is provided in the .NET Framework 4 and later versions. Events are enabled just before the CLR calls ProfilerAttachComplete, so the profiler is assured that events have been enabled when it calls the enumeration from inside ProfilerAttachComplete. This eliminates any potential holes in catch-up information.

The following examples show how the profiler can encounter duplicates when it calls an enumeration method from inside the ICorProfilerCallback3::ProfilerAttachComplete method. Profilers should recognize the possibility of this race condition and be prepared to handle it.

Loading with a duplicate:

  1. The module starts to load. The ModuleID is now enumerable.

  2. The profiler attaches.

  3. CLR enables events and calls the ProfilerAttachComplete method.

  4. The profiler calls the EnumModules method.

  5. The profiler receives the ModuleLoadFinished event.

In this sequence, the enumerator contains the ModuleID, so the profiler sees that the module is loaded. However, the profiler then receives a ModuleLoadFinished event, which may be unexpected because the enumerator had reported that the module was already loaded. The profiler is notified of a ModuleID twice: once by the enumeration, and once by the event.

Unloading with a duplicate:

  1. The module loads. The event would have fired if the profiler were attached, but it is not. The ModuleID becomes enumerable.

  2. The module starts to unload. The ModuleID is no longer enumerable.

  3. The profiler attaches.

  4. The CLR enables events and calls the ProfilerAttachComplete method.

  5. The profiler calls the EnumModules method.

  6. The profiler receives the ModuleUnloadStarted event.

In step 5, the profiler does not see the unloading ModuleID, because it was not enumerated. However, in step 6, the profiler is notified that the ModuleID is unloading. In this case, a profiler is notified that a ModuleID it does not know about is unloading. The profiler must be aware that it can receive an event for a module it does not know about.

Another sequence to consider occurs when the profiler attach occurs immediately before the ID disappears from the enumeration.

Unloading without a duplicate:

  1. The module loads. The event would fire if the profiler were attached, but it is not. The ModuleID becomes enumerable.

  2. The module starts to unload.

  3. The profiler attaches.

  4. The CLR enables events and calls the ProfilerAttachComplete method.

  5. The profiler calls the EnumModules method. (The ModuleID is still present in the enumeration.)

  6. The ModuleID is no longer enumerable.

  7. The profiler receives the ModuleUnloadStarted event.

In this sequence, the ModuleID exists in the enumeration. However, the profiler could have been iterating though the enumeration for some time, and might not know about the ModuleID when it receives the ModuleUnloadStarted event. For this reason, the profiler should be aware of the event when it encounters the ModuleID in the enumeration.

The general rule is that load/unload callbacks are always issued after the enumerability of an ID changes. Therefore, callbacks are more recent than enumerations and should always take precedence over earlier enumerations.

Most memory profilers respond to garbage collection events. When the memory profiler attaches to a running process, it must deal gracefully with the fact that it does not yet have a cache of objects on the heap. Memory profilers must be cautious about garbage collection that is already in progress at the time the profiler attaches, and should consider inducing a garbage collection at attach time to build the initial cache of garbage collection objects.

The profiler attaches at an arbitrary point during process execution, possibly while a garbage collection is already in progress. This means that once the profiler has enabled callback events, it may start seeing garbage collection callbacks (for example, MovedReference and ObjectReferences) from that garbage collection, without seeing a GarbageCollectionStarted notification first. The profiler should ignore all garbage collection callbacks until the first full garbage collection or profiler-induced garbage collection begins (that is, until it sees the first GarbageCollectionStarted callback, either for a standard garbage collection or after calling the ICorProfilerInfo::ForceGC method).

It may be beneficial for profilers to trigger the first full garbage collection after attaching to the process. This allows the profiler to use events such as RootReferences2 and ObjectReferences during that initial garbage collection to build its cache of objects. After that initial catch-up garbage collection, the profiler can handle successive garbage collections in the standard manner.

Note that the ICorProfilerCallback::ObjectAllocated callback is unavailable to profilers that attach to running processes. Therefore, the profiler will have to address any assumptions about ObjectAllocated callbacks. Objects that were allocated since the last garbage collection may be unknown to the profiler until it encounters their references through garbage collection callbacks during the next collection (unless the profiler comes across those objects in other ways; for example, as parameters to methods you hook with the enter/leave/tailcall probes).

Was this page helpful?
(1500 characters remaining)
Thank you for your feedback
Show:
© 2014 Microsoft