summaryrefslogtreecommitdiff
path: root/src/vm/stacksampler.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/vm/stacksampler.cpp')
-rw-r--r--src/vm/stacksampler.cpp467
1 files changed, 467 insertions, 0 deletions
diff --git a/src/vm/stacksampler.cpp b/src/vm/stacksampler.cpp
new file mode 100644
index 0000000000..270d278b66
--- /dev/null
+++ b/src/vm/stacksampler.cpp
@@ -0,0 +1,467 @@
+// 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.
+
+//
+// Summary:
+// --------
+//
+// StackSampler is intended to identify methods where the process is spending most of its time
+// and to re-JIT such methods in the background. Call these methods hot.
+//
+// Identifying hot methods:
+// ========================
+//
+// There is no easy way to tell at a given point in execution whether in the future an unseen
+// or un-hot method will become hot; So we track an evolving list of hot methods.
+//
+// We identify hot methods by suspending the runtime every "m" milliseconds. This operation
+// freezes all the threads. We now get a list of threads that are executing and walk their
+// stacks to get the managed method at the top of their stacks. The sampled list of methods
+// for each thread constitute a single sample. Once we obtain a sample, the threads are thawed.
+//
+// The more a method is present in samples, it is clear that the process is spending its time
+// in that method at several given points in time.
+//
+// We track this information on a per method basis, the count of its occurrences in each sample
+// using a hash map.
+//
+// Note:
+// =====
+// o Using the above technique we have only identified top methods at a given point in the execution.
+// The list of hot methods keeps evolving as we get more samples. Only at the process end can we
+// say that the evolving list of hot methods is THE list of hot methods for the whole process.
+// o Because we get the top managed method in the thread, this includes time spent by that method
+// in helper calls.
+// o If GC is in progress it has suspended the threads, and we would not be able to suspend the threads.
+//
+// Future Consideration:
+// =====================
+// We could track "trending" methods, as methods decay out so we can keep only "trending" variants
+// in the code manager and kick out "past" hot methods.
+//
+// Jitting in the background:
+// ==========================
+// Once we have the hot methods at a given point in time, we JIT them. The decision to JIT is configurable
+// by configuring the number of times a method is seen in samples before we would JIT it.
+// For example, if we are sampling every 10 msec and if we expect methods that spend at least 1 second
+// to be hot, then the number of times to see this method is roughly, 100, it is best to be conservative
+// with this number.
+//
+// Note that we JIT the evolving list of methods, without knowing ahead of time that the methods we JIT
+// will be the final top "n" hot methods due to the lack of knowledge of when the process will end.
+// This means we would over-JIT but this only yields results with false negatives.
+//
+// Currently we JIT in the background only once (with the current goal of getting a trace.)
+//
+// Note:
+// =====
+// o To run the JIT in the background, we try our best to JIT in the same app domain in which the original
+// JITting happened. But if we fail to acquire (ngen'ed method) or enter (unloaded domain) the original domain,
+// we then try to JIT it under the thread's app domain in which the method was last seen to be executing.
+//
+// o The JIT to use is configurable with COMPlus_AltJitName when COMPlus_StackSampling is enabled.
+//
+// o One use case is to collect traces as an .mc file from SuperPMI Shim JIT.
+//
+// Jitting parameters:
+// ==========================
+// The prestub tells us at JITting time using "RecordJittingInfo" to record the parameters used to JIT
+// originally. We use these parameters to JIT in the background when we decide to JIT the method.
+//
+
+
+#include "common.h"
+#include "corjit.h"
+#include "stacksampler.h"
+#include "threadsuspend.h"
+
+#ifdef FEATURE_STACK_SAMPLING
+
+// Global instance of the sampler
+StackSampler* g_pStackSampler = nullptr;
+
+// Create an instance of the stack sampler if sampling is enabled.
+void StackSampler::Init()
+{
+ STANDARD_VM_CONTRACT;
+
+ bool samplingEnabled = (CLRConfig::GetConfigValue(CLRConfig::UNSUPPORTED_StackSamplingEnabled) != 0);
+ if (samplingEnabled)
+ {
+ g_pStackSampler = new (nothrow) StackSampler();
+ }
+}
+
+// ThreadProc for performing sampling and JITting.
+/* static */
+DWORD __stdcall StackSampler::SamplingThreadProc(void* arg)
+{
+ WRAPPER_NO_CONTRACT;
+
+ StackSampler* pThis = (StackSampler*) arg;
+ pThis->ThreadProc();
+ return 0;
+}
+
+// Constructor
+StackSampler::StackSampler()
+ : m_nSampleAfter(0)
+ , m_nSampleEvery(s_knDefaultSamplingIntervalMsec)
+ , m_nNumMethods(s_knDefaultNumMethods)
+ , m_crstJitInfo(CrstStackSampler, (CrstFlags) (CRST_UNSAFE_ANYMODE))
+{
+ // When to start sampling after the thread launch.
+ int nSampleAfter = CLRConfig::GetConfigValue(CLRConfig::UNSUPPORTED_StackSamplingAfter);
+ if (nSampleAfter != INT_MAX && nSampleAfter >= 0)
+ {
+ m_nSampleAfter = nSampleAfter;
+ }
+
+ // How frequently to sample.
+ int nSampleEvery = CLRConfig::GetConfigValue(CLRConfig::UNSUPPORTED_StackSamplingEvery);
+ if (nSampleEvery != INT_MAX && nSampleEvery > 0)
+ {
+ m_nSampleEvery = nSampleEvery;
+ }
+
+ // Max number of methods to track.
+ int nNumMethods = CLRConfig::GetConfigValue(CLRConfig::UNSUPPORTED_StackSamplingNumMethods);
+ if (nNumMethods != INT_MAX && nNumMethods > 0)
+ {
+ m_nNumMethods = nNumMethods;
+ }
+
+ // Launch the thread.
+ m_pThread = SetupUnstartedThread();
+ m_pThread->SetBackground(TRUE);
+
+ // Passing "this" to the thread in the constructor.
+ if (m_pThread->CreateNewThread(1*1024*1024, SamplingThreadProc, this))
+ {
+ m_pThread->StartThread();
+ }
+}
+
+// Is "pMD" a good method, that is suitable for tracking as HOT and
+// JITting in the background.
+bool IsGoodMethodDesc(MethodDesc* pMD)
+{
+ LIMITED_METHOD_CONTRACT;
+ return !(pMD == nullptr || !pMD->IsIL() || pMD->IsUnboxingStub() || pMD->GetMethodTable()->Collectible());
+}
+
+//
+// An opportunity to record the parameters passed to the JIT at the time of JITting this method.
+/* static */
+void StackSampler::RecordJittingInfo(MethodDesc* pMD, DWORD dwFlags, DWORD dwFlags2)
+{
+ WRAPPER_NO_CONTRACT;
+ if (g_pStackSampler == nullptr)
+ {
+ return;
+ }
+ // Skip if this is not a good method desc.
+ if (!IsGoodMethodDesc(pMD))
+ {
+ return;
+ }
+ // Record in the hash map.
+ g_pStackSampler->RecordJittingInfoInternal(pMD, dwFlags);
+}
+
+void StackSampler::RecordJittingInfoInternal(MethodDesc* pMD, DWORD dwFlags)
+{
+ ADID dwDomainId = GetThread()->GetDomain()->GetId();
+ JitInfoHashEntry entry(pMD, dwDomainId);
+
+ // Record the domain in the hash map.
+ {
+ CrstHolder ch(&m_crstJitInfo);
+ m_jitInfo.AddOrReplace(entry);
+ }
+}
+
+// Obtain the domain ID in which the method was originally JITted, if
+// it was never JITted (Ngened) or the original app domain was unloaded
+// use the "defaultId" supplied.
+ADID StackSampler::GetDomainId(MethodDesc* pMD, const ADID& defaultId)
+{
+ ADID adId;
+ BOOL bPresent = FALSE;
+ {
+ CrstHolder ch(&m_crstJitInfo);
+ bPresent = m_jitInfo.Lookup(pMD, &adId);
+ }
+ if (bPresent != FALSE)
+ {
+ AppDomainFromIDHolder pDomain(adId, FALSE);
+ if (!pDomain.IsUnloaded())
+ {
+ return adId;
+ }
+ }
+ return defaultId;
+}
+
+// Stack walk callback data.
+struct WalkInfo
+{
+ StackSampler* pThis;
+
+ // The thread in which the walk is happening and the method is executing.
+ // Used to obtain the app domain.
+ Thread* pMdThread;
+};
+
+// Visitor for stack walk callback.
+StackWalkAction StackSampler::StackWalkCallback(CrawlFrame* pCf, VOID* data)
+{
+ WRAPPER_NO_CONTRACT;
+
+ WalkInfo* info = (WalkInfo*) data;
+ return ((StackSampler*) info->pThis)->CrawlFrameVisitor(pCf, info->pMdThread);
+}
+
+// Stack walk visitor helper to maintain the hash map of method desc, their count
+// and the thread's domain in which the method is executing.
+StackWalkAction StackSampler::CrawlFrameVisitor(CrawlFrame* pCf, Thread* pMdThread)
+{
+ CONTRACTL
+ {
+ THROWS;
+ GC_NOTRIGGER;
+ SO_TOLERANT;
+ MODE_ANY;
+ }
+ CONTRACTL_END;
+
+ MethodDesc* pMD = pCf->GetFunction();
+
+ // Filter out methods we don't care about
+ if (!IsGoodMethodDesc(pMD))
+ {
+ return SWA_CONTINUE;
+ }
+
+ // Lookup the method desc and obtain info.
+ ADID adId = pMdThread->GetDomain()->GetId();
+ CountInfo info(adId);
+ m_countInfo.Lookup(pMD, &info);
+
+ // Record the current domain ID of the method's thread, i.e.,
+ // the method is last known to be executing.
+ info.adDomainId = adId;
+ info.uCount++;
+
+ // Put the info back.
+ m_countInfo.AddOrReplace(CountInfoHashEntry(pMD, info));
+
+ // We got the top good one, skip.
+ return SWA_ABORT;
+}
+
+// Thread routine that suspends the runtime, walks the other threads' stacks to get the
+// top managed method. Restarts the runtime after samples are collected. Identifies top
+// methods from the samples and re-JITs them in the background.
+void StackSampler::ThreadProc()
+{
+ CONTRACTL
+ {
+ THROWS;
+ GC_TRIGGERS;
+ MODE_ANY;
+ SO_INTOLERANT;
+ }
+ CONTRACTL_END;
+
+ // Complete the thread init.
+ if (!m_pThread->HasStarted())
+ {
+ return;
+ }
+
+ BEGIN_SO_INTOLERANT_CODE(m_pThread);
+
+ // User asked us to sample after certain time.
+ m_pThread->UserSleep(m_nSampleAfter);
+
+ WalkInfo info = { this, nullptr };
+
+ while (true)
+ {
+ EX_TRY
+ {
+ // Suspend the runtime.
+ ThreadSuspend::SuspendEE(ThreadSuspend::SUSPEND_OTHER);
+
+ // Walk all other threads.
+ Thread* pThread = nullptr;
+ while ((pThread = ThreadStore::GetThreadList(pThread)) != nullptr)
+ {
+ if (pThread == m_pThread)
+ {
+ continue;
+ }
+ // TODO: Detect if thread is suspended by user before we suspended and skip.
+
+ info.pMdThread = pThread;
+
+ // Walk the frames.
+ pThread->StackWalkFrames(StackWalkCallback, &info, FUNCTIONSONLY | ALLOW_ASYNC_STACK_WALK);
+ }
+
+ // Restart the runtime.
+ ThreadSuspend::RestartEE(FALSE, TRUE);
+
+ // JIT the methods that frequently occur in samples.
+ JitFrequentMethodsInSamples();
+ }
+ EX_CATCH
+ {
+ }
+ EX_END_CATCH(SwallowAllExceptions);
+
+ // User asked us to sample every few seconds.
+ // TODO: Measure time to JIT using CycleTimer and subtract from the time we sleep every time.
+ m_pThread->UserSleep(m_nSampleEvery);
+ }
+
+ END_SO_INTOLERANT_CODE;
+}
+
+// Find the most frequent method in the samples and JIT them.
+void StackSampler::JitFrequentMethodsInSamples()
+{
+ struct Count
+ {
+ MethodDesc* pMD;
+ CountInfo info;
+
+ static int __cdecl Decreasing(const void* e1, const void* e2)
+ {
+ return ((Count*) e2)->info.uCount - ((Count*) e1)->info.uCount;
+ }
+ };
+
+ // We want to keep a max-heap of the top frequent methods in the samples.
+ NewHolder<Count> freq(new (nothrow) Count[m_nNumMethods]);
+
+ //
+ // For each element in the samples, call it incoming, add to the "frequent" list
+ // if the list has space to hold the incoming element.
+ //
+ // If the list doesn't have space, replace the min frequent element in the list
+ // with the incoming element, if the latter is more frequent.
+ //
+ unsigned uLength = 0;
+ for (CountInfoHash::Iterator iter = m_countInfo.Begin(), end = m_countInfo.End(); iter != end; iter++)
+ {
+ Count c = { (*iter).Key(), (*iter).Value() };
+
+ // Is the list full? Drop the min element if incoming is more frequent.
+ if (uLength == m_nNumMethods)
+ {
+ // Find the min element and the min index.
+ unsigned uMinIndex = 0;
+ unsigned uMin = freq[0].info.uCount;
+ for (unsigned i = 1; i < uLength; ++i)
+ {
+ if (uMin > freq[i].info.uCount)
+ {
+ uMin = freq[i].info.uCount;
+ uMinIndex = i;
+ }
+ }
+ if (uMin < c.info.uCount)
+ {
+ freq[uMinIndex] = c;
+ }
+ }
+ // List is not full, just add the incoming element.
+ else
+ {
+ freq[uLength] = c;
+ uLength++;
+ }
+ }
+
+ // Sort by most frequent element first.
+ qsort(freq, uLength, sizeof(Count), Count::Decreasing);
+
+#ifdef _DEBUG
+ LOG((LF_JIT, LL_INFO100000, "-----------HOT METHODS-------\n"));
+ for (unsigned i = 0; i < uLength; ++i)
+ {
+ // printf("%s:%s, %u\n", freq[i].pMD->GetMethodTable()->GetClass()->GetDebugClassName(), freq[i].pMD->GetName(), freq[i].info.uCount);
+ LOG((LF_JIT, LL_INFO100000, "%s:%s, %u\n", freq[i].pMD->GetMethodTable()->GetClass()->GetDebugClassName(), freq[i].pMD->GetName(), freq[i].info.uCount));
+ }
+ LOG((LF_JIT, LL_INFO100000, "-----------------------------\n"));
+#endif
+
+ // Do the JITting.
+ for (unsigned i = 0; i < uLength; ++i)
+ {
+ // If not already JITted and the method is frequent enough to be important.
+ if (!freq[i].info.fJitted && freq[i].info.uCount > s_knDefaultCountForImportance)
+ {
+ // Try to get the original app domain ID in which the method was JITTed, if not
+ // use the app domain ID the method was last seen executing.
+ ADID adId = GetDomainId(freq[i].pMD, freq[i].info.adDomainId);
+ JitAndCollectTrace(freq[i].pMD, adId);
+ }
+ }
+}
+
+// Invoke the JIT for the method desc. Switch to the appropriate domain.
+void StackSampler::JitAndCollectTrace(MethodDesc* pMD, const ADID& adId)
+{
+ CONTRACTL
+ {
+ NOTHROW;
+ GC_TRIGGERS;
+ MODE_ANY;
+ }
+ CONTRACTL_END;
+
+ // Indicate to the JIT or the JIT interface that we are JITting
+ // in the background for stack sampling.
+ DWORD dwFlags2 = CORJIT_FLG2_SAMPLING_JIT_BACKGROUND;
+
+ _ASSERTE(pMD->IsIL());
+
+ EX_TRY
+ {
+ ENTER_DOMAIN_ID(adId)
+ {
+ GCX_PREEMP();
+
+ COR_ILMETHOD_DECODER::DecoderStatus status;
+ NewHolder<COR_ILMETHOD_DECODER> pDecoder(
+ new COR_ILMETHOD_DECODER(pMD->GetILHeader(),
+ pMD->GetMDImport(),
+ &status));
+
+#ifdef _DEBUG
+ LOG((LF_JIT, LL_INFO100000, "Jitting the hot method desc using SuperPMI in the background thread -> "));
+ LOG((LF_JIT, LL_INFO100000, "%s:%s\n", pMD->GetMethodTable()->GetClass()->GetDebugClassName(), pMD->GetName()));
+#endif
+
+ PCODE pCode = UnsafeJitFunction(pMD, pDecoder, 0, dwFlags2);
+ }
+ END_DOMAIN_TRANSITION;
+
+ // Update that this method has been already JITted.
+ CountInfo info((ADID) DefaultADID);
+ m_countInfo.Lookup(pMD, &info);
+ info.fJitted = true;
+ m_countInfo.AddOrReplace(CountInfoHashEntry(pMD, info));
+ }
+ EX_CATCH
+ {
+ }
+ EX_END_CATCH(SwallowAllExceptions)
+
+}
+
+#endif // FEATURE_STACK_SAMPLING