Profiler Attach and Detach
In .NET Framework versions before the .NET Framework version 4, an application and its profiler had to be loaded at the same time. When an application started, the runtime determined whether the application should be profiled by examining environment variables and registry settings, and then loaded the required profiler. The profiler then remained in the process space until the application exited.
Starting with the .NET Framework 4, you can attach a profiler to an application after the application has already started, profile the application, and then detach the profiler before the application ends. After the profiler is detached, the application continues to run as it did before the profiler was attached. In other words, no trace remains of the profiler’s temporary presence in the process space.
In addition, starting with the .NET Framework 4, profiling attach and detach methods and related profiling API enhancements enable you to use profiler-based tools as just-in-time, out-of-the-box diagnostic tools.
These enhancements are discussed in the following sections:
Attaching a Profiler
Attach Details
Step-by-Step Attach
Locating the AttachProfiler Method
Attaching to a Target Application Before the Runtime Is Loaded
Attach Restrictions
Profiler Attach Example
Initializing the Profiler After Attach
Completing Profiler Attach
Detaching a Profiler
Thread Considerations During Detach
Step-by-Step Detach
Detach Restrictions
Debugging
Attaching a Profiler
Two separate processes are required to attach a profiler to a running application: the trigger process and the target process.
The trigger process is an executable that causes the profiler DLL to be loaded into a running application's process space. The trigger process uses the ICLRProfiling::AttachProfiler method to ask the common language runtime (CLR) to load the profiler DLL. When the profiler DLL has been loaded in the target process, the trigger process may remain or exit, at the profiler developer’s discretion.
The target process contains the application that you want to profile and the CLR. After the AttachProfiler method is called, the profiler DLL is loaded into this process space along with the target application.
Attach Details
AttachProfiler attaches the specified profiler to the specified process, optionally passing some data to the profiler. It waits for the specified time-out for the attach to complete.
Within the target process, the procedure for loading a profiler at attach-time is similar to the procedure for loading a profiler at startup-time: The CLR CoCreates the given CLSID, queries for the ICorProfilerCallback3 interface, and calls the ICorProfilerCallback3::InitializeForAttach method. If these operations succeed within the allotted time-out, AttachProfiler returns an S_OK HRESULT. Therefore, the trigger process should choose a time-out that is sufficient for the profiler to complete initialization.
Note
Time-outs can occur because a finalizer in the target process ran longer than the time-out value, resulting in an HRESULT_FROM_WIN32 (ERROR_TIMEOUT). If you receive this error, you can retry the attach operation.
If the time-out expires before the attach is complete, AttachProfiler returns an HRESULT error; however, the attach may have succeeded. In such cases, there are alternate ways to determine success. Profiler developers often have a custom communication channel between their trigger process and the target application. Such a communication channel can be used to detect a successful attach. The trigger process can also detect success by calling AttachProfiler again and receiving CORPROF_E_PROFILER_ALREADY_ACTIVE.
In addition, the target process must have sufficient access rights to be able to load the profiler’s DLL. This can be a problem when attaching to services (for example, the W3wp.exe process) that may be running under accounts with restricted access rights, such as NETWORK SERVICE. In such cases, some directories on the file system may have security restrictions that prevent access by the target process. As a result, it is the profiler developer’s responsibility to install the profiler DLL in a location that permits appropriate access.
Failures are logged to the application event log.
Step-by-Step Attach
The trigger process calls AttachProfiler, identifying the target process and the profiler to be attached, and optionally passing data to be given to the profiler after attach.
Within the target process, the CLR receives the attach request.
Within the target process, the CLR creates the profiler object and gets the ICorProfilerCallback3 interface from it.
The CLR then calls the InitializeForAttach method, passing the data that the trigger process included in the attach request.
The profiler initializes itself, enables the callbacks it needs, and returns successfully.
Within the trigger process, AttachProfiler returns an S_OK HRESULT, indicating that the profiler has successfully attached. The trigger process ceases to be relevant.
Profiling continues from this point as in all previous versions.
Locating the AttachProfiler Method
Trigger processes can locate the AttachProfiler method by following these steps:
Call the LoadLibrary method to load mscoree.dll and to find the CLRCreateInstance method.
Call the CLRCreateInstance method with the CLSID_CLRMetaHost and IID_ICLRMetaHost arguments, which returns an ICLRMetaHost interface.
Call the ICLRMetaHost::EnumerateLoadedRuntimes method to retrieve an ICLRRuntimeInfo interface for each CLR instance in the process.
Iterate through the ICLRRuntimeInfo interfaces until the desired interface to which to attach the profiler is found.
Call the ICLRRuntimeInfo::GetInterface method with an argument of IID_ICLRProfiling to obtain an ICLRProfiling interface, which provides the AttachProfiler method.
Attaching to a Target Application Before the Runtime Is Loaded
This functionality is not supported. If the trigger process attempts to call the AttachProfiler method by specifying a process that has not yet loaded the runtime, the AttachProfiler method returns a failure HRESULT. If a user wants to profile an application from the instant the runtime is loaded, the user should set the appropriate environment variables (or run an application authored by the profiler developer to do so) before launching the target application.
Attach Restrictions
Only a subset of the ICorProfilerCallback and ICorProfilerInfo methods are available to attaching profilers, as explained in the following sections.
ICorProfilerCallback Restrictions
When the profiler calls the ICorProfilerInfo::SetEventMask method, it must specify only event flags that appear in the COR_PRF_ALLOWABLE_AFTER_ATTACH enumeration. No other callbacks are available to attaching profilers. If an attaching profiler tries to specify a flag that is not in the COR_PRF_ALLOWABLE_AFTER_ATTACH bitmask, ICorProfilerInfo::SetEventMask returns a CORPROF_E_UNSUPPORTED_FOR_ATTACHING_PROFILER HRESULT.
ICorProfilerInfo Restrictions
If a profiler that was loaded by calling the AttachProfiler method tries to call any of the following restricted ICorProfilerInfo or ICorProfilerInfo2 methods, that method will return an CORPROF_E_UNSUPPORTED_FOR_ATTACHING_PROFILER HRESULT.
Restricted ICorProfilerInfo methods:
Restricted ICorProfilerInfo2 methods:
Restricted ICorProfilerInfo3 methods:
Profiler Attach Example
This example assumes that the trigger process already knows the process identifier (PID) of the target process, the number of milliseconds it wants to wait for time-out, the CLSID of the profiler to load, and the full path to the profiler’s DLL file. It also assumes that the profiler developer has defined a structure called MyProfilerConfigData, which holds configuration data for the profiler, and a function called PopulateMyProfilerConfigData, which puts configuration data into that structure.
HRESULT CallAttachProfiler(DWORD dwProfileeProcID, DWORD dwMillisecondsTimeout,
GUID profilerCLSID, LPCWSTR wszProfilerPath)
{
// This can be a data type of your own choosing for sending configuration data
// to your profiler:
MyProfilerConfigData profConfig;
PopulateMyProfilerConfigData(&profConfig);
LPVOID pvClientData = (LPVOID) &profConfig;
DWORD cbClientData = sizeof(profConfig);
ICLRMetaHost * pMetaHost = NULL;
IEnumUnknown * pEnum = NULL;
IUnknown * pUnk = NULL;
ICLRRuntimeInfo * pRuntime = NULL;
ICLRProfiling * pProfiling = NULL;
HRESULT hr = E_FAIL;
hModule = LoadLibrary(L"mscoree.dll");
if (hModule == NULL)
goto Cleanup;
CLRCreateInstanceFnPtr pfnCreateInterface =
(CLRCreateInstanceFnPtr)GetProcAddress(hModule, "CLRCreateInterface");
if (pfnCreateInterface == NULL)
goto Cleanup;
hr = (*pfnCreateInterface)(CLSID_CLRMetaHost, IID_ICLRMetaHost, (LPVOID *)&pMetaHost);
if (FAILED(hr))
goto Cleanup;
hr = pMetaHost->EnumerateLoadedRuntimes(hProcess, &pEnum);
if (FAILED(hr))
goto Cleanup;
while (pEnum->Next(1, &pUnk, NULL) == S_OK)
{
hr = pUnk->QueryInterface(IID_ICLRRuntimeInfo, (LPVOID *) &pRuntime);
if (FAILED(hr))
{
pUnk->Release();
pUnk = NULL;
continue;
}
WCHAR wszVersion[30];
DWORD cchVersion = sizeof(wszVersion)/sizeof(wszVersion[0]);
hr = pRuntime->GetVersionString(wszVersion, &cchVersion);
if (SUCCEEDED(hr) &&
(cchVersion >= 3) &&
((wszVersion[0] == L'v') || (wszVersion[0] == L'V')) &&
((wszVersion[1] >= L'4') || (wszVersion[2] != L'.')))
{
hr = pRuntime->GetInterface(CLSID_CLRProfiling, IID_ICLRProfiling, (LPVOID *)&pProfiling);
if (SUCCEEDED(hr))
{
hr = pProfiling->AttachProfiler(
dwProfileeProcID,
dwMillisecondsTimeout,
profilerCLSID,
wszProfilerPath,
pvClientData,
cbClientData
);
pProfiling->Release();
pProfiling = NULL;
break;
}
}
pRuntime->Release();
pRuntime = NULL;
pUnk->Release();
pUnk = NULL;
}
Cleanup:
if (pProfiling != NULL)
{
pProfiling->Release();
pProfiling = NULL;
}
if (pRuntime != NULL)
{
pRuntime->Release();
pRuntime = NULL;
}
if (pUnk != NULL)
{
pUnk->Release();
pUnk = NULL;
}
if (pEnum != NULL)
{
pEnum->Release();
pEnum = NULL;
}
if (pMetaHost != NULL)
{
pMetaHost->Release();
pMetaHost = NULL;
}
if (hModule != NULL)
{
FreeLibrary(hModule);
hModule = NULL;
}
return hr;
}
Initializing the Profiler After Attach
Starting with the .NET Framework 4, the ICorProfilerCallback3::InitializeForAttach method is provided as the attach counterpart of the ICorProfilerCallback::Initialize method. The CLR calls InitializeForAttach to give the profiler an opportunity to initialize its state after an attach. In this callback, the profiler calls the ICorProfilerInfo::SetEventMask method to request one or more events. Unlike calls made during the ICorProfilerCallback::Initialize method, calls to the SetEventMask method made from InitializeForAttach may only set bits that are in the COR_PRF_ALLOWABLE_AFTER_ATTACH bitmask (see Attach Restrictions).
Upon receiving an error code from InitializeForAttach, the CLR logs the failure to the Windows Application Event Log, releases the profiler callback interface, and unloads the profiler. AttachProfiler returns the same error code as InitializeForAttach.
Completing Profiler Attach
Starting with the .NET Framework 4, the ICorProfilerCallback3::ProfilerAttachComplete callback is issued after the InitializeForAttach method. ProfilerAttachComplete indicates that the callbacks that were requested by the profiler in the InitializeForAttach method have been activated, and that the profiler can now perform catch-up on the associated IDs without worrying about missing notifications.
For example, assume that the profiler has set COR_PRF_MONITOR_MODULE_LOADS during its InitializeForAttach callback. When the profiler returns from InitializeForAttach, the CLR enables ModuleLoad callbacks, and then issues the ProfilerAttachComplete callback. The profiler can then use the ICorProfilerInfo3::EnumModules method to enumerate ModuleIDs for all currently loaded modules during its ProfilerAttachComplete callback. In addition, ModuleLoad events are issued for any new modules that are loaded during the enumeration. Profilers will have to deal properly with any duplicates they encounter. For example, a module that loads at just the right time while a profiler is attaching may result in the ModuleID appearing twice: in the enumeration that is returned by ICorProfilerInfo3::EnumModules and in a ModuleLoadFinished callback.
Detaching a Profiler
The attach request must be initiated out-of-process by the trigger process. However, the detach request is initiated in-process by the profiler DLL when it calls the ICorProfilerInfo3::RequestProfilerDetach method. If the profiler developer wishes to initiate the detach request from out-of-process (for example, from a custom UI), the developer must create an inter-process communication mechanism to signal the profiler DLL (running alongside the target application) to call RequestProfilerDetach.
RequestProfilerDetach performs some of its work (including setting its internal state to prevent sending events to the profiler DLL) synchronously before returning. The rest of its work is done asynchronously, after RequestProfilerDetach returns. This remaining work executes on a separate thread (DetachThread) and includes polling and ensuring that all profiler code has been popped off the stacks of all the application threads. When RequestProfilerDetach is finished, the profiler receives one final callback (ICorProfilerCallback3::ProfilerDetachSucceeded), before the CLR releases the profiler’s interface and code heaps, and unloads the profiler’s DLL.
Thread Considerations During Detach
Execution control may transfer to a profiler in many ways. However, control must not be passed to a profiler after it is unloaded, and both the profiler and the runtime must share the responsibility to ensure that this behavior does not occur:
The runtime does not know about threads that are created or hijacked by the profiler that contain, or may soon contain, profiler code on the stack. Therefore, the profiler must ensure that it exits from all the threads it has created, and must stop all sampling or hijacking before it calls ICorProfilerInfo3::RequestProfilerDetach. There is one exception to this rule: The profiler may use a thread that it created to call ICorProfilerInfo3::RequestProfilerDetach. However, this thread must maintain its own reference to the profiler DLL through the LoadLibrary and FreeLibraryAndExitThread functions (see the next section for details).
After the profiler calls RequestProfilerDetach, the runtime must ensure that ICorProfilerCallback methods do not cause profiler code to remain on the stack of any threads when the runtime tries to fully unload the profiler.
Step-by-Step Detach
The following steps take place when a profiler is detached:
The profiler exits any threads it has created, and stops all sampling and hijacking before calling ICorProfilerInfo3::RequestProfilerDetach, with the following exception:
A profiler may implement detaching by using one of its own threads to call the ICorProfilerInfo3::RequestProfilerDetach method (instead of using a CLR-created thread). If a profiler implements this behavior, it is acceptable for this profiler thread to exist when the RequestProfilerDetach method is called (because this is the thread that calls the method). However, if a profiler chooses this implementation, it must ensure the following:
The thread that will remain to call RequestProfilerDetach must keep its own reference to the profiler DLL (by calling the LoadLibrary function on itself).
After the thread calls RequestProfilerDetach, it must immediately call the FreeLibraryAndExitThread function to release its hold on the profiler DLL and exit.
RequestProfilerDetach sets the internal state of the runtime to consider the profiler as disabled. This prevents future calls into the profiler through the callback methods.
RequestProfilerDetach signals DetachThread to begin checking that all threads have popped any remaining callback methods off their stacks.
RequestProfilerDetach returns with a status code indicating whether detach was successfully started.
At this point, the CLR disallows any further calls from the profiler into the CLR through the ICorProfilerInfo, ICorProfilerInfo2 and ICorProfilerInfo3 interface methods. Any such calls immediately fail and return a CORPROF_E_PROFILER_DETACHING HRESULT.
The profiler returns or exits the thread. If the profiler used one of its own threads to make this call to RequestProfilerDetach, it must call FreeLibraryAndExitThread from this thread now. If the profiler called RequestProfilerDetach by using a CLR thread (that is, from within a callback), the profiler returns control back to the CLR.
Meanwhile, DetachThread continues to check whether all threads have popped any remaining callback methods off their stacks.
When DetachThread has determined that no callbacks remain on any thread’s stack, it calls the ICorProfilerCallback3::ProfilerDetachSucceeded method. The profiler should do minimal work in the ProfilerDetachSucceeded method, and return quickly.
DetachThread performs final Release() on the profiler’s ICorProfilerCallback interface.
DetachThread calls the FreeLibrary function on the profiler’s DLL.
Detach Restrictions
Detaching is not supported in the following scenarios:
When the profiler sets immutable event flags.
When the profiler uses enter/leave/tailcall (ELT) probes.
When the profiler use Microsoft intermediate language (MSIL) instrumentation (for example, by calling the SetILFunctionBody method).
If profiler detach is attempted in these scenarios, RequestProfilerDetach returns an error HRESULT.
Debugging
Profiler attach and detach functionality does not prevent an application from being debugged. An application may have a profiler attach and detach at any time during the debugging session. Conversely, an application that has a profiler attach (and later detach) may also have a debugger attach and detach at any point. However, an application that is suspended by a debugger cannot be profiled because it cannot respond to a profiler attach request.