Permalink
Fetching contributors…
Cannot retrieve contributors at this time
714 lines (612 sloc) 25.4 KB
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
//
// ProfDetach.cpp
//
//
// Implementation of helper classes and structures used for Profiling API Detaching
//
// ======================================================================================
#include "common.h"
#ifdef FEATURE_PROFAPI_ATTACH_DETACH
#include "profdetach.h"
#include "profilinghelper.h"
// Class static member variables
ProfilerDetachInfo ProfilingAPIDetach::s_profilerDetachInfo;
CLREvent ProfilingAPIDetach::s_eventDetachWorkAvailable;
// ---------------------------------------------------------------------------------------
// ProfilerDetachInfo constructor
//
// Description:
// Set every member variable to NULL or 0. They'll get initialized to real values
// in ProfilingAPIDetach::RequestProfilerDetach.
//
ProfilerDetachInfo::ProfilerDetachInfo()
{
// Executed during construction of a global object, therefore we cannot
// use real contracts, as this requires that utilcode has been initialized.
STATIC_CONTRACT_NOTHROW;
STATIC_CONTRACT_MODE_ANY;
STATIC_CONTRACT_GC_NOTRIGGER;
STATIC_CONTRACT_CANNOT_TAKE_LOCK;
Init();
}
void ProfilerDetachInfo::Init()
{
// Executed during construction of a global object, therefore we cannot
// use real contracts, as this requires that utilcode has been initialized.
STATIC_CONTRACT_LEAF;
m_pEEToProf = NULL;
m_ui64DetachStartTime = 0;
m_dwExpectedCompletionMilliseconds = 0;
}
// ----------------------------------------------------------------------------
// Implementation of ProfilingAPIAttachDetach statics
// ----------------------------------------------------------------------------
// ProfilingAPIDetach::Initialize
//
// Description:
// Initialize static event
// static
HRESULT ProfilingAPIDetach::Initialize()
{
CONTRACTL
{
NOTHROW;
MODE_ANY;
GC_TRIGGERS;
}
CONTRACTL_END;
if (!s_eventDetachWorkAvailable.IsValid())
{
HRESULT hr = S_OK;
EX_TRY
{
s_eventDetachWorkAvailable.CreateAutoEvent(FALSE);
}
EX_CATCH
{
hr = GET_EXCEPTION()->GetHR();
if (SUCCEEDED(hr))
{
// For exceptions that give us useless hr's, just use E_FAIL
hr = E_FAIL;
}
}
EX_END_CATCH(RethrowTerminalExceptions)
if (FAILED(hr))
{
return hr;
}
}
return S_OK;
}
// ----------------------------------------------------------------------------
// ProfilingAPIDetach::RequestProfilerDetach
//
// Description:
// Initialize ProfilerDetachInfo structures with parameters passed from
// ICorProfilerInfo3::RequestProfilerDetach
//
// Arguments:
// * dwExpectedCompletionMilliseconds - A hint to the CLR as to how long it should
// wait before checking to see if execution has evacuated the profiler and all
// profiler-instrumented code. If this is 0, the CLR will select a default.
//
// Notes:
//
// Invariants maintained by profiler:
// * Before calling RequestProfilerDetach, the profiler must turn off all hijacking.
// * If RequestProfilerDetach is called from a thread created by the CLR (i.e., from
// within a callback), the profiler must first have exited all threads of its own
// creation
// * If RequestProfilerDetach is called from a thread of the profiler's own creation,
// then
// * The profiler must first have exited all OTHER threads of its own creation,
// AND
// * The profiler must immediately call FreeLibraryAndExitThread() after
// RequestProfilerDetach returns.
//
// The above invariants result in the following possiblities:
// * RequestProfilerDetach() may be called multi-threaded, but only from within
// profiler callbacks. As such, evacuation counters will have been incremented
// before entry into RequestProfilerDetach(), so the DetachThread will be
// blocked until all such threads have returned from RequestProfilerDetach and
// the callback from which RequestProfilerDetach was called. OR
// * RequestProfilerDetach() is called single-threaded, from a thread of the
// profiler's creation, which promises not to make any more calls into the CLR
// afterward. In this case, the DetachThread will be blocked until
// RequestProfilerDetach signals s_eventDetachWorkAvailable at the end.
//
// static
HRESULT ProfilingAPIDetach::RequestProfilerDetach(DWORD dwExpectedCompletionMilliseconds)
{
CONTRACTL
{
NOTHROW;
// Crst is used so GC may be triggered
GC_TRIGGERS;
MODE_ANY;
EE_THREAD_NOT_REQUIRED;
// Crst is used to synchronize the initialization of ProfilingAPIDetach internal structure
CAN_TAKE_LOCK;
PRECONDITION(ProfilingAPIUtility::GetStatusCrst() != NULL);
PRECONDITION(s_eventDetachWorkAvailable.IsValid());
}
CONTRACTL_END;
// Runtime must be fully started, or else CpuStoreBufferControl used below may not
// be initialized yet.
if (!g_fEEStarted)
{
return CORPROF_E_RUNTIME_UNINITIALIZED;
}
if (dwExpectedCompletionMilliseconds == 0)
{
// Pick suitable default if the profiler just leaves this at 0. 5 seconds is
// reasonable.
dwExpectedCompletionMilliseconds = 5000;
}
{
CRITSEC_Holder csh(ProfilingAPIUtility::GetStatusCrst());
// return immediately if detach is in progress
if (s_profilerDetachInfo.m_pEEToProf != NULL)
{
return CORPROF_E_PROFILER_DETACHING;
}
ProfilerStatus curProfStatus = g_profControlBlock.curProfStatus.Get();
if ((curProfStatus == kProfStatusInitializingForStartupLoad) ||
(curProfStatus == kProfStatusInitializingForAttachLoad))
{
return CORPROF_E_PROFILER_NOT_YET_INITIALIZED;
}
if (curProfStatus != kProfStatusActive)
{
// Before we acquired the lock, someone else must have unloaded the profiler
// for us (e.g., shutdown or the DetachThread in response to a prior
// RequestProfilerDetach call).
return CORPROF_E_PROFILER_DETACHING;
}
EEToProfInterfaceImpl * pEEToProf = g_profControlBlock.pProfInterface;
// Since prof status was active after entering the lock, the profiler must not
// have unloaded out from under us.
_ASSERTE(pEEToProf != NULL);
if (!pEEToProf->IsCallback3Supported())
{
return CORPROF_E_CALLBACK3_REQUIRED;
}
// Did the profiler do anything immutable? That will prevent us from allowing it to
// detach.
HRESULT hr = pEEToProf->EnsureProfilerDetachable();
if (FAILED(hr))
{
return hr;
}
s_profilerDetachInfo.m_pEEToProf = pEEToProf;
s_profilerDetachInfo.m_ui64DetachStartTime = CLRGetTickCount64();
s_profilerDetachInfo.m_dwExpectedCompletionMilliseconds = dwExpectedCompletionMilliseconds;
// Ok, time to seal the profiler from receiving or making calls with the CLR.
// (This will force a FlushStoreBuffers().)
g_profControlBlock.curProfStatus.Set(kProfStatusDetaching);
}
// Sealing done. Wake up the DetachThread so it can loop until the profiler code is
// fully evacuated off of all stacks.
if (!s_eventDetachWorkAvailable.Set())
{
return HRESULT_FROM_WIN32(GetLastError());
}
// FUTURE: Currently, kProfStatusDetaching prevents callbacks from being sent to the
// profiler AND prevents another profiler from attaching. In the future, when
// implementing the reattach-with-neutered-profilers feature crew, we may want to add
// another block here to call ProfilingAPIUtility::SetProfStatus(kProfStatusNone), so callbacks are
// prevented but a new profiler may attempt to attach.
EX_TRY
{
ProfilingAPIUtility::LogProfInfo(IDS_PROF_DETACH_INITIATED);
}
EX_CATCH
{
// Oh well, rest of detach succeeded, so we should still return success to the
// profiler.
}
EX_END_CATCH(RethrowTerminalExceptions);
return S_OK;
}
//---------------------------------------------------------------------------------------
//
// This is where the DetachThread spends its life. This waits until there's a profiler
// to detach, then loops until the profiler code is completely evacuated off all stacks.
// This will then unload the profiler.
//
// static
void ProfilingAPIDetach::ExecuteEvacuationLoop()
{
CONTRACTL
{
THROWS;
GC_TRIGGERS;
MODE_PREEMPTIVE;
CAN_TAKE_LOCK;
}
CONTRACTL_END;
// Wait until there's a profiler to detach (or until this thread should "wake up"
// for some other reason, such as exiting due to an unsuccessful startup-load of a
// profiler).
DWORD dwRet = s_eventDetachWorkAvailable.Wait(INFINITE, FALSE /* alertable */);
if (dwRet != WAIT_OBJECT_0)
{
// The wait ended due to a failure or a reason other than the event getting
// signaled (e.g., WAIT_ABANDONED)
DWORD dwErr;
if (dwRet == WAIT_FAILED)
{
dwErr = GetLastError();
LOG((
LF_CORPROF,
LL_ERROR,
"**PROF: DetachThread wait for s_eventDetachWorkAvailable failed with GetLastError = %d.\n",
dwErr));
}
else
{
dwErr = dwRet; // No extra error info available beyond the return code
LOG((
LF_CORPROF,
LL_ERROR,
"**PROF: DetachThread wait for s_eventDetachWorkAvailable terminated with %d.\n",
dwErr));
}
ProfilingAPIUtility::LogProfError(IDS_PROF_DETACH_THREAD_ERROR, dwErr);
return;
}
// Peek to make sure there's actually a profiler to detach
{
CRITSEC_Holder csh(ProfilingAPIUtility::GetStatusCrst());
if (s_profilerDetachInfo.m_pEEToProf == NULL)
{
// Nothing to detach. This can happen if the DetachThread (i.e., current
// thread) was created but then the profiler failed to load.
return;
}
}
do
{
// Give profiler a chance to return from its procs
SleepWhileProfilerEvacuates();
}
while (!IsProfilerEvacuated());
UnloadProfiler();
}
//---------------------------------------------------------------------------------------
//
// This is called in between evacuation counter checks. This calculates how long to
// sleep, and then sleeps.
//
// static
void ProfilingAPIDetach::SleepWhileProfilerEvacuates()
{
CONTRACTL
{
THROWS;
GC_TRIGGERS;
MODE_PREEMPTIVE;
CAN_TAKE_LOCK;
}
CONTRACTL_END;
// Don't want to check evacuation any more frequently than every 300ms
const DWORD kdwDefaultMinSleepMs = 300;
// The default "steady state" max sleep is how long we'll wait if, after a couple
// tries the profiler still hasn't evacuated. Default to every 10 minutes
const DWORD kdwDefaultMaxSleepMs = 600000;
static DWORD s_dwMinSleepMs = 0;
static DWORD s_dwMaxSleepMs = 0;
// First time through, initialize the static min / max sleep times. Normally, we'll
// just use the constants above, but the user may customize these (within reason).
// They should either both be uninitialized or both initialized
_ASSERTE(
((s_dwMinSleepMs == 0) && (s_dwMaxSleepMs == 0)) ||
((s_dwMinSleepMs != 0) && (s_dwMaxSleepMs != 0)));
if (s_dwMaxSleepMs == 0)
{
// No race here, since only the DetachThread runs this code
s_dwMinSleepMs = CLRConfig::GetConfigValue(CLRConfig::EXTERNAL_ProfAPI_DetachMinSleepMs);
s_dwMaxSleepMs = CLRConfig::GetConfigValue(CLRConfig::EXTERNAL_ProfAPI_DetachMaxSleepMs);
// Here's the "within reason" part: the user may not customize these values to
// be more "extreme" than the constants, or to be 0 (which would confuse the
// issue of whether these statics were intialized yet).
if ((s_dwMinSleepMs < kdwDefaultMinSleepMs) || (s_dwMinSleepMs > kdwDefaultMaxSleepMs))
{
// Sleeping less than 300ms between evac checks could negatively affect the
// app by having the DetachThread execute too often. And a min sleep time
// that's too high could result in a profiler hanging around way too long
// when it's actually ready to be unloaded.
s_dwMinSleepMs = kdwDefaultMinSleepMs;
}
if ((s_dwMaxSleepMs < kdwDefaultMinSleepMs) || (s_dwMaxSleepMs > kdwDefaultMaxSleepMs))
{
// A steady state that's too small would retry the evac checks too often on
// an ongoing basis. A steady state that's too high could result in a
// profiler hanging around way too long when it's actually ready to be
// unloaded.
s_dwMaxSleepMs = kdwDefaultMaxSleepMs;
}
}
// Take note of when the detach was requested and how long to sleep for
ULONGLONG ui64ExpectedCompletionMilliseconds;
ULONGLONG ui64DetachStartTime;
{
CRITSEC_Holder csh(ProfilingAPIUtility::GetStatusCrst());
_ASSERTE(s_profilerDetachInfo.m_pEEToProf != NULL);
ui64ExpectedCompletionMilliseconds = s_profilerDetachInfo.m_dwExpectedCompletionMilliseconds;
ui64DetachStartTime = s_profilerDetachInfo.m_ui64DetachStartTime;
}
// ui64SleepMilliseconds is calculated to ensure that CLR checks evacuation status roughly:
// * After profiler's ui64ExpectedCompletionMilliseconds hint has elapsed (but not
// too soon)
// * At least once more after 2*ui64ExpectedCompletionMilliseconds have elapsed
// (but not too soon)
// * Occasionally thereafter (steady state)
ULONGLONG ui64ElapsedMilliseconds = CLRGetTickCount64() - ui64DetachStartTime;
ULONGLONG ui64SleepMilliseconds;
if (ui64ExpectedCompletionMilliseconds > ui64ElapsedMilliseconds)
{
// Haven't hit ui64ExpectedCompletionMilliseconds yet, so sleep for remainder
ui64SleepMilliseconds = ui64ExpectedCompletionMilliseconds - ui64ElapsedMilliseconds;
}
else if ((2*ui64ExpectedCompletionMilliseconds) > ui64ElapsedMilliseconds)
{
// We're between ui64ExpectedCompletionMilliseconds &
// 2*ui64ExpectedCompletionMilliseconds, so sleep until
// 2*ui64ExpectedCompletionMilliseconds have transpired
ui64SleepMilliseconds = (2*ui64ExpectedCompletionMilliseconds) - ui64ElapsedMilliseconds;
}
else
{
// Steady state
ui64SleepMilliseconds = s_dwMaxSleepMs;
}
// ...but keep it in bounds!
ui64SleepMilliseconds = min(
max(ui64SleepMilliseconds, s_dwMinSleepMs),
s_dwMaxSleepMs);
// At this point it's safe to cast ui64SleepMilliseconds down to a DWORD since we
// know it's between s_dwMinSleepMs & s_dwMaxSleepMs
_ASSERTE(ui64SleepMilliseconds <= 0xFFFFffff);
ClrSleepEx((DWORD) ui64SleepMilliseconds, FALSE /* alertable */);
}
//---------------------------------------------------------------------------------------
//
// Performs the evacuation checks by grabbing the thread store lock, iterating through
// all EE Threads, and querying each one's evacuation counter. If they're all 0, the
// profiler is ready to be unloaded.
//
// Return Value:
// Nonzero iff the profiler is fully evacuated and ready to be unloaded.
//
// static
BOOL ProfilingAPIDetach::IsProfilerEvacuated()
{
CONTRACTL
{
NOTHROW;
GC_TRIGGERS;
MODE_ANY;
CAN_TAKE_LOCK;
}
CONTRACTL_END;
_ASSERTE(g_profControlBlock.curProfStatus.Get() == kProfStatusDetaching);
// Check evacuation counters on all the threads (see
// code:ProfilingAPIUtility::InitializeProfiling#LoadUnloadCallbackSynchronization
// for details). Doing this under the thread store lock not only ensures we can
// iterate through the Thread objects safely, but also forces us to serialize with
// the GC. The latter is important, as server GC enters the profiler on non-EE
// Threads, and so no evacuation counters might be incremented during server GC even
// though control could be entering the profiler.
{
ThreadStoreLockHolder TSLockHolder;
Thread * pThread = ThreadStore::GetAllThreadList(
NULL, // cursor thread; always NULL to begin with
0, // mask to AND with Thread::m_State to filter returned threads
0); // bits to match the result of the above AND. (m_State & 0 == 0,
// so we won't filter out any threads)
// Note that, by not filtering out any of the threads, we're intentionally including
// stuff like TS_Dead or TS_Unstarted. But that keeps us on the safe
// side. If an EE Thread object exists, we want to check its counters to be
// absolutely certain it isn't executing in a profiler.
while (pThread != NULL)
{
// Note that pThread is still in motion as we check its evacuation counter.
// This is ok, because we've already changed the profiler status to
// kProfStatusDetaching and flushed CPU buffers. So at this point the counter
// will typically only go down to 0 (and not increment anymore), with one
// small exception (below). So if we get a read of 0 below, the counter will
// typically stay there. Specifically:
// * pThread is most likely not about to increment its evacuation counter
// from 0 to 1 because pThread sees that the status is
// kProfStatusDetaching.
// * Note that there is a small race where pThread might actually
// increment its evac counter from 0 to 1 (if it dirty-read the
// profiler status a tad too early), but that implies that when
// pThread rechecks the profiler status (clean read) then pThread
// will immediately decrement the evac counter back to 0 and avoid
// calling into the EEToProfInterfaceImpl pointer.
//
// (see
// code:ProfilingAPIUtility::InitializeProfiling#LoadUnloadCallbackSynchronization
// for details)
DWORD dwEvacCounter = pThread->GetProfilerEvacuationCounter();
if (dwEvacCounter != 0)
{
LOG((
LF_CORPROF,
LL_INFO100,
"**PROF: Profiler not yet evacuated because OS Thread ID 0x%x has evac counter of %d (decimal).\n",
pThread->GetOSThreadId(),
dwEvacCounter));
return FALSE;
}
pThread = ThreadStore::GetAllThreadList(pThread, 0, 0);
}
}
// FUTURE: When rejit feature crew complete, add code to verify all rejitted
// functions are fully reverted and off of all stacks. If this is very easy to
// verify (e.g., checking a single value), consider putting it above the loop
// above so we can early-out quicker if rejitted code is still around.
// We got this far without returning, so the profiler is fully evacuated
return TRUE;
}
// ---------------------------------------------------------------------------------------
// After we've verified a detaching profiler has fully evacuated, call this to unload the
// profiler and clean up state.
//
// Assumptions:
// Since this is called well after the profiler called RequestProfilerDetach, the
// profiler must not have any other threads in use. Also, now that the profiler has
// been evacuated, no CLR threads will be calling into the profiler (thus the
// profiler will not gain control via CLR threads either). That means the profiler
// may not call back into the CLR on any other threads.
//
// static
void ProfilingAPIDetach::UnloadProfiler()
{
CONTRACTL
{
THROWS;
GC_TRIGGERS;
MODE_ANY;
CAN_TAKE_LOCK;
}
CONTRACTL_END;
_ASSERTE(g_profControlBlock.curProfStatus.Get() == kProfStatusDetaching);
{
CRITSEC_Holder csh(ProfilingAPIUtility::GetStatusCrst());
// Notify profiler it's about to be unloaded
_ASSERTE(s_profilerDetachInfo.m_pEEToProf != NULL);
s_profilerDetachInfo.m_pEEToProf->ProfilerDetachSucceeded();
// Reset detach state.
s_profilerDetachInfo.Init();
// This deletes the EEToProfInterfaceImpl object managing the detaching profiler,
// releases the profiler's callback interfaces, unloads the profiler DLL, sets
// the status to kProfStatusNone, and resets g_profControlBlock for use next time
// a profiler tries to attach.
//
// Note that s_profilerDetachInfo.Init() has already NULL'd out
// s_profilerDetachInfo.m_pEEToProf, so we won't have a dangling pointer to the
// EEToProfInterfaceImpl that's about to be destroyed.
ProfilingAPIUtility::TerminateProfiling();
}
ProfilingAPIUtility::LogProfInfo(IDS_PROF_DETACH_COMPLETE);
}
// ----------------------------------------------------------------------------
// ProfilingAPIDetach::ProfilingAPIDetachThreadStart
//
// Description:
// Thread proc for DetachThread. Serves as a simple try/catch wrapper around a call to
// ProfilingAPIDetach::ExecuteEvacuationLoop. This thread proc is specified by
// code:ProfilingAPIDetach::CreateDetachThread when it spins up the new DetachThread.
// This occurs when a profiler is either startup-loaded or attach-loaded.
//
// Arguments:
// * LPVOID thread proc param is ignored
//
// Return Value:
// Just returns 0 always.
//
// static
DWORD WINAPI ProfilingAPIDetach::ProfilingAPIDetachThreadStart(LPVOID)
{
CONTRACTL
{
NOTHROW;
GC_TRIGGERS;
MODE_PREEMPTIVE;
CAN_TAKE_LOCK;
}
CONTRACTL_END;
// At start of this thread, set its type so SOS !threads and anyone else knows who we
// are.
ClrFlsSetThreadType(ThreadType_ProfAPI_Detach);
LOG((
LF_CORPROF,
LL_INFO10,
"**PROF: DetachThread created and executing.\n"));
// This try block is a last-ditch stop-gap to prevent an unhandled exception on the
// DetachThread from bringing down the process. Note that if the unhandled
// exception is a terminal one, then hey, sure, let's tear everything down. Also
// note that any naughtiness in the profiler (e.g., throwing an exception from its
// Initialize callback) should already be handled before we pop back to here, so this
// is just being super paranoid.
EX_TRY
{
// Don't care about return value, thread proc will just return 0 regardless
ExecuteEvacuationLoop();
}
EX_CATCH
{
_ASSERTE(!"Unhandled exception on profiling API detach thread");
}
EX_END_CATCH(RethrowTerminalExceptions);
LOG((
LF_CORPROF,
LL_INFO10,
"**PROF: DetachThread exiting.\n"));
return 0;
}
// ---------------------------------------------------------------------------------------
// Called during startup or attach load of a profiler to create a new thread to fill the role of
// the DetachThread.
//
// static
HRESULT ProfilingAPIDetach::CreateDetachThread()
{
// This function is practically a leaf (though not quite), so keeping the contract
// strict to allow for maximum flexibility on when this may called.
CONTRACTL
{
NOTHROW;
GC_NOTRIGGER;
MODE_ANY;
CANNOT_TAKE_LOCK;
}
CONTRACTL_END;
// FUTURE: When reattach with neutered profilers is implemented, this
// function should check if a DetachThread already exists (use synchronization
// to prevent race), and just return if so.
HandleHolder hDetachThread;
// The DetachThread is intentionally not an EE Thread-object thread (it won't
// execute managed code).
hDetachThread = ::CreateThread(
NULL, // lpThreadAttributes; don't want child processes inheriting this handle
0, // dwStackSize (0 = use default)
ProfilingAPIDetachThreadStart,
NULL, // lpParameter (none to pass)
0, // dwCreationFlags (0 = use default flags, start thread immediately)
NULL // lpThreadId (don't need therad ID)
);
if (hDetachThread == NULL)
{
DWORD dwErr = GetLastError();
LOG((
LF_CORPROF,
LL_ERROR,
"**PROF: Failed to create DetachThread. GetLastError=%d.\n",
dwErr));
return HRESULT_FROM_WIN32(dwErr);
}
return S_OK;
}
//---------------------------------------------------------------------------------------
//
// Accessor for ProfilingAPIDetach::s_profilerDetachInfo.m_pEEToProf, which is the
// profiler being detached (or NULL if no profiler is being detached).
//
// Return Value:
// EEToProfInterfaceImpl * for the profiler being detached.
//
// static
EEToProfInterfaceImpl * ProfilingAPIDetach::GetEEToProfPtr()
{
LIMITED_METHOD_CONTRACT;
return s_profilerDetachInfo.m_pEEToProf;
}
#endif // FEATURE_PROFAPI_ATTACH_DETACH