// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. #include "common.h" #ifdef FEATURE_TIERED_COMPILATION #include "callcounting.h" #include "threadsuspend.h" #ifndef DACCESS_COMPILE extern "C" void STDCALL OnCallCountThresholdReachedStub(); #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // CallCountingStub #ifndef DACCESS_COMPILE const PCODE CallCountingStub::TargetForThresholdReached = (PCODE)GetEEFuncEntryPoint(OnCallCountThresholdReachedStub); #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // CallCountingManager::CallCountingInfo #ifndef DACCESS_COMPILE CallCountingManager::CallCountingInfo::CallCountingInfo(NativeCodeVersion codeVersion) : m_codeVersion(codeVersion), m_callCountingStub(nullptr), m_remainingCallCount(0), m_stage(Stage::Disabled) { WRAPPER_NO_CONTRACT; _ASSERTE(!codeVersion.IsNull()); } CallCountingManager::CallCountingInfo * CallCountingManager::CallCountingInfo::CreateWithCallCountingDisabled(NativeCodeVersion codeVersion) { CONTRACTL { THROWS; GC_NOTRIGGER; MODE_ANY; } CONTRACTL_END; return new CallCountingInfo(codeVersion); } CallCountingManager::CallCountingInfo::CallCountingInfo(NativeCodeVersion codeVersion, CallCount callCountThreshold) : m_codeVersion(codeVersion), m_callCountingStub(nullptr), m_remainingCallCount(callCountThreshold), m_stage(Stage::StubIsNotActive) { WRAPPER_NO_CONTRACT; _ASSERTE(!codeVersion.IsNull()); _ASSERTE(callCountThreshold != 0); } CallCountingManager::CallCountingInfo::~CallCountingInfo() { WRAPPER_NO_CONTRACT; _ASSERTE(m_stage == Stage::Complete); _ASSERTE(m_callCountingStub == nullptr); } #endif // !DACCESS_COMPILE CallCountingManager::PTR_CallCountingInfo CallCountingManager::CallCountingInfo::From(PTR_CallCount remainingCallCountCell) { WRAPPER_NO_CONTRACT; _ASSERTE(remainingCallCountCell != nullptr); return PTR_CallCountingInfo(dac_cast(remainingCallCountCell) - offsetof(CallCountingInfo, m_remainingCallCount)); } NativeCodeVersion CallCountingManager::CallCountingInfo::GetCodeVersion() const { WRAPPER_NO_CONTRACT; return m_codeVersion; } #ifndef DACCESS_COMPILE const CallCountingStub *CallCountingManager::CallCountingInfo::GetCallCountingStub() const { WRAPPER_NO_CONTRACT; _ASSERTE(m_stage != Stage::Disabled); return m_callCountingStub; } void CallCountingManager::CallCountingInfo::SetCallCountingStub(const CallCountingStub *callCountingStub) { WRAPPER_NO_CONTRACT; _ASSERTE(g_pConfig->TieredCompilation_UseCallCountingStubs()); _ASSERTE(m_stage == Stage::StubIsNotActive); _ASSERTE(m_callCountingStub == nullptr); _ASSERTE(callCountingStub != nullptr); ++s_callCountingStubCount; m_callCountingStub = callCountingStub; } void CallCountingManager::CallCountingInfo::ClearCallCountingStub() { WRAPPER_NO_CONTRACT; _ASSERTE(m_stage == Stage::StubIsNotActive || m_stage == Stage::Complete); _ASSERTE(m_callCountingStub != nullptr); m_callCountingStub = nullptr; // The total and completed stub counts are updated along with deleting stubs } PTR_CallCount CallCountingManager::CallCountingInfo::GetRemainingCallCountCell() { WRAPPER_NO_CONTRACT; _ASSERTE(m_stage != Stage::Disabled); //_ASSERTE(m_callCountingStub != nullptr); return &m_remainingCallCount; } #endif // !DACCESS_COMPILE CallCountingManager::CallCountingInfo::Stage CallCountingManager::CallCountingInfo::GetStage() const { WRAPPER_NO_CONTRACT; return m_stage; } #ifndef DACCESS_COMPILE FORCEINLINE void CallCountingManager::CallCountingInfo::SetStage(Stage stage) { WRAPPER_NO_CONTRACT; _ASSERTE(m_stage != Stage::Disabled); _ASSERTE(stage <= Stage::Complete); switch (stage) { case Stage::StubIsNotActive: _ASSERTE(m_stage == Stage::StubMayBeActive); _ASSERTE(m_callCountingStub != nullptr); _ASSERTE(s_activeCallCountingStubCount != 0); --s_activeCallCountingStubCount; break; case Stage::StubMayBeActive: _ASSERTE(m_callCountingStub != nullptr); FALLTHROUGH; case Stage::PendingCompletion: _ASSERTE(m_stage == Stage::StubIsNotActive || m_stage == Stage::StubMayBeActive); if (m_stage == Stage::StubIsNotActive && m_callCountingStub != nullptr) { ++s_activeCallCountingStubCount; } break; case Stage::Complete: _ASSERTE(m_stage != Stage::Complete); if (m_callCountingStub != nullptr) { if (m_stage != Stage::StubIsNotActive) { _ASSERTE(s_activeCallCountingStubCount != 0); --s_activeCallCountingStubCount; } ++s_completedCallCountingStubCount; } break; default: UNREACHABLE(); } m_stage = stage; } #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // CallCountingManager::CallCountingInfo::CodeVersionHashTraits CallCountingManager::CallCountingInfo::CodeVersionHashTraits::key_t CallCountingManager::CallCountingInfo::CodeVersionHashTraits::GetKey(const element_t &e) { WRAPPER_NO_CONTRACT; return e->GetCodeVersion(); } BOOL CallCountingManager::CallCountingInfo::CodeVersionHashTraits::Equals(const key_t &k1, const key_t &k2) { WRAPPER_NO_CONTRACT; return k1 == k2; } CallCountingManager::CallCountingInfo::CodeVersionHashTraits::count_t CallCountingManager::CallCountingInfo::CodeVersionHashTraits::Hash(const key_t &k) { WRAPPER_NO_CONTRACT; return (count_t)dac_cast(k.GetMethodDesc()) + k.GetVersionId(); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // CallCountingManager::CallCountingStubAllocator CallCountingManager::CallCountingStubAllocator::CallCountingStubAllocator() : m_heap(nullptr) { WRAPPER_NO_CONTRACT; } CallCountingManager::CallCountingStubAllocator::~CallCountingStubAllocator() { CONTRACTL { NOTHROW; GC_NOTRIGGER; MODE_ANY; } CONTRACTL_END; #ifndef DACCESS_COMPILE LoaderHeap *heap = m_heap; if (heap != nullptr) { delete m_heap; } #endif } #ifndef DACCESS_COMPILE void CallCountingManager::CallCountingStubAllocator::Reset() { WRAPPER_NO_CONTRACT; this->~CallCountingStubAllocator(); new(this) CallCountingStubAllocator(); } const CallCountingStub *CallCountingManager::CallCountingStubAllocator::AllocateStub( CallCount *remainingCallCountCell, PCODE targetForMethod) { CONTRACTL { THROWS; GC_NOTRIGGER; MODE_ANY; } CONTRACTL_END; LoaderHeap *heap = m_heap; if (heap == nullptr) { heap = AllocateHeap(); } SIZE_T sizeInBytes = sizeof(CallCountingStub); AllocMemHolder allocationAddressHolder(heap->AllocAlignedMem(sizeInBytes, 1)); CallCountingStub *stub = (CallCountingStub*)(void*)allocationAddressHolder; allocationAddressHolder.SuppressRelease(); stub->Initialize(targetForMethod, remainingCallCountCell); return stub; } #if defined(TARGET_ARM64) && defined(TARGET_UNIX) #define ENUM_PAGE_SIZE(size) \ extern "C" void CallCountingStubCode##size(); \ extern "C" void CallCountingStubCode##size##_End(); ENUM_PAGE_SIZES #undef ENUM_PAGE_SIZE #else extern "C" void CallCountingStubCode(); extern "C" void CallCountingStubCode_End(); #endif #ifdef TARGET_X86 extern "C" size_t CallCountingStubCode_RemainingCallCountCell_Offset; extern "C" size_t CallCountingStubCode_TargetForMethod_Offset; extern "C" size_t CallCountingStubCode_TargetForThresholdReached_Offset; #define SYMBOL_VALUE(name) ((size_t)&name) #endif #if defined(TARGET_ARM64) && defined(TARGET_UNIX) void (*CallCountingStub::CallCountingStubCode)(); #endif #ifndef DACCESS_COMPILE void CallCountingStub::StaticInitialize() { #if defined(TARGET_ARM64) && defined(TARGET_UNIX) int pageSize = GetStubCodePageSize(); #define ENUM_PAGE_SIZE(size) \ case size: \ CallCountingStubCode = CallCountingStubCode##size; \ _ASSERTE((SIZE_T)((BYTE*)CallCountingStubCode##size##_End - (BYTE*)CallCountingStubCode##size) <= CallCountingStub::CodeSize); \ break; switch (pageSize) { ENUM_PAGE_SIZES default: EEPOLICY_HANDLE_FATAL_ERROR_WITH_MESSAGE(COR_E_EXECUTIONENGINE, W("Unsupported OS page size")); } #undef ENUM_PAGE_SIZE #else _ASSERTE((SIZE_T)((BYTE*)CallCountingStubCode_End - (BYTE*)CallCountingStubCode) <= CallCountingStub::CodeSize); #endif } #endif // DACCESS_COMPILE void CallCountingStub::GenerateCodePage(BYTE* pageBase, BYTE* pageBaseRX, SIZE_T pageSize) { #ifdef TARGET_X86 int totalCodeSize = (pageSize / CallCountingStub::CodeSize) * CallCountingStub::CodeSize; for (int i = 0; i < totalCodeSize; i += CallCountingStub::CodeSize) { memcpy(pageBase + i, (const void*)CallCountingStubCode, CallCountingStub::CodeSize); // Set absolute addresses of the slots in the stub BYTE* pCounterSlot = pageBaseRX + i + pageSize + offsetof(CallCountingStubData, RemainingCallCountCell); *(BYTE**)(pageBase + i + SYMBOL_VALUE(CallCountingStubCode_RemainingCallCountCell_Offset)) = pCounterSlot; BYTE* pTargetSlot = pageBaseRX + i + pageSize + offsetof(CallCountingStubData, TargetForMethod); *(BYTE**)(pageBase + i + SYMBOL_VALUE(CallCountingStubCode_TargetForMethod_Offset)) = pTargetSlot; BYTE* pCountReachedZeroSlot = pageBaseRX + i + pageSize + offsetof(CallCountingStubData, TargetForThresholdReached); *(BYTE**)(pageBase + i + SYMBOL_VALUE(CallCountingStubCode_TargetForThresholdReached_Offset)) = pCountReachedZeroSlot; } #else // TARGET_X86 FillStubCodePage(pageBase, (const void*)PCODEToPINSTR((PCODE)CallCountingStubCode), CallCountingStub::CodeSize, pageSize); #endif } NOINLINE LoaderHeap *CallCountingManager::CallCountingStubAllocator::AllocateHeap() { CONTRACTL { THROWS; GC_NOTRIGGER; MODE_ANY; } CONTRACTL_END; _ASSERTE(m_heap == nullptr); LoaderHeap *heap = new LoaderHeap(0, 0, &m_heapRangeList, UnlockedLoaderHeap::HeapKind::Interleaved, true /* fUnlocked */, CallCountingStub::GenerateCodePage, CallCountingStub::CodeSize); m_heap = heap; return heap; } #endif // !DACCESS_COMPILE bool CallCountingManager::CallCountingStubAllocator::IsStub(TADDR entryPoint) { WRAPPER_NO_CONTRACT; _ASSERTE(entryPoint != (TADDR)NULL); return !!m_heapRangeList.IsInRange(entryPoint); } #ifdef DACCESS_COMPILE void CallCountingManager::CallCountingStubAllocator::EnumerateHeapRanges(CLRDataEnumMemoryFlags flags) { WRAPPER_NO_CONTRACT; m_heapRangeList.EnumMemoryRegions(flags); } #endif // DACCESS_COMPILE //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // CallCountingManager::MethodDescForwarderStubHashTraits CallCountingManager::MethodDescForwarderStubHashTraits::key_t CallCountingManager::MethodDescForwarderStubHashTraits::GetKey(const element_t &e) { WRAPPER_NO_CONTRACT; return e->GetMethodDesc(); } BOOL CallCountingManager::MethodDescForwarderStubHashTraits::Equals(const key_t &k1, const key_t &k2) { WRAPPER_NO_CONTRACT; return k1 == k2; } CallCountingManager::MethodDescForwarderStubHashTraits::count_t CallCountingManager::MethodDescForwarderStubHashTraits::Hash(const key_t &k) { WRAPPER_NO_CONTRACT; return (count_t)(size_t)k; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // CallCountingManager::CallCountingManagerHashTraits CallCountingManager::CallCountingManagerHashTraits::key_t CallCountingManager::CallCountingManagerHashTraits::GetKey(const element_t &e) { WRAPPER_NO_CONTRACT; return e; } BOOL CallCountingManager::CallCountingManagerHashTraits::Equals(const key_t &k1, const key_t &k2) { WRAPPER_NO_CONTRACT; return k1 == k2; } CallCountingManager::CallCountingManagerHashTraits::count_t CallCountingManager::CallCountingManagerHashTraits::Hash(const key_t &k) { WRAPPER_NO_CONTRACT; return (count_t)dac_cast(k); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // CallCountingManager CallCountingManager::PTR_CallCountingManagerHash CallCountingManager::s_callCountingManagers = PTR_NULL; COUNT_T CallCountingManager::s_callCountingStubCount = 0; COUNT_T CallCountingManager::s_activeCallCountingStubCount = 0; COUNT_T CallCountingManager::s_completedCallCountingStubCount = 0; CallCountingManager::CallCountingManager() { CONTRACTL { THROWS; GC_NOTRIGGER; MODE_ANY; } CONTRACTL_END; #ifndef DACCESS_COMPILE CodeVersionManager::LockHolder codeVersioningLockHolder; s_callCountingManagers->Add(this); #endif } CallCountingManager::~CallCountingManager() { CONTRACTL { NOTHROW; GC_NOTRIGGER; MODE_ANY; } CONTRACTL_END; #ifndef DACCESS_COMPILE CodeVersionManager::LockHolder codeVersioningLockHolder; for (auto itEnd = m_callCountingInfoByCodeVersionHash.End(), it = m_callCountingInfoByCodeVersionHash.Begin(); it != itEnd; ++it) { CallCountingInfo *callCountingInfo = *it; delete callCountingInfo; } s_callCountingManagers->Remove(this); #endif } #ifndef DACCESS_COMPILE void CallCountingManager::StaticInitialize() { WRAPPER_NO_CONTRACT; s_callCountingManagers = PTR_CallCountingManagerHash(new CallCountingManagerHash()); CallCountingStub::StaticInitialize(); } #endif bool CallCountingManager::IsCallCountingEnabled(NativeCodeVersion codeVersion) { CONTRACTL { NOTHROW; GC_NOTRIGGER; MODE_ANY; } CONTRACTL_END; _ASSERTE(!codeVersion.IsNull()); _ASSERTE(codeVersion.IsDefaultVersion()); _ASSERTE(codeVersion.GetMethodDesc()->IsEligibleForTieredCompilation()); CodeVersionManager::LockHolder codeVersioningLockHolder; PTR_CallCountingInfo callCountingInfo = m_callCountingInfoByCodeVersionHash.Lookup(codeVersion); return callCountingInfo == NULL || callCountingInfo->GetStage() != CallCountingInfo::Stage::Disabled; } #ifndef DACCESS_COMPILE void CallCountingManager::DisableCallCounting(NativeCodeVersion codeVersion) { CONTRACTL { THROWS; GC_NOTRIGGER; MODE_ANY; } CONTRACTL_END; _ASSERTE(!codeVersion.IsNull()); _ASSERTE(codeVersion.IsDefaultVersion()); _ASSERTE(codeVersion.GetMethodDesc()->IsEligibleForTieredCompilation()); CodeVersionManager::LockHolder codeVersioningLockHolder; CallCountingInfo *callCountingInfo = m_callCountingInfoByCodeVersionHash.Lookup(codeVersion); if (callCountingInfo != nullptr) { // Call counting may already have been disabled due to the possibility of concurrent or reentering JIT of the same // native code version of a method. The call counting info is created with call counting enabled or disabled and it // cannot be changed thereafter for consistency in dependents of the info. _ASSERTE(callCountingInfo->GetStage() == CallCountingInfo::Stage::Disabled); return; } NewHolder callCountingInfoHolder = CallCountingInfo::CreateWithCallCountingDisabled(codeVersion); m_callCountingInfoByCodeVersionHash.Add(callCountingInfoHolder); callCountingInfoHolder.SuppressRelease(); } // Returns true if the code entry point was updated to reflect the active code version, false otherwise. In normal paths, the // code entry point is not updated only when the use of call counting stubs is disabled, as in that case returning to the // prestub is necessary for further call counting. On exception, the code entry point may or may not have been updated and it's // up to the caller to decide how to proceed. bool CallCountingManager::SetCodeEntryPoint( NativeCodeVersion activeCodeVersion, PCODE codeEntryPoint, bool wasMethodCalled, bool *createTieringBackgroundWorkerRef) { CONTRACTL { THROWS; GC_NOTRIGGER; PRECONDITION(!activeCodeVersion.IsNull()); MODE_ANY; } CONTRACTL_END; MethodDesc *methodDesc = activeCodeVersion.GetMethodDesc(); _ASSERTE(!methodDesc->MayHaveEntryPointSlotsToBackpatch() || MethodDescBackpatchInfoTracker::IsLockOwnedByCurrentThread()); _ASSERTE(CodeVersionManager::IsLockOwnedByCurrentThread()); _ASSERTE( activeCodeVersion == methodDesc->GetCodeVersionManager()->GetActiveILCodeVersion(methodDesc).GetActiveNativeCodeVersion(methodDesc)); _ASSERTE(codeEntryPoint != (PCODE)NULL); _ASSERTE(codeEntryPoint == activeCodeVersion.GetNativeCode()); _ASSERTE(!wasMethodCalled || createTieringBackgroundWorkerRef != nullptr); _ASSERTE(createTieringBackgroundWorkerRef == nullptr || !*createTieringBackgroundWorkerRef); if (!methodDesc->IsEligibleForTieredCompilation() || ( // For a default code version that is not tier 0, call counting will have been disabled by this time (checked // below). Avoid the redundant and not-insignificant expense of GetOptimizationTier() on a default code version. !activeCodeVersion.IsDefaultVersion() && activeCodeVersion.IsFinalTier() ) || !g_pConfig->TieredCompilation_CallCounting()) { methodDesc->SetCodeEntryPoint(codeEntryPoint); return true; } const CallCountingStub *callCountingStub; CallCountingManager *callCountingManager = methodDesc->GetLoaderAllocator()->GetCallCountingManager(); CallCountingInfoByCodeVersionHash &callCountingInfoByCodeVersionHash = callCountingManager->m_callCountingInfoByCodeVersionHash; CallCountingInfo *callCountingInfo = callCountingInfoByCodeVersionHash.Lookup(activeCodeVersion); do { if (callCountingInfo != nullptr) { _ASSERTE(callCountingInfo->GetCodeVersion() == activeCodeVersion); CallCountingInfo::Stage callCountingStage = callCountingInfo->GetStage(); if (callCountingStage >= CallCountingInfo::Stage::PendingCompletion) { // Call counting is disabled, complete, or pending completion. The pending completion stage here would be // relatively rare, let it be handled elsewhere. methodDesc->SetCodeEntryPoint(codeEntryPoint); return true; } _ASSERTE(!activeCodeVersion.IsFinalTier()); // If the tiering delay is active, postpone further work if (GetAppDomain() ->GetTieredCompilationManager() ->TrySetCodeEntryPointAndRecordMethodForCallCounting(methodDesc, codeEntryPoint)) { if (callCountingStage == CallCountingInfo::Stage::StubMayBeActive) { callCountingInfo->SetStage(CallCountingInfo::Stage::StubIsNotActive); } return true; } do { if (!wasMethodCalled) { break; } CallCount remainingCallCount = --*callCountingInfo->GetRemainingCallCountCell(); if (remainingCallCount != 0) { break; } callCountingInfo->SetStage(CallCountingInfo::Stage::PendingCompletion); if (!activeCodeVersion.GetILCodeVersion().HasAnyOptimizedNativeCodeVersion(activeCodeVersion)) { GetAppDomain() ->GetTieredCompilationManager() ->AsyncPromoteToTier1(activeCodeVersion, createTieringBackgroundWorkerRef); } methodDesc->SetCodeEntryPoint(codeEntryPoint); callCountingInfo->SetStage(CallCountingInfo::Stage::Complete); return true; } while (false); callCountingStub = callCountingInfo->GetCallCountingStub(); if (callCountingStub != nullptr) { break; } } else { _ASSERTE(!activeCodeVersion.IsFinalTier()); // If the tiering delay is active, postpone further work if (GetAppDomain() ->GetTieredCompilationManager() ->TrySetCodeEntryPointAndRecordMethodForCallCounting(methodDesc, codeEntryPoint)) { return true; } CallCount callCountThreshold = g_pConfig->TieredCompilation_CallCountThreshold(); _ASSERTE(callCountThreshold != 0); // Let's tier up all cast helpers faster than other methods. This is because we want to import them as // direct calls in codegen and they need to be promoted earlier than their callers. if (methodDesc->GetMethodTable() == g_pCastHelpers) { callCountThreshold = max(1, (CallCount)(callCountThreshold / 2)); } NewHolder callCountingInfoHolder = new CallCountingInfo(activeCodeVersion, callCountThreshold); callCountingInfoByCodeVersionHash.Add(callCountingInfoHolder); callCountingInfo = callCountingInfoHolder.Extract(); } if (!g_pConfig->TieredCompilation_UseCallCountingStubs()) { // Call counting is not yet complete, so reset or don't set the code entry point to continue counting calls if (wasMethodCalled) { return false; } // This path is reached after activating a code version when publishing its code entry point. The method may // currently be pointing to the code entry point of a different code version, so an explicit reset is necessary. methodDesc->ResetCodeEntryPoint(); return true; } callCountingStub = callCountingManager->m_callCountingStubAllocator.AllocateStub( callCountingInfo->GetRemainingCallCountCell(), codeEntryPoint); callCountingInfo->SetCallCountingStub(callCountingStub); } while (false); PCODE callCountingCodeEntryPoint = callCountingStub->GetEntryPoint(); if (methodDesc->MayHaveEntryPointSlotsToBackpatch()) { // The call counting stub should not be the entry point that is called first in the process of a call // - Stubs should be deletable. Many methods will have call counting stubs associated with them, and although the memory // involved is typically insignificant compared to the average memory overhead per method, by steady-state it would // otherwise be unnecessary memory overhead serving no purpose. // - In order to be able to delete a stub, the jitted code of a method cannot be allowed to load the stub as the entry // point of a callee into a register in a GC-safe point that allows for the stub to be deleted before the register is // reused to call the stub. On some processor architectures, perhaps the JIT can guarantee that it would not load the // entry point into a register before the call, but this is not possible on arm32 or arm64. Rather, perhaps the // region containing the load and call would not be considered GC-safe. Calls are considered GC-safe points, and this // may cause many methods that are currently fully interruptible to have to be partially interruptible and record // extra GC info instead. This would be nontrivial and there would be tradeoffs. // - For any method that may have an entry point slot that would be backpatched with the call counting stub's entry // point, a small forwarder stub (precode) is created. The forwarder stub has loader allocator lifetime and forwards to // the larger call counting stub. This is a simple solution for now and seems to have negligible impact. // - Reusing FuncPtrStubs was considered. FuncPtrStubs are currently not used as a code entry point for a virtual or // interface method and may be bypassed. For example, a call may call through the vtable slot, or a devirtualized call // may call through a FuncPtrStub. The target of a FuncPtrStub is a code entry point and is backpatched when a // method's active code entry point changes. Mixing the current use of FuncPtrStubs with the use as a forwarder for // call counting does not seem trivial and would likely complicate its use. There may not be much gain in reusing // FuncPtrStubs, as typically, they are created for only a small percentage of virtual/interface methods. MethodDescForwarderStubHash &methodDescForwarderStubHash = callCountingManager->m_methodDescForwarderStubHash; Precode *forwarderStub = methodDescForwarderStubHash.Lookup(methodDesc); if (forwarderStub == nullptr) { AllocMemTracker forwarderStubAllocationTracker; forwarderStub = Precode::Allocate( methodDesc->GetPrecodeType(), methodDesc, methodDesc->GetLoaderAllocator(), &forwarderStubAllocationTracker); methodDescForwarderStubHash.Add(forwarderStub); forwarderStubAllocationTracker.SuppressRelease(); } forwarderStub->SetTargetInterlocked(callCountingCodeEntryPoint, false); callCountingCodeEntryPoint = forwarderStub->GetEntryPoint(); } else { _ASSERTE(methodDesc->IsVersionableWithPrecode()); } methodDesc->SetCodeEntryPoint(callCountingCodeEntryPoint); callCountingInfo->SetStage(CallCountingInfo::Stage::StubMayBeActive); return true; } extern "C" PCODE STDCALL OnCallCountThresholdReached(TransitionBlock *transitionBlock, TADDR stubIdentifyingToken) { WRAPPER_NO_CONTRACT; return CallCountingManager::OnCallCountThresholdReached(transitionBlock, stubIdentifyingToken); } PCODE CallCountingManager::OnCallCountThresholdReached(TransitionBlock *transitionBlock, TADDR stubIdentifyingToken) { STATIC_CONTRACT_THROWS; STATIC_CONTRACT_GC_TRIGGERS; STATIC_CONTRACT_MODE_COOPERATIVE; PCODE codeEntryPoint = 0; BEGIN_PRESERVE_LAST_ERROR; MAKE_CURRENT_THREAD_AVAILABLE(); #ifdef _DEBUG Thread::ObjectRefFlush(CURRENT_THREAD); #endif // Get the code version from the call counting stub/info in cooperative GC mode to synchronize with deletion. The stub/info // may be deleted only when the runtime is suspended, so when we are in cooperative GC mode it is safe to read from them. NativeCodeVersion codeVersion = CallCountingInfo::From(CallCountingStub::From(stubIdentifyingToken)->GetRemainingCallCountCell())->GetCodeVersion(); MethodDesc *methodDesc = codeVersion.GetMethodDesc(); FrameWithCookie frameWithCookie(transitionBlock, methodDesc); CallCountingHelperFrame *frame = &frameWithCookie; frame->Push(CURRENT_THREAD); INSTALL_MANAGED_EXCEPTION_DISPATCHER; INSTALL_UNWIND_AND_CONTINUE_HANDLER; // The switch to preemptive GC mode no longer guarantees that the stub/info will be valid. Only the code version will be // used going forward under appropriate locking to synchronize further with deletion. GCX_PREEMP_THREAD_EXISTS(CURRENT_THREAD); _ASSERTE(!codeVersion.IsFinalTier()); codeEntryPoint = codeVersion.GetNativeCode(); do { { CallCountingManager *callCountingManager = methodDesc->GetLoaderAllocator()->GetCallCountingManager(); CodeVersionManager::LockHolder codeVersioningLockHolder; CallCountingInfo *callCountingInfo = callCountingManager->m_callCountingInfoByCodeVersionHash.Lookup(codeVersion); if (callCountingInfo == nullptr) { break; } CallCountingInfo::Stage callCountingStage = callCountingInfo->GetStage(); if (callCountingStage >= CallCountingInfo::Stage::PendingCompletion) { break; } // Fully completing call counting for a method is relative expensive. Call counting with stubs is relatively cheap. // Since many methods will typically reach the call count threshold at roughly the same time (a perf spike), // delegate as much of the overhead as possible to the background. This significantly decreases the degree of the // perf spike. callCountingManager->m_callCountingInfosPendingCompletion.Append(callCountingInfo); callCountingInfo->SetStage(CallCountingInfo::Stage::PendingCompletion); } GetAppDomain()->GetTieredCompilationManager()->AsyncCompleteCallCounting(); } while (false); UNINSTALL_UNWIND_AND_CONTINUE_HANDLER; UNINSTALL_MANAGED_EXCEPTION_DISPATCHER; frame->Pop(CURRENT_THREAD); END_PRESERVE_LAST_ERROR; return codeEntryPoint; } COUNT_T CallCountingManager::GetCountOfCodeVersionsPendingCompletion() { CONTRACTL { NOTHROW; GC_NOTRIGGER; MODE_PREEMPTIVE; } CONTRACTL_END; COUNT_T count = 0; CodeVersionManager::LockHolder codeVersioningLockHolder; for (auto itEnd = s_callCountingManagers->End(), it = s_callCountingManagers->Begin(); it != itEnd; ++it) { CallCountingManager *callCountingManager = *it; count += callCountingManager->m_callCountingInfosPendingCompletion.GetCount(); } return count; } void CallCountingManager::CompleteCallCounting() { CONTRACTL { THROWS; GC_TRIGGERS; MODE_PREEMPTIVE; } CONTRACTL_END; _ASSERTE(GetThreadNULLOk() == TieredCompilationManager::GetBackgroundWorkerThread()); AppDomain *appDomain = GetAppDomain(); TieredCompilationManager *tieredCompilationManager = appDomain->GetTieredCompilationManager(); CodeVersionManager *codeVersionManager = appDomain->GetCodeVersionManager(); MethodDescBackpatchInfoTracker::ConditionalLockHolder slotBackpatchLockHolder; CodeVersionManager::LockHolder codeVersioningLockHolder; for (auto itEnd = s_callCountingManagers->End(), it = s_callCountingManagers->Begin(); it != itEnd; ++it) { CallCountingManager *callCountingManager = *it; SArray &callCountingInfosPendingCompletion = callCountingManager->m_callCountingInfosPendingCompletion; COUNT_T callCountingInfoCount = callCountingInfosPendingCompletion.GetCount(); if (callCountingInfoCount == 0) { continue; } CallCountingInfo **callCountingInfos = callCountingInfosPendingCompletion.GetElements(); for (COUNT_T i = 0; i < callCountingInfoCount; ++i) { CallCountingInfo *callCountingInfo = callCountingInfos[i]; CallCountingInfo::Stage callCountingStage = callCountingInfo->GetStage(); if (callCountingStage != CallCountingInfo::Stage::PendingCompletion) { continue; } NativeCodeVersion codeVersion = callCountingInfo->GetCodeVersion(); MethodDesc *methodDesc = codeVersion.GetMethodDesc(); _ASSERTE(codeVersionManager == methodDesc->GetCodeVersionManager()); EX_TRY { if (!codeVersion.GetILCodeVersion().HasAnyOptimizedNativeCodeVersion(codeVersion)) { bool createTieringBackgroundWorker = false; tieredCompilationManager->AsyncPromoteToTier1(codeVersion, &createTieringBackgroundWorker); _ASSERTE(!createTieringBackgroundWorker); // the current thread is the background worker thread } // The active code version may have changed externally after the call counting stub was activated, // deactivating the call counting stub without our knowledge. Check the active code version and determine // what needs to be done. NativeCodeVersion activeCodeVersion = codeVersionManager->GetActiveILCodeVersion(methodDesc).GetActiveNativeCodeVersion(methodDesc); do { if (activeCodeVersion == codeVersion) { methodDesc->SetCodeEntryPoint(activeCodeVersion.GetNativeCode()); break; } // There is at least one case where the IL code version is changed inside the code versioning lock, the // lock is released and reacquired, then the method's code entry point is reset. So if this path is // reached between those locks, the method would still be pointing to the call counting stub. Once the // stub is marked as complete, it may be deleted, so in all cases update the method's code entry point // to ensure that the method is no longer pointing to the call counting stub. if (!activeCodeVersion.IsNull()) { PCODE activeNativeCode = activeCodeVersion.GetNativeCode(); if (activeNativeCode != 0) { methodDesc->SetCodeEntryPoint(activeNativeCode); break; } } methodDesc->ResetCodeEntryPoint(); } while (false); callCountingInfo->SetStage(CallCountingInfo::Stage::Complete); } EX_CATCH { // Avoid abandoning call counting completion for all recorded call counting infos on exception. Since this // is happening on a background thread, following the general policy so far, the exception will be caught, // logged, and ignored anyway, so make an attempt to complete call counting for each item. Individual items // that fail will result in those code versions not getting promoted (similar to elsewhere). STRESS_LOG1(LF_TIEREDCOMPILATION, LL_WARNING, "CallCountingManager::CompleteCallCounting: " "Exception, hr=0x%x\n", GET_EXCEPTION()->GetHR()); } EX_END_CATCH(RethrowTerminalExceptions); } callCountingInfosPendingCompletion.Clear(); if (callCountingInfosPendingCompletion.GetAllocation() > 64) { callCountingInfosPendingCompletion.Trim(); EX_TRY { callCountingInfosPendingCompletion.Preallocate(64); } EX_CATCH { } EX_END_CATCH(RethrowTerminalExceptions); } } } void CallCountingManager::StopAndDeleteAllCallCountingStubs() { CONTRACTL { THROWS; GC_TRIGGERS; MODE_PREEMPTIVE; } CONTRACTL_END; _ASSERTE(GetThreadNULLOk() == TieredCompilationManager::GetBackgroundWorkerThread()); // If a number of call counting stubs have completed, we can try to delete them to reclaim some memory. Deleting // involves suspending the runtime and will delete all call counting stubs, and after that some call counting stubs may // be recreated in the foreground. The threshold is to decrease the impact of both of those overheads. COUNT_T deleteCallCountingStubsAfter = g_pConfig->TieredCompilation_DeleteCallCountingStubsAfter(); if (deleteCallCountingStubsAfter == 0 || s_completedCallCountingStubCount < deleteCallCountingStubsAfter) { return; } TieredCompilationManager *tieredCompilationManager = GetAppDomain()->GetTieredCompilationManager(); ThreadSuspend::SuspendEE(ThreadSuspend::SUSPEND_OTHER); struct AutoRestartEE { ~AutoRestartEE() { WRAPPER_NO_CONTRACT; ThreadSuspend::RestartEE(false, true); } } autoRestartEE; MethodDescBackpatchInfoTracker::ConditionalLockHolder slotBackpatchLockHolder; CodeVersionManager::LockHolder codeVersioningLockHolder; // After the following, no method's entry point would be pointing to a call counting stub StopAllCallCounting(tieredCompilationManager); // Call counting has been stopped above and call counting stubs will soon be deleted. Ensure that call counting stubs // will not be used after resuming the runtime. The following ensures that other threads will not use an old cached // entry point value that will not be valid. Do this here in case of exception later. MemoryBarrier(); // flush writes from this thread first to guarantee ordering FlushProcessWriteBuffers(); // At this point, allocated call counting stubs won't be used anymore. Call counting stubs and corresponding infos may // now be safely deleted. Note that call counting infos may not be deleted prior to this point because call counting // stubs refer to the remaining call count in the info, and the call counting info is necessary to get a code version // from a call counting stub address. DeleteAllCallCountingStubs(); } void CallCountingManager::StopAllCallCounting(TieredCompilationManager *tieredCompilationManager) { CONTRACTL { THROWS; GC_NOTRIGGER; MODE_COOPERATIVE; // for slot backpatching } CONTRACTL_END; _ASSERTE(GetThreadNULLOk() == TieredCompilationManager::GetBackgroundWorkerThread()); _ASSERTE(MethodDescBackpatchInfoTracker::IsLockOwnedByCurrentThread()); _ASSERTE(CodeVersionManager::IsLockOwnedByCurrentThread()); _ASSERTE(tieredCompilationManager != nullptr); for (auto itEnd = s_callCountingManagers->End(), it = s_callCountingManagers->Begin(); it != itEnd; ++it) { CallCountingManager *callCountingManager = *it; CallCountingInfoByCodeVersionHash &callCountingInfoByCodeVersionHash = callCountingManager->m_callCountingInfoByCodeVersionHash; for (auto itEnd = callCountingInfoByCodeVersionHash.End(), it = callCountingInfoByCodeVersionHash.Begin(); it != itEnd; ++it) { CallCountingInfo *callCountingInfo = *it; CallCountingInfo::Stage callCountingStage = callCountingInfo->GetStage(); if (callCountingStage != CallCountingInfo::Stage::StubMayBeActive && callCountingStage != CallCountingInfo::Stage::PendingCompletion) { continue; } NativeCodeVersion codeVersion = callCountingInfo->GetCodeVersion(); CallCountingInfo::Stage newCallCountingStage; if (callCountingStage == CallCountingInfo::Stage::StubMayBeActive) { newCallCountingStage = CallCountingInfo::Stage::StubIsNotActive; } else { _ASSERTE(callCountingStage == CallCountingInfo::Stage::PendingCompletion); if (!codeVersion.GetILCodeVersion().HasAnyOptimizedNativeCodeVersion(codeVersion)) { bool createTieringBackgroundWorker = false; tieredCompilationManager->AsyncPromoteToTier1(codeVersion, &createTieringBackgroundWorker); _ASSERTE(!createTieringBackgroundWorker); // the current thread is the background worker thread } newCallCountingStage = CallCountingInfo::Stage::Complete; } // The intention is that all call counting stubs will be deleted shortly, and only methods that are called again // will cause stubs to be recreated, so reset the code entry point codeVersion.GetMethodDesc()->ResetCodeEntryPoint(); callCountingInfo->SetStage(newCallCountingStage); } // Clear recorded call counting infos pending completion, they have been completed above SArray &callCountingInfosPendingCompletion = callCountingManager->m_callCountingInfosPendingCompletion; if (!callCountingInfosPendingCompletion.IsEmpty()) { callCountingInfosPendingCompletion.Clear(); if (callCountingInfosPendingCompletion.GetAllocation() > 64) { callCountingInfosPendingCompletion.Trim(); EX_TRY { callCountingInfosPendingCompletion.Preallocate(64); } EX_CATCH { } EX_END_CATCH(RethrowTerminalExceptions); } } // Reset forwarder stubs, they are not in use anymore MethodDescForwarderStubHash &methodDescForwarderStubHash = callCountingManager->m_methodDescForwarderStubHash; for (auto itEnd = methodDescForwarderStubHash.End(), it = methodDescForwarderStubHash.Begin(); it != itEnd; ++it) { Precode *forwarderStub = *it; forwarderStub->ResetTargetInterlocked(); } } } void CallCountingManager::DeleteAllCallCountingStubs() { CONTRACTL { THROWS; GC_NOTRIGGER; MODE_ANY; } CONTRACTL_END; _ASSERTE(CodeVersionManager::IsLockOwnedByCurrentThread()); _ASSERTE(IsSuspendEEThread()); _ASSERTE(s_activeCallCountingStubCount == 0); // An attempt will be made to delete stubs below, clear the counts first in case of exception later s_callCountingStubCount = 0; s_completedCallCountingStubCount = 0; for (auto itEnd = s_callCountingManagers->End(), it = s_callCountingManagers->Begin(); it != itEnd; ++it) { CallCountingManager *callCountingManager = *it; _ASSERTE(callCountingManager->m_callCountingInfosPendingCompletion.IsEmpty()); // Clear the call counting stub from call counting infos and delete completed infos MethodDescForwarderStubHash &methodDescForwarderStubHash = callCountingManager->m_methodDescForwarderStubHash; CallCountingInfoByCodeVersionHash &callCountingInfoByCodeVersionHash = callCountingManager->m_callCountingInfoByCodeVersionHash; for (auto itEnd = callCountingInfoByCodeVersionHash.End(), it = callCountingInfoByCodeVersionHash.Begin(); it != itEnd; ++it) { CallCountingInfo *callCountingInfo = *it; CallCountingInfo::Stage callCountingStage = callCountingInfo->GetStage(); if (callCountingStage == CallCountingInfo::Stage::Disabled) { continue; } if (callCountingInfo->GetCallCountingStub() != nullptr) { callCountingInfo->ClearCallCountingStub(); } if (callCountingStage != CallCountingInfo::Stage::Complete) { _ASSERTE(callCountingStage == CallCountingInfo::Stage::StubIsNotActive); continue; } // Currently, tier 0 is the last code version that is counted, and the method is typically not counted anymore. // Remove the forwarder stub if one exists, a new one will be created if necessary, for example, if a profiler adds // an IL code version for the method. Precode *const *forwarderStubPtr = methodDescForwarderStubHash.LookupPtr(callCountingInfo->GetCodeVersion().GetMethodDesc()); if (forwarderStubPtr != nullptr) { methodDescForwarderStubHash.RemovePtr(const_cast(forwarderStubPtr)); } callCountingInfoByCodeVersionHash.Remove(it); delete callCountingInfo; } callCountingManager->TrimCollections(); // All call counting stubs are deleted, not just the completed stubs. Typically, there are many methods that are called // only a few times and don't reach the call count threshold, so many stubs may not be recreated. On the other hand, // some methods may still be getting called, just less frequently, then call counting stubs would be recreated in the // foreground, which has some overhead that is currently managed in the conditions for deleting call counting stubs. // There are potential solutions to reclaim as much memory as possible and to minimize the foreground overhead, but they // seem to involve significantly higher complexity that doesn't seem worthwhile. callCountingManager->m_callCountingStubAllocator.Reset(); } } void CallCountingManager::TrimCollections() { CONTRACTL { THROWS; GC_NOTRIGGER; MODE_ANY; } CONTRACTL_END; _ASSERTE(CodeVersionManager::IsLockOwnedByCurrentThread()); // Resize the hash tables if it would save some space. The hash tables' item counts typically spikes and then stabilizes at // a lower value after most of the repeatedly called methods are promoted and the call counting infos deleted above. COUNT_T count = m_callCountingInfoByCodeVersionHash.GetCount(); COUNT_T capacity = m_callCountingInfoByCodeVersionHash.GetCapacity(); if (count == 0) { if (capacity != 0) { m_callCountingInfoByCodeVersionHash.RemoveAll(); } } else if (count <= capacity / 4) { EX_TRY { m_callCountingInfoByCodeVersionHash.Reallocate(count * 2); } EX_CATCH { } EX_END_CATCH(RethrowTerminalExceptions); } count = m_methodDescForwarderStubHash.GetCount(); capacity = m_methodDescForwarderStubHash.GetCapacity(); if (count == 0) { if (capacity != 0) { m_methodDescForwarderStubHash.RemoveAll(); } } else if (count <= capacity / 4) { EX_TRY { m_methodDescForwarderStubHash.Reallocate(count * 2); } EX_CATCH { } EX_END_CATCH(RethrowTerminalExceptions); } } #endif // !DACCESS_COMPILE bool CallCountingManager::IsCallCountingStub(PCODE entryPoint) { CONTRACTL { NOTHROW; GC_NOTRIGGER; MODE_ANY; SUPPORTS_DAC; } CONTRACTL_END; TADDR entryAddress = PCODEToPINSTR(entryPoint); _ASSERTE(entryAddress != (PCODE)NULL); CodeVersionManager::LockHolder codeVersioningLockHolder; PTR_CallCountingManagerHash callCountingManagers = s_callCountingManagers; if (callCountingManagers == NULL) { return false; } for (auto itEnd = callCountingManagers->End(), it = callCountingManagers->Begin(); it != itEnd; ++it) { PTR_CallCountingManager callCountingManager = *it; if (callCountingManager->m_callCountingStubAllocator.IsStub(entryAddress)) { return true; } } return false; } PCODE CallCountingManager::GetTargetForMethod(PCODE callCountingStubEntryPoint) { CONTRACTL { NOTHROW; GC_NOTRIGGER; MODE_COOPERATIVE; // the call counting stub cannot be deleted while inspecting it SUPPORTS_DAC; } CONTRACTL_END; _ASSERTE(IsCallCountingStub(callCountingStubEntryPoint)); return PTR_CallCountingStub(PCODEToPINSTR(callCountingStubEntryPoint))->GetTargetForMethod(); } #ifdef DACCESS_COMPILE void CallCountingManager::DacEnumerateCallCountingStubHeapRanges(CLRDataEnumMemoryFlags flags) { CONTRACTL { NOTHROW; GC_NOTRIGGER; MODE_ANY; SUPPORTS_DAC; } CONTRACTL_END; CodeVersionManager::LockHolder codeVersioningLockHolder; PTR_CallCountingManagerHash callCountingManagers = s_callCountingManagers; if (callCountingManagers == NULL) { return; } for (auto itEnd = callCountingManagers->End(), it = callCountingManagers->Begin(); it != itEnd; ++it) { PTR_CallCountingManager callCountingManager = *it; callCountingManager->m_callCountingStubAllocator.EnumerateHeapRanges(flags); } } #endif // DACCESS_COMPILE //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // CallCountingManager::CallCountingStubManager SPTR_IMPL(CallCountingStubManager, CallCountingStubManager, g_pManager); #ifndef DACCESS_COMPILE CallCountingStubManager::CallCountingStubManager() { WRAPPER_NO_CONTRACT; } void CallCountingStubManager::Init() { CONTRACTL { THROWS; GC_NOTRIGGER; MODE_ANY; } CONTRACTL_END; g_pManager = new CallCountingStubManager(); StubManager::AddStubManager(g_pManager); } #endif // !DACCESS_COMPILE #ifdef _DEBUG const char *CallCountingStubManager::DbgGetName() { WRAPPER_NO_CONTRACT; return "CallCountingStubManager"; } #endif #ifdef DACCESS_COMPILE LPCWSTR CallCountingStubManager::GetStubManagerName(PCODE addr) { WRAPPER_NO_CONTRACT; return W("CallCountingStub"); } #endif BOOL CallCountingStubManager::CheckIsStub_Internal(PCODE entryPoint) { WRAPPER_NO_CONTRACT; SUPPORTS_DAC; return CallCountingManager::IsCallCountingStub(entryPoint); } BOOL CallCountingStubManager::DoTraceStub(PCODE callCountingStubEntryPoint, TraceDestination *trace) { WRAPPER_NO_CONTRACT; SUPPORTS_DAC; _ASSERTE(trace != nullptr); trace->InitForStub(CallCountingManager::GetTargetForMethod(callCountingStubEntryPoint)); return true; } #ifdef DACCESS_COMPILE void CallCountingStubManager::DoEnumMemoryRegions(CLRDataEnumMemoryFlags flags) { WRAPPER_NO_CONTRACT; SUPPORTS_DAC; DAC_ENUM_VTHIS(); EMEM_OUT(("MEM: %p CallCountingStubManager\n", dac_cast(this))); CallCountingManager::DacEnumerateCallCountingStubHeapRanges(flags); } #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #endif // FEATURE_TIERED_COMPILATION