From fe5bb85a2d47a20a94e24bfa962ff15a34842a6a Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 25 Aug 2026 14:09:36 +0200 Subject: [PATCH 01/29] wip --- src/coreclr/clrfeatures.cmake | 4 + src/coreclr/inc/clrconfigvalues.h | 1 + src/coreclr/interpreter/compiler.cpp | 95 +++++++++++++ src/coreclr/interpreter/compiler.h | 11 ++ src/coreclr/interpreter/eeinterp.cpp | 7 + src/coreclr/interpreter/inc/intops.def | 4 + src/coreclr/interpreter/interpconfigvalues.h | 2 + .../vm/eventing/eventpipe/ep-rt-coreclr.cpp | 17 +++ .../vm/eventing/eventpipe/ep-rt-coreclr.h | 10 ++ src/coreclr/vm/interpexec.cpp | 8 ++ src/coreclr/vm/jitinterface.cpp | 27 +++- src/coreclr/vm/jitinterface.h | 93 ++++++------- src/coreclr/vm/pgo.cpp | 6 + src/coreclr/vm/pgo.h | 1 + src/mono/mono/eventpipe/ep-rt-mono.h | 8 ++ src/mono/wasm/features.md | 50 +++++++ src/native/eventpipe/ep-rt.h | 11 ++ src/native/eventpipe/ep.c | 4 + .../libs/Common/JavaScript/loader/dotnet.d.ts | 130 +++++++++--------- .../Common/JavaScript/types/public-api.ts | 6 + .../diagnostics/client-commands.ts | 31 +++++ .../diagnostics/diagnostic-server-js.ts | 4 + .../diagnostics/dotnet-pgo-trace.ts | 55 ++++++++ .../diagnostics/index.ts | 2 + .../diagnostics/types.ts | 4 + 25 files changed, 476 insertions(+), 115 deletions(-) create mode 100644 src/native/libs/System.Native.Browser/diagnostics/dotnet-pgo-trace.ts diff --git a/src/coreclr/clrfeatures.cmake b/src/coreclr/clrfeatures.cmake index bfe85ca39febd7..b765eb69734d23 100644 --- a/src/coreclr/clrfeatures.cmake +++ b/src/coreclr/clrfeatures.cmake @@ -13,6 +13,10 @@ if (FEATURE_DYNAMIC_CODE_COMPILED) set(FEATURE_PGO 1) endif() +if (NOT DEFINED FEATURE_PGO AND CLR_CMAKE_TARGET_ARCH_WASM) + set(FEATURE_PGO 1) +endif() + # On desktop, if dynamic code compiled is false, we still enable static linking so we don't have to add platform manifest entries # for interpreter library, which is required for the packs build if (CLR_CMAKE_TARGET_ARCH_WASM OR CLR_CMAKE_TARGET_APPLE_MOBILE OR NOT FEATURE_DYNAMIC_CODE_COMPILED) diff --git a/src/coreclr/inc/clrconfigvalues.h b/src/coreclr/inc/clrconfigvalues.h index d6badce75704c0..d397bc1b60107a 100644 --- a/src/coreclr/inc/clrconfigvalues.h +++ b/src/coreclr/inc/clrconfigvalues.h @@ -514,6 +514,7 @@ CONFIG_DWORD_INFO(INTERNAL_OSR_HighId, W("OSR_HighId"), 10000000, "High end of e RETAIL_CONFIG_STRING_INFO(INTERNAL_PGODataPath, W("PGODataPath"), "Read/Write PGO data from/to the indicated file.") RETAIL_CONFIG_DWORD_INFO(INTERNAL_ReadPGOData, W("ReadPGOData"), 0, "Read PGO data") RETAIL_CONFIG_DWORD_INFO(INTERNAL_WritePGOData, W("WritePGOData"), 0, "Write PGO data") +RETAIL_CONFIG_DWORD_INFO(INTERNAL_InterpPGO, W("InterpPGO"), 0, "Instrument interpreted methods with block counters and make the profile available for offline PGO (e.g. dotnet-pgo).") RETAIL_CONFIG_DWORD_INFO(EXTERNAL_TieredPGO, W("TieredPGO"), 1, "Instrument Tier0 code and make counts available to Tier1") // TieredPGO_InstrumentOnlyHotCode values: diff --git a/src/coreclr/interpreter/compiler.cpp b/src/coreclr/interpreter/compiler.cpp index 3ecfaaf31e0b80..a843093417664e 100644 --- a/src/coreclr/interpreter/compiler.cpp +++ b/src/coreclr/interpreter/compiler.cpp @@ -51,6 +51,9 @@ bool InterpCompiler::s_samplingProfilerEnabled = false; bool InterpCompiler::s_browserProfilerEnabled = false; #endif #endif // PERFTRACING_DISABLE_THREADS +#if defined(TARGET_BROWSER) || defined(TARGET_WASI) +bool InterpCompiler::s_interpPgoEnabled = false; +#endif // defined(TARGET_BROWSER) || defined(TARGET_WASI) #if MEASURE_MEM_ALLOC #include @@ -2246,6 +2249,12 @@ InterpCompiler::InterpCompiler(COMP_HANDLE compHnd, #endif #endif // PERFTRACING_DISABLE_THREADS +#if defined(TARGET_BROWSER) || defined(TARGET_WASI) + m_emitInterpPGO = s_interpPgoEnabled + && (InterpConfig.InterpPGOMethods().isEmpty() + || InterpConfig.InterpPGOMethods().contains(compHnd, m_methodHnd, m_classHnd, &m_methodInfo->args)); +#endif // defined(TARGET_BROWSER) || defined(TARGET_WASI) + #ifdef DEBUG m_methodName = ::PrintMethodName(compHnd, m_classHnd, m_methodHnd, &m_methodInfo->args, /* includeAssembly */ false, @@ -2348,6 +2357,11 @@ bool InterpCompiler::CompileMethod() } #endif +#if defined(TARGET_BROWSER) || defined(TARGET_WASI) + if (m_emitInterpPGO) + InstrumentBlockCounts(); +#endif // defined(TARGET_BROWSER) || defined(TARGET_WASI) + AllocOffsets(); PatchInitLocals(m_methodInfo); @@ -8668,6 +8682,87 @@ void InterpCompiler::CreateSynchronizedRetValVar() INTERP_DUMP("Created ret val var V%d\n", m_synchronizedOrAsyncRetValVarIndex); } +#if defined(TARGET_BROWSER) || defined(TARGET_WASI) +// Instrument each basic block with a block-count PGO probe. The counters are allocated by +// allocPgoInstrumentationBySchema (native PgoManager memory), so they persist independently of +// EventPipe session lifetime; the accumulated profile is flushed to the trace as +// JitInstrumentationDataVerbose events, which dotnet-pgo consumes to build an .mibc. +void InterpCompiler::InstrumentBlockCounts() +{ + // Mark blocks that are the target of a branch or switch (loop and branch heads). Together with + // the method entry, these are the block heads whose execution count can't be inferred from a + // single predecessor, so only they are worth a counter; the precompiler reconstructs the rest of + // the flow graph from them. This deliberately avoids the JIT's edge/spanning-tree scheme (#130517). + bool *isBranchTarget = getAllocator(IMK_BasicBlock).allocateZeroed(m_BBCount); + for (InterpBasicBlock *bb = m_pEntryBB; bb != NULL; bb = bb->pNextBB) + { + for (InterpInst *ins = bb->pFirstIns; ins != NULL; ins = ins->pNext) + { + if (ins->opcode == INTOP_SWITCH) + { + int32_t n = ins->data[0]; + for (int32_t i = 0; i < n; i++) + isBranchTarget[ins->info.ppTargetBBTable[i]->index] = true; + } + else if (InterpOpIsUncondBranch(ins->opcode) || InterpOpIsCondBranch(ins->opcode) || + ins->opcode == INTOP_LEAVE_CATCH || ins->opcode == INTOP_CALL_FINALLY) + { + isBranchTarget[ins->info.pTargetBB->index] = true; + } + } + } + + // Collect the canonical block for each real IL offset that is the method entry (IL offset 0) or + // a branch/loop target. Clones (funclet / leave-chain islands) and blocks removed by optimization + // are skipped, so every schema entry carries a unique IL offset, matching what + // getPgoInstrumentationResults and dotnet-pgo expect. + TArray blocks(GetMemPoolAllocator(IMK_DataItem)); + for (InterpBasicBlock *bb = m_pEntryBB; bb != NULL; bb = bb->pNextBB) + { + if (bb->ilOffset < 0 || bb->ilOffset >= m_ILCodeSize || m_ppOffsetToBB[bb->ilOffset] != bb) + continue; + if (bb->ilOffset == 0 || isBranchTarget[bb->index]) + blocks.Add(bb); + } + + int32_t numBlocks = blocks.GetSize(); + if (numBlocks == 0) + return; + + // One 4-byte block counter per block. + TArray schema(GetMemPoolAllocator(IMK_DataItem)); + for (int32_t i = 0; i < numBlocks; i++) + { + ICorJitInfo::PgoInstrumentationSchema schemaElem; + schemaElem.Offset = 0; + schemaElem.InstrumentationKind = ICorJitInfo::PgoInstrumentationKind::BasicBlockIntCount; + schemaElem.ILOffset = blocks.Get(i)->ilOffset; + schemaElem.Count = 1; + schemaElem.Other = 0; + schema.Add(schemaElem); + } + + ICorJitInfo::PgoInstrumentationSchema *pSchema = schema.GetUnderlyingArray(); + uint8_t *pInstrumentationData = NULL; + HRESULT hr = m_compHnd->allocPgoInstrumentationBySchema(m_methodHnd, pSchema, (uint32_t)numBlocks, &pInstrumentationData); + if (FAILED(hr) || pInstrumentationData == NULL) + { + INTERP_DUMP("InstrumentBlockCounts: allocPgoInstrumentationBySchema failed (hr=0x%08x)\n", hr); + return; + } + + // Insert an INTOP_PGO_COUNT probe at the start of each block, pointing at its counter. + for (int32_t i = 0; i < numBlocks; i++) + { + int32_t *pCounter = (int32_t*)(pInstrumentationData + pSchema[i].Offset); + InterpInst *ins = InsertInsBB(blocks.Get(i), NULL, INTOP_PGO_COUNT); + // Probe is a pure counter increment with no IL mapping; keep it out of the debug maps. + ins->ilOffset = -1; + ins->data[0] = GetDataItemIndex((void*)pCounter); + } +} +#endif // TARGET_BROWSER || TARGET_WASI + void InterpCompiler::GenerateCode(CORINFO_METHOD_INFO* methodInfo) { bool readonly = false; diff --git a/src/coreclr/interpreter/compiler.h b/src/coreclr/interpreter/compiler.h index 88680004fc7441..ef09fc9a9bac44 100644 --- a/src/coreclr/interpreter/compiler.h +++ b/src/coreclr/interpreter/compiler.h @@ -641,6 +641,10 @@ class InterpCompiler #endif #endif // PERFTRACING_DISABLE_THREADS +#if defined(TARGET_BROWSER) || defined(TARGET_WASI) + bool m_emitInterpPGO; +#endif // defined(TARGET_BROWSER) || defined(TARGET_WASI) + void DeclarePointerIsClass(CORINFO_CLASS_HANDLE clsHnd) { #ifdef DEBUG @@ -767,6 +771,10 @@ class InterpCompiler void CreateSynchronizedRetValVar(); void GenerateCode(CORINFO_METHOD_INFO* methodInfo); + +#if defined(TARGET_BROWSER) || defined(TARGET_WASI) + void InstrumentBlockCounts(); +#endif // defined(TARGET_BROWSER) || defined(TARGET_WASI) InterpBasicBlock* GenerateCodeForLeaveChainIslands(InterpBasicBlock *pNewBB, InterpBasicBlock *pPrevBB); void PatchInitLocals(CORINFO_METHOD_INFO* methodInfo); @@ -1150,6 +1158,9 @@ class InterpCompiler static bool s_browserProfilerEnabled; #endif #endif // PERFTRACING_DISABLE_THREADS +#if defined(TARGET_BROWSER) || defined(TARGET_WASI) + static bool s_interpPgoEnabled; +#endif // defined(TARGET_BROWSER) || defined(TARGET_WASI) #if MEASURE_MEM_ALLOC // Memory statistics for profiling. diff --git a/src/coreclr/interpreter/eeinterp.cpp b/src/coreclr/interpreter/eeinterp.cpp index 0f827b52b79a6d..6fb261a5071944 100644 --- a/src/coreclr/interpreter/eeinterp.cpp +++ b/src/coreclr/interpreter/eeinterp.cpp @@ -41,6 +41,13 @@ extern "C" INTERP_API void jitStartup(ICorJitHost* jitHost) #endif // PERFTRACING_DISABLE_THREADS } + if (InterpConfig.InterpPGO() != 0) + { +#if defined(TARGET_BROWSER) || defined(TARGET_WASI) + InterpCompiler::s_interpPgoEnabled = true; +#endif // defined(TARGET_BROWSER) || defined(TARGET_WASI) + } + g_interpInitialized = true; } /*****************************************************************************/ diff --git a/src/coreclr/interpreter/inc/intops.def b/src/coreclr/interpreter/inc/intops.def index 7e4934cd218afb..8e0f454f1ea30a 100644 --- a/src/coreclr/interpreter/inc/intops.def +++ b/src/coreclr/interpreter/inc/intops.def @@ -90,6 +90,10 @@ OPDEF(INTOP_PROF_ENTER, "prof.enter", 2, 0, 0, InterpOpMethodHandle) OPDEF(INTOP_PROF_LEAVE, "prof.leave", 1, 0, 0, InterpOpNoArgs) #endif +#if defined(TARGET_BROWSER) || defined(TARGET_WASI) +OPDEF(INTOP_PGO_COUNT, "pgo.count", 2, 0, 0, InterpOpLdPtr) +#endif + OPDEF(INTOP_BR, "br", 2, 0, 0, InterpOpBranch) OPDEF(INTOP_BRFALSE_I4, "brfalse.i4", 3, 0, 1, InterpOpBranch) diff --git a/src/coreclr/interpreter/interpconfigvalues.h b/src/coreclr/interpreter/interpconfigvalues.h index 74e8bf1a40e52e..0ddde0b990b9ae 100644 --- a/src/coreclr/interpreter/interpconfigvalues.h +++ b/src/coreclr/interpreter/interpconfigvalues.h @@ -37,6 +37,8 @@ RELEASE_CONFIG_INTEGER(InterpMode, "InterpMode", 0); // Interpreter mode, one of RELEASE_CONFIG_INTEGER(DisplayMemStats, "JitMemStats", 0); // Display interpreter memory usage statistics (0=off, 1=summary, 2=detailed per-method) RELEASE_CONFIG_METHODSET(WasmPerformanceInstrumentation, "WasmPerformanceInstrumentation") // Method filter for WASM performance instrumentation profiler. Uses standard MethodSet pattern format. +RELEASE_CONFIG_INTEGER(InterpPGO, "InterpPGO", 0); // Instrument interpreted methods with block counters and make the profile available for offline PGO (e.g. dotnet-pgo). +RELEASE_CONFIG_METHODSET(InterpPGOMethods, "InterpPGOMethods") // Optional method filter scoping InterpPGO instrumentation (empty = all methods). Uses standard MethodSet pattern format. #undef CONFIG_STRING #undef RELEASE_CONFIG_STRING diff --git a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp index d53531ec8e2d3c..6440a23208d475 100644 --- a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp +++ b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp @@ -9,6 +9,9 @@ #include #include #include "threadsuspend.h" +#ifdef FEATURE_PGO +#include "pgo.h" +#endif ep_rt_lock_handle_t _ep_rt_coreclr_config_lock_handle; CrstStatic _ep_rt_coreclr_config_lock; @@ -169,4 +172,18 @@ ep_rt_coreclr_sample_profiler_write_sampling_event_for_threads ( return; } +void +ep_rt_coreclr_session_stopping (void) +{ + STATIC_CONTRACT_NOTHROW; +#ifdef FEATURE_PGO + EX_TRY + { + PgoManager::FlushInstrumentationData (); + } + EX_CATCH { } + EX_END_CATCH +#endif // FEATURE_PGO +} + #endif /* ENABLE_PERFTRACING */ diff --git a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h index c1dd51efd63215..e57d143fb530fd 100644 --- a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h +++ b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h @@ -659,6 +659,16 @@ ep_rt_notify_profiler_provider_created (EventPipeProvider *provider) #endif // !DACCESS_COMPILE && PROFILING_SUPPORTED } +static +inline +void +ep_rt_session_stopping (void) +{ + STATIC_CONTRACT_NOTHROW; + extern void ep_rt_coreclr_session_stopping (void); + ep_rt_coreclr_session_stopping (); +} + /* * Arrays. */ diff --git a/src/coreclr/vm/interpexec.cpp b/src/coreclr/vm/interpexec.cpp index b19d3a575a910f..391c2fca399416 100644 --- a/src/coreclr/vm/interpexec.cpp +++ b/src/coreclr/vm/interpexec.cpp @@ -2066,6 +2066,14 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr INTOP_NEXT; #endif // TARGET_BROWSER && PERFTRACING_DISABLE_THREADS +#if defined(TARGET_BROWSER) || defined(TARGET_WASI) + INTOP_CASE(INTOP_PGO_COUNT) + // Increment the block-count PGO counter whose address is stored as a data item. + (*(int32_t*)pMethod->pDataItems[ip[1]])++; + ip += 2; + INTOP_NEXT; +#endif // TARGET_BROWSER || TARGET_WASI + INTOP_CASE(INTOP_BR) ip += ip[1]; INTOP_NEXT; diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index 6ec53518659e9f..78154e66bfc9ec 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -13061,7 +13061,19 @@ CORJIT_FLAGS* CEECodeGenInfo::getJitFlagsInternal() } /*********************************************************************/ -HRESULT CEEJitInfo::allocPgoInstrumentationBySchema( +#ifdef FEATURE_PGO +static bool InterpreterPgoInstrumentationEnabled() +{ + static LONG s_enabled = -1; + if (s_enabled == -1) + { + InterlockedExchange(&s_enabled, (CLRConfig::GetConfigValue(CLRConfig::INTERNAL_InterpPGO) != 0) ? 1 : 0); + } + return s_enabled != 0; +} +#endif // FEATURE_PGO + +HRESULT CEECodeGenInfo::allocPgoInstrumentationBySchema( CORINFO_METHOD_HANDLE ftnHnd, /* IN */ PgoInstrumentationSchema* pSchema, /* IN/OUT */ uint32_t countSchemaItems, /* IN */ @@ -13080,9 +13092,10 @@ HRESULT CEEJitInfo::allocPgoInstrumentationBySchema( #ifdef FEATURE_PGO - // Only try instrumenting tiering-eligible methods + // Only try instrumenting tiering-eligible methods, unless interpreter PGO is enabled, in + // which case we instrument every method for offline profile collection. MethodDesc* pMD = (MethodDesc*)ftnHnd; - if (pMD->IsEligibleForTieredCompilation()) + if (pMD->IsEligibleForTieredCompilation() || InterpreterPgoInstrumentationEnabled()) { hr = PgoManager::allocPgoInstrumentationBySchema(pMD, m_ILHeader, pSchema, countSchemaItems, pInstrumentationData); } @@ -13091,7 +13104,7 @@ HRESULT CEEJitInfo::allocPgoInstrumentationBySchema( hr = E_NOTIMPL; } #else - _ASSERTE(!"allocMethodBlockCounts not implemented on CEEJitInfo!"); + _ASSERTE(!"allocMethodBlockCounts not implemented on CEECodeGenInfo!"); hr = E_NOTIMPL; #endif // !FEATURE_PGO @@ -13100,9 +13113,9 @@ HRESULT CEEJitInfo::allocPgoInstrumentationBySchema( return hr; } -// Consider implementing getBBProfileData on CEEJitInfo. This will allow us +// Consider implementing getBBProfileData on CEECodeGenInfo. This will allow us // to use profile info in codegen for non zapped images. -HRESULT CEEJitInfo::getPgoInstrumentationResults( +HRESULT CEECodeGenInfo::getPgoInstrumentationResults( CORINFO_METHOD_HANDLE ftnHnd, PgoInstrumentationSchema **pSchema, // pointer to the schema table which describes the instrumentation results (pointer will not remain valid after jit completes) uint32_t * pCountSchemaItems, // pointer to the count schema items @@ -13163,7 +13176,7 @@ HRESULT CEEJitInfo::getPgoInstrumentationResults( *pPgoSource = pDataCur->m_pgoSource; hr = pDataCur->m_hr; #else - _ASSERTE(!"getPgoInstrumentationResults not implemented on CEEJitInfo!"); + _ASSERTE(!"getPgoInstrumentationResults not implemented on CEECodeGenInfo!"); hr = E_NOTIMPL; #endif diff --git a/src/coreclr/vm/jitinterface.h b/src/coreclr/vm/jitinterface.h index d0a26f4adaa838..2a5d8126cdce88 100644 --- a/src/coreclr/vm/jitinterface.h +++ b/src/coreclr/vm/jitinterface.h @@ -452,6 +452,19 @@ class CEECodeGenInfo : public CEEInfo if (m_pNativeVarInfo != NULL) freeArrayInternal(m_pNativeVarInfo); + +#ifdef FEATURE_PGO + if (m_foundPgoData != NULL) + { + ComputedPgoData* current = m_foundPgoData; + while (current != NULL) + { + ComputedPgoData* next = current->m_next; + delete current; + current = next; + } + } +#endif } virtual void ResetForJitRetry() @@ -548,6 +561,22 @@ class CEECodeGenInfo : public CEEInfo CORINFO_METHOD_INFO* getMethodInfoInternal(); CORJIT_FLAGS* getJitFlagsInternal(); + HRESULT allocPgoInstrumentationBySchema( + CORINFO_METHOD_HANDLE ftnHnd, /* IN */ + PgoInstrumentationSchema* pSchema, /* IN/OUT */ + uint32_t countSchemaItems, /* IN */ + uint8_t** pInstrumentationData /* OUT */ + ) override; + + HRESULT getPgoInstrumentationResults( + CORINFO_METHOD_HANDLE ftnHnd, /* IN */ + PgoInstrumentationSchema** pSchema, /* OUT */ + uint32_t* pCountSchemaItems, /* OUT */ + uint8_t**pInstrumentationData, /* OUT */ + PgoSource *pPgoSource, /* OUT */ + bool* pDynamicPgo /* OUT */ + ) override; + protected: template @@ -580,6 +609,24 @@ class CEECodeGenInfo : public CEEInfo COR_ILMETHOD_DECODER* m_ILHeader; // the code header to use. This may have been generated due to dynamic IL generation. CORINFO_METHOD_INFO m_MethodInfo; +#ifdef FEATURE_PGO + // PGO data + struct ComputedPgoData + { + ComputedPgoData(MethodDesc* pMD) : m_pMD(pMD) {} + + ComputedPgoData* m_next = nullptr; + MethodDesc *m_pMD; + NewArrayHolder m_allocatedData; + PgoInstrumentationSchema* m_schema = nullptr; + UINT32 m_cSchemaElems; + BYTE *m_pInstrumentationData = nullptr; + HRESULT m_hr = E_NOTIMPL; + PgoSource m_pgoSource = PgoSource::Unknown; + }; + ComputedPgoData* m_foundPgoData = nullptr; +#endif + #if defined(_DEBUG) ULONG m_codeSize; // Code size requested via allocMem #endif @@ -650,22 +697,6 @@ class CEEJitInfo final : public CEECodeGenInfo CorJitFuncKind funcKind /* IN */ ) override; - HRESULT allocPgoInstrumentationBySchema( - CORINFO_METHOD_HANDLE ftnHnd, /* IN */ - PgoInstrumentationSchema* pSchema, /* IN/OUT */ - uint32_t countSchemaItems, /* IN */ - uint8_t** pInstrumentationData /* OUT */ - ) override; - - HRESULT getPgoInstrumentationResults( - CORINFO_METHOD_HANDLE ftnHnd, /* IN */ - PgoInstrumentationSchema** pSchema, /* OUT */ - uint32_t* pCountSchemaItems, /* OUT */ - uint8_t**pInstrumentationData, /* OUT */ - PgoSource *pPgoSource, /* OUT */ - bool* pDynamicPgo /* OUT */ - ) override; - void recordCallSite( uint32_t instrOffset, /* IN */ CORINFO_SIG_INFO * callSig, /* IN */ @@ -825,18 +856,6 @@ class CEEJitInfo final : public CEECodeGenInfo #ifdef FEATURE_ON_STACK_REPLACEMENT if (m_pPatchpointInfoFromJit != NULL) freeArrayInternal(m_pPatchpointInfoFromJit); -#endif -#ifdef FEATURE_PGO - if (m_foundPgoData != NULL) - { - ComputedPgoData* current = m_foundPgoData; - while (current != NULL) - { - ComputedPgoData* next = current->m_next; - delete current; - current = next; - } - } #endif } @@ -858,24 +877,6 @@ class CEEJitInfo final : public CEECodeGenInfo protected : -#ifdef FEATURE_PGO - // PGO data - struct ComputedPgoData - { - ComputedPgoData(MethodDesc* pMD) : m_pMD(pMD) {} - - ComputedPgoData* m_next = nullptr; - MethodDesc *m_pMD; - NewArrayHolder m_allocatedData; - PgoInstrumentationSchema* m_schema = nullptr; - UINT32 m_cSchemaElems; - BYTE *m_pInstrumentationData = nullptr; - HRESULT m_hr = E_NOTIMPL; - PgoSource m_pgoSource = PgoSource::Unknown; - }; - ComputedPgoData* m_foundPgoData = nullptr; -#endif - TADDR m_moduleBase; // Base for unwind Infos ULONG m_totalUnwindSize; // Total reserved unwind space uint32_t m_usedUnwindSize; // used space in m_theUnwindBlock diff --git a/src/coreclr/vm/pgo.cpp b/src/coreclr/vm/pgo.cpp index 9de27286122c81..65872e89206585 100644 --- a/src/coreclr/vm/pgo.cpp +++ b/src/coreclr/vm/pgo.cpp @@ -77,6 +77,12 @@ void PgoManager::Shutdown() } } +void PgoManager::FlushInstrumentationData() +{ + WRAPPER_NO_CONTRACT; + WritePgoData(); +} + void PgoManager::VerifyAddress(void* address) { // TODO Insert an assert to check that an address is a valid pgo address diff --git a/src/coreclr/vm/pgo.h b/src/coreclr/vm/pgo.h index f4d8ac7f3c3b37..451486100df96b 100644 --- a/src/coreclr/vm/pgo.h +++ b/src/coreclr/vm/pgo.h @@ -17,6 +17,7 @@ class PgoManager public: static void Initialize(); + static void FlushInstrumentationData(); static void Shutdown(); #endif // FEATURE_PGO diff --git a/src/mono/mono/eventpipe/ep-rt-mono.h b/src/mono/mono/eventpipe/ep-rt-mono.h index 4c28f385c02391..54bf3b47986502 100644 --- a/src/mono/mono/eventpipe/ep-rt-mono.h +++ b/src/mono/mono/eventpipe/ep-rt-mono.h @@ -713,6 +713,14 @@ ep_rt_notify_profiler_provider_created (EventPipeProvider *provider) ; } +static +inline +void +ep_rt_session_stopping (void) +{ + ; +} + /* * Arrays. */ diff --git a/src/mono/wasm/features.md b/src/mono/wasm/features.md index 0071a938490b9e..7ee03d09f115a9 100644 --- a/src/mono/wasm/features.md +++ b/src/mono/wasm/features.md @@ -430,6 +430,56 @@ globalThis.getDotnetRuntime(0).collectCpuSamples({durationSeconds: 60}) The counters could be opened in VS or in `PerfView.exe` +```js +globalThis.getDotnetRuntime(0).collectPgoTrace() +// stops and downloads automatically after 10s; pass a different duration with: +globalThis.getDotnetRuntime(0).collectPgoTrace({durationSeconds: 30}) +``` + +This captures a startup PGO trace on the CoreCLR interpreter. To arm the trace before the first +managed method compiles, set `DOTNET_DiagnosticPorts=js://pgo`. The trace always contains the method +list; to also capture **block counts** the interpreter must be instrumented, which is enabled with +`DOTNET_InterpPGO=1`: + +```xml + + + + +``` + +Instrumentation can be scoped to a subset of methods with `DOTNET_InterpPGOMethods` (standard MethodSet +filter format; empty = all methods) to bound overhead during bring-up. + +Convert the downloaded `xxx.nettrace` into an `.mibc` with `dotnet-pgo` (installed via +`dotnet tool install --global dotnet-pgo`, or from `artifacts/bin/coreclr/../dotnet-pgo/` +in a runtime build): + +```console +dotnet-pgo create-mibc --trace xxx.nettrace --reference /*.dll --output app.mibc +``` + +The resulting `.mibc` drives a profile-guided crossgen2 build. + +#### Referencing the right (IL-trimmed) assemblies + +`--reference` must point at assemblies whose **MVID** matches the modules recorded in the trace, otherwise +`dotnet-pgo` reports `Dll mismatch ...` (or `Unknown ModuleID` for the affected methods). On browser/wasm +the assemblies loaded by the runtime are the **IL-trimmed** ones: `PublishTrimmed`/ILLink rewrites each +assembly and **generates a fresh MVID**, then those trimmed DLLs are converted to the fingerprinted +`*.wasm` files in `_framework` (webcil preserves the MVID byte-for-byte). So the trace records the +**trimmed** MVIDs, which do **not** match the untrimmed assemblies in the runtime pack +(`microsoft.netcore.app.runtime.browser-wasm/.../runtimes/browser-wasm/native/*.dll`). + +Point `--reference` at the ILLink trim output instead: + +- Published app: `obj///browser-wasm/linked/*.dll` +- In this repo's WASM samples: `artifacts/obj/mono//browser.wasm./linked/*.dll` + +If a method's assembly is not among the references, `dotnet-pgo` skips it with an `Unknown ModuleID` +warning; pass every trimmed assembly the app exercised to capture all of them. `dotnet-pgo dump --input app.mibc` +lists the methods and their block counts. + ### Profiling in the browser dev tools diff --git a/src/native/eventpipe/ep-rt.h b/src/native/eventpipe/ep-rt.h index 5d5a9d60b3e52c..d260002f68ad18 100644 --- a/src/native/eventpipe/ep-rt.h +++ b/src/native/eventpipe/ep-rt.h @@ -234,6 +234,17 @@ static void ep_rt_notify_profiler_provider_created (EventPipeProvider *provider); +/* + * Session lifecycle notifications. + */ + +// Invoked while a session is stopping, before its providers are disabled and its buffers are +// flushed, so the runtime can emit any pending end-of-session data (e.g. block-count PGO) into +// the still-open session. Runs on the disabling thread with the EventPipe lock held. +static +void +ep_rt_session_stopping (void); + /* * Arrays. */ diff --git a/src/native/eventpipe/ep.c b/src/native/eventpipe/ep.c index eefd36f43e7396..36a4801b5427d2 100644 --- a/src/native/eventpipe/ep.c +++ b/src/native/eventpipe/ep.c @@ -723,6 +723,10 @@ disable_holding_lock ( // Log the process information event. log_process_info_event (ep_event_source_get ()); + // Give the runtime a chance to emit any pending end-of-session data (e.g. block-count + // PGO) while the session's providers are still enabled and its buffers are still live. + ep_rt_session_stopping (); + // Disable session tracing. config_enable_disable (ep_config_get (), session, provider_callback_data_queue, false); diff --git a/src/native/libs/Common/JavaScript/loader/dotnet.d.ts b/src/native/libs/Common/JavaScript/loader/dotnet.d.ts index 43637d44c19159..39905a6c1b4687 100644 --- a/src/native/libs/Common/JavaScript/loader/dotnet.d.ts +++ b/src/native/libs/Common/JavaScript/loader/dotnet.d.ts @@ -340,68 +340,68 @@ interface LoadingResource { url: string; response: Promise; } -type SingleAssetBehaviors = -/** - * The binary of the .NET runtime. - */ -"dotnetwasm" -/** - * The javascript module for loader. - */ - | "js-module-dotnet" -/** - * The javascript module for diagnostic server and client. - */ - | "js-module-diagnostics" -/** - * The javascript module for runtime. - */ - | "js-module-runtime" -/** - * The javascript module for emscripten. - */ - | "js-module-native" -/** - * Typically dotnet.boot.js - */ - | "manifest" -/** - * The debugging symbols - */ - | "symbols"; -type AssetBehaviors = SingleAssetBehaviors | -/** - * Load asset as a managed resource assembly. - */ -"resource" -/** - * Load asset as a managed assembly. - */ - | "assembly" -/** - * Load asset as a managed debugging information. - */ - | "pdb" -/** - * Store asset into the native heap. - */ - | "heap" -/** - * Load asset as an ICU data archive. - */ - | "icu" -/** - * Load asset into the virtual filesystem (for fopen, File.Open, etc). - */ - | "vfs" -/** - * The javascript module that came from nuget package . - */ - | "js-module-library-initializer" -/** - * Managed assembly packaged as Webcil v 1.0 - */ - | "webcil"; +type SingleAssetBehaviors = + /** + * The binary of the .NET runtime. + */ + "dotnetwasm" + /** + * The javascript module for loader. + */ + | "js-module-dotnet" + /** + * The javascript module for diagnostic server and client. + */ + | "js-module-diagnostics" + /** + * The javascript module for runtime. + */ + | "js-module-runtime" + /** + * The javascript module for emscripten. + */ + | "js-module-native" + /** + * Typically dotnet.boot.js + */ + | "manifest" + /** + * The debugging symbols + */ + | "symbols"; +type AssetBehaviors = SingleAssetBehaviors | + /** + * Load asset as a managed resource assembly. + */ + "resource" + /** + * Load asset as a managed assembly. + */ + | "assembly" + /** + * Load asset as a managed debugging information. + */ + | "pdb" + /** + * Store asset into the native heap. + */ + | "heap" + /** + * Load asset as an ICU data archive. + */ + | "icu" + /** + * Load asset into the virtual filesystem (for fopen, File.Open, etc). + */ + | "vfs" + /** + * The javascript module that came from nuget package . + */ + | "js-module-library-initializer" + /** + * Managed assembly packaged as Webcil v 1.0 + */ + | "webcil"; declare const enum GlobalizationMode { /** * Load sharded ICU data. @@ -633,6 +633,12 @@ type DiagnosticsAPIType = { * It could be converted for Visual Studio using `dotnet-gcdump convert`. */ collectGcDump: (options?: DiagnosticCommandOptions) => Promise; + /** + * creates a startup PGO trace file (method list, and block counts when the interpreter is instrumented). + * The trace stops and downloads automatically after `durationSeconds` (default 10s). + * Convert it with `dotnet-pgo create-mibc --trace xxx.nettrace ...` to drive a profile-guided crossgen2 build. + */ + collectPgoTrace: (options?: DiagnosticCommandOptions) => Promise; /** * changes DOTNET_DiagnosticPorts and makes a new connection to WebSocket on that URL. */ diff --git a/src/native/libs/Common/JavaScript/types/public-api.ts b/src/native/libs/Common/JavaScript/types/public-api.ts index ce4f51ef9645ab..dec79010bda183 100644 --- a/src/native/libs/Common/JavaScript/types/public-api.ts +++ b/src/native/libs/Common/JavaScript/types/public-api.ts @@ -661,6 +661,12 @@ export type DiagnosticsAPIType = { * It could be converted for Visual Studio using `dotnet-gcdump convert`. */ collectGcDump: (options?: DiagnosticCommandOptions) => Promise; + /** + * creates a startup PGO trace file (method list, and block counts when the interpreter is instrumented). + * The trace stops and downloads automatically after `durationSeconds` (default 10s). + * Convert it with `dotnet-pgo create-mibc --trace xxx.nettrace ...` to drive a profile-guided crossgen2 build. + */ + collectPgoTrace: (options?: DiagnosticCommandOptions) => Promise; /** * changes DOTNET_DiagnosticPorts and makes a new connection to WebSocket on that URL. */ diff --git a/src/native/libs/System.Native.Browser/diagnostics/client-commands.ts b/src/native/libs/System.Native.Browser/diagnostics/client-commands.ts index ee31e21cdc2a9e..361336b6934bdb 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/client-commands.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/client-commands.ts @@ -116,6 +116,37 @@ export function commandSampleProfiler(options: DiagnosticCommandOptions) { }); } +export function commandPgoTrace(options: DiagnosticCommandOptions) { + return commandCollectTracing2({ + circularBufferMB: options.circularBufferMB ?? 256, + format: 1, + requestRundown: true, + providers: [ + { + // Microsoft-Windows-DotNETRuntime keyword 0x1F000080018, level 5 (Verbose) - matches the + // desktop dotnet-optimization IBC collection so dotnet-pgo sees the same event set: + // 0x8 Loader (ModuleLoadUnload - module identity) + // 0x10 Jit (MethodJittingStarted/MethodLoadVerbose - the method list) + // 0x80000 Type (GCBulkType - type identity for generics) + // 0x1000000000 Compilation (JIT compilation events; not emitted by the interpreter) + // 0x2000000000 CompilationDiagnostic + // 0x4000000000 MethodDiagnostic (MethodDetails - generic instantiation resolution) + // 0x8000000000 TypeDiagnostic (type identity for generics) + // 0x10000000000 JitInstrumentationData (JitInstrumentationDataVerbose - block counts) + // keyword is serialized as [hi, lo]: 0x1F000080018 -> hi 0x000001F0, lo 0x00080018 + keywords: [ + 0x0000_01F0, + 0x0008_0018, + ], + logLevel: 5, + providerName: "Microsoft-Windows-DotNETRuntime", + arguments: null + }, + ...options.extraProviders || [], + ] + }); +} + function commandCollectTracing2(payload2: PayloadV2) { const payloadLength = computeCollectTracing2PayloadByteLength(payload2); const messageLength = computeMessageByteLength(payloadLength); diff --git a/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server-js.ts b/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server-js.ts index 8e7a6410255125..e3a2f6d02ae087 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server-js.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/diagnostic-server-js.ts @@ -8,6 +8,7 @@ import { dotnetLoaderExports, dotnetLogger, dotnetNativeBrowserExports } from ". import { collectGcDump } from "./dotnet-gcdump"; import { collectMetrics } from "./dotnet-counters"; import { collectCpuSamples } from "./dotnet-cpu-profiler"; +import { collectPgoTrace } from "./dotnet-pgo-trace"; //let diagClient:IDiagClient|undefined = undefined as any; //let server:DiagServer = undefined as any; @@ -174,6 +175,9 @@ export function createDiagConnectionJs(socketHandle: number, scenarioName: strin if (scenarioName.startsWith("js://cpu-samples")) { collectCpuSamples({}, true); } + if (scenarioName.startsWith("js://pgo")) { + collectPgoTrace({}, true); + } const dotnetDiagnosticClient: FnClientProvider = (globalThis as any).dotnetDiagnosticClient; if (typeof dotnetDiagnosticClient === "function") { nextJsClient.resolve(dotnetDiagnosticClient(scenarioName)); diff --git a/src/native/libs/System.Native.Browser/diagnostics/dotnet-pgo-trace.ts b/src/native/libs/System.Native.Browser/diagnostics/dotnet-pgo-trace.ts new file mode 100644 index 00000000000000..1f90a7319916d3 --- /dev/null +++ b/src/native/libs/System.Native.Browser/diagnostics/dotnet-pgo-trace.ts @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import type { DiagnosticCommandOptions } from "../types"; + +import { commandResumeRuntime, commandStopTracing, commandPgoTrace } from "./client-commands"; +import { dotnetLoaderExports, Module } from "./cross-module"; +import { serverSession, setupJsClient } from "./diagnostic-server-js"; +import { IDiagnosticSession } from "./types"; + +// Default trace duration when the caller doesn't specify one. +const DEFAULT_PGO_DURATION_SECONDS = 10; + +// the session currently collecting a PGO trace; stopped internally when the duration elapses +let pgoSession: IDiagnosticSession | undefined = undefined; + +export function collectPgoTrace(options?: DiagnosticCommandOptions, startup?: boolean): Promise { + if (!options) options = {}; + if (!startup && !serverSession) { + throw new Error("No active JS diagnostic session"); + } + + const durationSeconds = options.durationSeconds ?? DEFAULT_PGO_DURATION_SECONDS; + + const onClosePromise = dotnetLoaderExports.createPromiseCompletionSource(); + function onSessionStart(session: IDiagnosticSession): void { + pgoSession = session; + session.sendCommand(commandResumeRuntime()); + // stop and flush the trace once the duration elapses + Module.safeSetTimeout(() => { + stopPgoTrace(); + }, 1000 * durationSeconds); + } + + setupJsClient({ + onClosePromise: onClosePromise, + skipDownload: options.skipDownload, + commandOnAdvertise: () => commandPgoTrace(options!), + onSessionStart, + onClose: () => { + pgoSession = undefined; + }, + }, startup); + return onClosePromise.promise; +} + +// stops the in-progress PGO trace when the collection duration elapses +function stopPgoTrace(): void { + if (!pgoSession) { + return; + } + const session = pgoSession; + pgoSession = undefined; + session.sendCommand(commandStopTracing(session.sessionId)); +} diff --git a/src/native/libs/System.Native.Browser/diagnostics/index.ts b/src/native/libs/System.Native.Browser/diagnostics/index.ts index 62cfe9b8a58466..9e63a43ecc60ea 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/index.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/index.ts @@ -13,6 +13,7 @@ import { installLoggingProxy } from "./console-proxy"; import { collectMetrics } from "./dotnet-counters"; import { collectGcDump } from "./dotnet-gcdump"; import { collectCpuSamples } from "./dotnet-cpu-profiler"; +import { collectPgoTrace } from "./dotnet-pgo-trace"; import { connectDSRouter, ds_rt_websocket_close, ds_rt_websocket_create, ds_rt_websocket_poll, ds_rt_websocket_recv, ds_rt_websocket_send, initializeDS } from "./diagnostic-server"; import { ds_rt_browser_performance_measure } from "./browser-profiler"; @@ -45,6 +46,7 @@ export function dotnetInitializeModule(internals: InternalExchange): void { dotnetApi.collectCpuSamples = collectCpuSamples; dotnetApi.collectMetrics = collectMetrics; dotnetApi.collectGcDump = collectGcDump; + dotnetApi.collectPgoTrace = collectPgoTrace; dotnetApi.connectDSRouter = connectDSRouter; // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/native/libs/System.Native.Browser/diagnostics/types.ts b/src/native/libs/System.Native.Browser/diagnostics/types.ts index 4784190ee98bc4..6454e9f0b3eca1 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/types.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/types.ts @@ -216,6 +216,10 @@ export const enum Keywords { WaitHandle = 0x40000000000, // // Summary: + // Diagnostic events carrying JIT/interpreter PGO instrumentation data (block counts). + JitInstrumentationData = 0x10000000000, + // + // Summary: // Recommend default flags (good compromise on verbosity). Default = 0x14C14FCCBD, // From ab76425d8ae550bcbc9fc3e6d0048e51eb6f97d0 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 25 Aug 2026 14:13:08 +0200 Subject: [PATCH 02/29] Define ep_rt_session_stopping no-op for NativeAOT eventpipe --- src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h b/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h index 1622d2bbae19f3..8ef4d373a628ec 100644 --- a/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h +++ b/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h @@ -594,6 +594,14 @@ ep_rt_notify_profiler_provider_created (EventPipeProvider *provider) // Following mono's path of no-op } +static +inline +void +ep_rt_session_stopping (void) +{ + // Following mono's path of no-op +} + /* * Arrays. */ From 1d4dd4700acb49daf213338eacfda94c5aa168ef Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 25 Aug 2026 16:34:38 +0200 Subject: [PATCH 03/29] Flush PGO data before taking the EventPipe lock on session stop --- src/native/eventpipe/ep.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/native/eventpipe/ep.c b/src/native/eventpipe/ep.c index 36a4801b5427d2..3d301ed05b3978 100644 --- a/src/native/eventpipe/ep.c +++ b/src/native/eventpipe/ep.c @@ -723,10 +723,6 @@ disable_holding_lock ( // Log the process information event. log_process_info_event (ep_event_source_get ()); - // Give the runtime a chance to emit any pending end-of-session data (e.g. block-count - // PGO) while the session's providers are still enabled and its buffers are still live. - ep_rt_session_stopping (); - // Disable session tracing. config_enable_disable (ep_config_get (), session, provider_callback_data_queue, false); @@ -806,6 +802,11 @@ stop_session (EventPipeSessionID id) EventPipeProviderCallbackData provider_callback_data; EventPipeProviderCallbackDataQueue *provider_callback_data_queue = ep_provider_callback_data_queue_init (&callback_data_queue); + // Give the runtime a chance to emit any pending end-of-session data (e.g. block-count PGO) + // into the still-live session. This must run before taking the EventPipe lock: emitting events + // re-enters the write path, which requires the lock not be held. + ep_rt_session_stopping (); + EP_LOCK_ENTER (section1) if (is_session_id_in_collection (id)) disable_holding_lock (id, provider_callback_data_queue); From 47d4eabae7347b0de6fa2c3b4186c0fd2fe36358 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 25 Aug 2026 17:43:00 +0200 Subject: [PATCH 04/29] enable BlazorEventPipeTestWithCpuSamples --- .../Microsoft.NET.Sdk.WebAssembly.Browser.CoreCLR.targets | 3 +++ .../wasm/Wasm.Build.Tests/Blazor/EventPipeDiagnosticsTests.cs | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.CoreCLR.targets b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.CoreCLR.targets index 70d3fc52add4e7..07a797aed9aebd 100644 --- a/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.CoreCLR.targets +++ b/src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.CoreCLR.targets @@ -44,6 +44,9 @@ Copyright (c) .NET Foundation. All rights reserved. Text="PublishReadyToRunComposite is not supported for CoreCLR browser-wasm; only non-composite (per-assembly) R2R images are supported." /> + + <_WasmRuntimePackR2RDir Condition="'$(_WasmRuntimePackR2RDir)' == '' and '$(_RuntimePackNativeDir)' != ''">$([MSBuild]::NormalizeDirectory('$(_RuntimePackNativeDir)', 'r2r')) diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/EventPipeDiagnosticsTests.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/EventPipeDiagnosticsTests.cs index aa9daf674b9198..909a61e68ae25d 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Blazor/EventPipeDiagnosticsTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Blazor/EventPipeDiagnosticsTests.cs @@ -41,13 +41,14 @@ public EventPipeDiagnosticsTests(ITestOutputHelper output, SharedBuildPerTestCla [Theory] [InlineData(Configuration.Debug, false)] [InlineData(Configuration.Release, false)] - [ActiveIssue("https://github.com/dotnet/runtime/issues/132410", typeof(BuildTestBase), nameof(IsCoreClrRuntime))] public async Task BlazorEventPipeTestWithCpuSamples(Configuration config, bool aot) { + // force no R2R until https://github.com/dotnet/runtime/issues/130521 string extraProperties = @" all,interval=0 true 0 + false true "; From b7df191e495f70ec35ac1ffbadb562edc36a7052 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 26 Aug 2026 11:30:07 +0200 Subject: [PATCH 05/29] WBT test for PGO trace --- .../Blazor/EventPipeDiagnosticsTests.cs | 75 +++++++++++++++++++ .../Wasm.Build.Tests/Wasm.Build.Tests.csproj | 18 +++++ 2 files changed, 93 insertions(+) diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/EventPipeDiagnosticsTests.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/EventPipeDiagnosticsTests.cs index 909a61e68ae25d..cff9dc4f9f400c 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Blazor/EventPipeDiagnosticsTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Blazor/EventPipeDiagnosticsTests.cs @@ -93,6 +93,49 @@ await RunForBuildWithDotnetRun(new BlazorRunOptions( Assert.True(methodFound, "The cpuprofile.nettrace should contain stack frames for the 'Counter.IncrementCount' method"); } + [ConditionalTheory(typeof(BuildTestBase), nameof(IsCoreClrRuntime))] + [InlineData(Configuration.Debug, false)] + [InlineData(Configuration.Release, false)] + public async Task BlazorEventPipeTestWithInterpPgo(Configuration config, bool aot) + { + // Interpreter block-count PGO: instrumentation is armed from startup by DOTNET_InterpPGO, the + // counters are collected into an EventPipe trace while the app runs, and dotnet-pgo turns that + // trace into an .mibc. R2R is forced off until https://github.com/dotnet/runtime/issues/130521. + string extraProperties = @" + true + 0 + false + true + "; + string extraItems = @""; + + ProjectInfo info = CopyTestAsset(config, aot, TestAsset.BlazorBasicTestApp, "blazor_interp_pgo", extraProperties: extraProperties, extraItems: extraItems); + + UpdateCounterPage(); + + BuildProject(info, config, new BuildOptions(AssertAppBundle: false)); + + async Task CollectInterpPgoTest(IPage page) + { + await SetupCounterPage(page, "pgo.nettrace", "globalThis.getDotnetRuntime(0).collectPgoTrace({ durationSeconds: 5.0, skipDownload: true })"); + await ClickAndCollect(page); + } + + await RunForBuildWithDotnetRun(new BlazorRunOptions( + Configuration: config, + Test: CollectInterpPgoTest, + TimeoutSeconds: 60, + CheckCounter: false, + ServerEnvironment: new Dictionary + { + ["DEVSERVER_UPLOAD_PATH"] = info.LogPath, + ["DEVSERVER_UPLOAD_PATTERN"] = uploadPattern + } + )); + + ValidateInterpPgoTrace(info, config, "pgo.nettrace", "IncrementCount"); + } + [Fact] public async Task BlazorEventPipeTestWithMetrics() { @@ -209,6 +252,38 @@ await RunForBuildWithDotnetRun(new BlazorRunOptions( } } + private void ValidateInterpPgoTrace(ProjectInfo info, Configuration config, string traceFileName, string expectedMethod) + { + string tracePath = Path.GetFullPath(Path.Combine(info.LogPath, traceFileName)); + Assert.True(File.Exists(tracePath), $"PGO trace {tracePath} was not created"); + + // The untrimmed IL assemblies next to the app (bin//) share the MVID of the served + // webcil, so dotnet-pgo can resolve the block-count events against them. + string referenceDir = Path.Combine(_projectDir, "bin", config.ToString(), DefaultTargetFrameworkForBlazor); + Assert.True(Directory.Exists(referenceDir), $"Reference assembly directory {referenceDir} was not found"); + + // dotnet-pgo is copied next to the test by the _CopyDotnetPgoToTestOutput target. + string pgoTool = Path.Combine(AppContext.BaseDirectory, "dotnet-pgo", "dotnet-pgo.dll"); + Assert.True(File.Exists(pgoTool), $"dotnet-pgo was not found at {pgoTool}"); + + string mibcPath = Path.Combine(info.LogPath, "pgo.mibc"); + + using (var createCmd = new DotNetCommand(s_buildEnv, _testOutput, useDefaultArgs: false).WithWorkingDirectory(_projectDir)) + { + createCmd.ExecuteWithCapturedOutput( + $"exec \"{pgoTool}\" create-mibc --trace \"{tracePath}\" --reference \"{Path.Combine(referenceDir, "*.dll")}\" --output \"{mibcPath}\"") + .EnsureSuccessful(); + } + Assert.True(File.Exists(mibcPath), $"dotnet-pgo did not produce {mibcPath}"); + + string dumpPath = Path.Combine(info.LogPath, "pgo.dump.txt"); + using (var dumpCmd = new DotNetCommand(s_buildEnv, _testOutput, useDefaultArgs: false).WithWorkingDirectory(_projectDir)) + { + dumpCmd.ExecuteWithCapturedOutput($"exec \"{pgoTool}\" dump --input \"{mibcPath}\" --output \"{dumpPath}\"").EnsureSuccessful(); + } + Assert.Contains(expectedMethod, File.ReadAllText(dumpPath)); + } + private string ConvertTrace(ProjectInfo info, string fileName) { var traceFilePath = Path.GetFullPath(Path.Combine(info.LogPath, fileName)); diff --git a/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj b/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj index 1d61acd4817efb..fdd3f5858cd2ee 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj +++ b/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj @@ -57,11 +57,29 @@ + + + + + + + + + + <_DotnetPgoOutputFiles Include="%(_DotnetPgoBuiltAssembly.RootDir)%(_DotnetPgoBuiltAssembly.Directory)**\*" /> + + + + From 38ef90059cb755a828538818cd4d9abcf7e65289 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 26 Aug 2026 11:43:27 +0200 Subject: [PATCH 06/29] feedback --- src/coreclr/vm/jitinterface.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index 78154e66bfc9ec..56898831e858fa 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -13064,12 +13064,8 @@ CORJIT_FLAGS* CEECodeGenInfo::getJitFlagsInternal() #ifdef FEATURE_PGO static bool InterpreterPgoInstrumentationEnabled() { - static LONG s_enabled = -1; - if (s_enabled == -1) - { - InterlockedExchange(&s_enabled, (CLRConfig::GetConfigValue(CLRConfig::INTERNAL_InterpPGO) != 0) ? 1 : 0); - } - return s_enabled != 0; + static ConfigDWORD s_interpPgo; + return s_interpPgo.val(CLRConfig::INTERNAL_InterpPGO) != 0; } #endif // FEATURE_PGO From 0b4176a6af8b6a254e647cb256acb5196c74f694 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 26 Aug 2026 14:10:46 +0200 Subject: [PATCH 07/29] fix event pipe early collection --- src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h index e57d143fb530fd..d54e561cbf9871 100644 --- a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h +++ b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h @@ -1103,8 +1103,13 @@ ep_rt_queue_job ( void *params) { #ifdef HOST_BROWSER - // In single-threaded mode the job runs on the browser event loop - SystemJS_DiagnosticServerQueueJob ((ep_rt_job_cb_t)job_func, params); + // In single-threaded mode the job runs on the browser event loop. Run the callback inline the + // first time so the diagnostic server makes progress synchronously (e.g. it can connect and + // resume during startup suspension) and only defer a re-schedule if it isn't done yet. Mirrors + // the Mono ep_rt_queue_job in ep-rt-mono.h. + ep_rt_job_cb_t cb = (ep_rt_job_cb_t)job_func; + if (!cb (params)) + SystemJS_DiagnosticServerQueueJob (cb, params); return true; #else EP_UNREACHABLE ("Not implemented on this platform"); From 62594a2bbd8e929d1d6d7ddb0585257f85632939 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 26 Aug 2026 17:28:42 +0200 Subject: [PATCH 08/29] Fix WBT dotnet-pgo copy: pre-create nested output dirs (amd64/arm64) The _CopyDotnetPgoToTestOutput Copy fails on Linux CI (MSB3026) creating nested dotnet-pgo/amd64 and /arm64 destination dirs; MakeDir them first. --- src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj b/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj index fdd3f5858cd2ee..1fdf72af914996 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj +++ b/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj @@ -75,6 +75,8 @@ <_DotnetPgoOutputFiles Include="%(_DotnetPgoBuiltAssembly.RootDir)%(_DotnetPgoBuiltAssembly.Directory)**\*" /> + + From 9e738b6dac7c6d5632f1b3a4668ed12f8c298970 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 9 Sep 2026 20:21:50 +0200 Subject: [PATCH 09/29] fix WBT linux --- src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj b/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj index 1fdf72af914996..fe67d398d39679 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj +++ b/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj @@ -73,12 +73,12 @@ - <_DotnetPgoOutputFiles Include="%(_DotnetPgoBuiltAssembly.RootDir)%(_DotnetPgoBuiltAssembly.Directory)**\*" /> + + <_DotnetPgoOutputFiles Include="%(_DotnetPgoBuiltAssembly.RootDir)%(_DotnetPgoBuiltAssembly.Directory)*" /> - - From 3f302aa638ecb86bda1948458d1d9cdce37c949d Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 10 Sep 2026 10:23:34 +0200 Subject: [PATCH 10/29] Deploy dotnet-pgo into WBT Helix payload via CopyToOutputDirectory The _CopyDotnetPgoToTestOutput target copied dotnet-pgo into $(OutDir), but the browser test archive zips $(BundleDir), so the tool never reached Helix (EventPipeDiagnosticsTests failed with 'dotnet-pgo was not found'). The dotnet-pgo ProjectReference also copy-localized the extensionless apphost to $(OutDir)dotnet-pgo, colliding with the copy destination folder on Linux (MSB3021). Build dotnet-pgo and deploy its root output as None/CopyToOutputDirectory items linked into a dotnet-pgo/ subfolder, matching testassets/data, so it flows into the payload with no root collision. --- .../Blazor/EventPipeDiagnosticsTests.cs | 2 +- .../Wasm.Build.Tests/Wasm.Build.Tests.csproj | 22 +++++++++---------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/EventPipeDiagnosticsTests.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/EventPipeDiagnosticsTests.cs index cff9dc4f9f400c..86ac62f8637d70 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Blazor/EventPipeDiagnosticsTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Blazor/EventPipeDiagnosticsTests.cs @@ -262,7 +262,7 @@ private void ValidateInterpPgoTrace(ProjectInfo info, Configuration config, stri string referenceDir = Path.Combine(_projectDir, "bin", config.ToString(), DefaultTargetFrameworkForBlazor); Assert.True(Directory.Exists(referenceDir), $"Reference assembly directory {referenceDir} was not found"); - // dotnet-pgo is copied next to the test by the _CopyDotnetPgoToTestOutput target. + // dotnet-pgo is deployed next to the test by the _AddDotnetPgoToTestPayload target. string pgoTool = Path.Combine(AppContext.BaseDirectory, "dotnet-pgo", "dotnet-pgo.dll"); Assert.True(File.Exists(pgoTool), $"dotnet-pgo was not found at {pgoTool}"); diff --git a/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj b/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj index fe67d398d39679..b6f2122263fcbe 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj +++ b/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj @@ -57,29 +57,27 @@ - - - - - - + + + - <_DotnetPgoOutputFiles Include="%(_DotnetPgoBuiltAssembly.RootDir)%(_DotnetPgoBuiltAssembly.Directory)*" /> + - From 6c453c90cf8156b33266393b6d02a86112054640 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 10 Sep 2026 12:19:03 +0200 Subject: [PATCH 11/29] Restore dotnet-pgo for WBT via Private=false ProjectReference Removing the ProjectReference took dotnet-pgo out of WBT's restore graph, so its project.assets.json was missing and the explicit build in _AddDotnetPgoToTestPayload failed with NETSDK1004. Re-add the reference with ReferenceOutputAssembly=false and Private=false so dotnet-pgo is restored/built without copying its apphost and deps into OutDir (which would re-collide with the dotnet-pgo/ folder). --- src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj b/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj index b6f2122263fcbe..d9fe15c7ea6c8f 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj +++ b/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj @@ -57,6 +57,14 @@ + + + From 9c215f5f1d155f27c645dcc06b1a24f7c4d46b46 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 11 Sep 2026 09:07:02 +0200 Subject: [PATCH 12/29] Fix dotnet-pgo payload glob dropping runtimeconfig.json The None Include mixed item-metadata batching with a wildcard, which MSBuild does not expand reliably: only dotnet-pgo.dll was deployed while dotnet-pgo.runtimeconfig.json and .deps.json were dropped, so on Helix the tool ran as self-contained and failed to find libhostpolicy.so. Transform the built-assembly item to a directory property, glob it into a separate item, then add that to None so the full tool folder (including runtimeconfig.json) reaches the test payload. --- .../wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj b/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj index d9fe15c7ea6c8f..5f3c837686ffbc 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj +++ b/src/mono/wasm/Wasm.Build.Tests/Wasm.Build.Tests.csproj @@ -79,10 +79,16 @@ + + <_DotnetPgoDir>@(_DotnetPgoBuiltAssembly->'%(RootDir)%(Directory)') + - - + <_DotnetPgoFile Include="$(_DotnetPgoDir)*" /> + From 1abefcb906e02257b8a7965e5cc3674655ef7c99 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 11 Sep 2026 16:38:18 +0200 Subject: [PATCH 13/29] Address PR review feedback for interpreter PGO - interpexec: increment block-count counter as uint32_t (no signed-overflow UB) - jitinterface: target-scope InterpreterPgoInstrumentationEnabled to browser/wasi - ep-rt-coreclr: only flush block-count PGO in the single-threaded (PERFTRACING_DISABLE_THREADS) config so concurrent sessions can't get duplicate chunks - dotnet-pgo-trace.ts: reject overlapping PGO collection and stop only the matching session - public-api.ts/dotnet.d.ts: capitalize collect* doc comments - EventPipeDiagnosticsTests: assert BasicBlockIntCount is present in the mibc dump --- .../vm/eventing/eventpipe/ep-rt-coreclr.cpp | 4 +- src/coreclr/vm/interpexec.cpp | 2 +- src/coreclr/vm/jitinterface.cpp | 4 + .../Blazor/EventPipeDiagnosticsTests.cs | 6 +- .../libs/Common/JavaScript/loader/dotnet.d.ts | 132 +++++++++--------- .../Common/JavaScript/types/public-api.ts | 8 +- .../diagnostics/dotnet-pgo-trace.ts | 18 ++- 7 files changed, 95 insertions(+), 79 deletions(-) diff --git a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp index 6440a23208d475..9db0b21e495acc 100644 --- a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp +++ b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp @@ -176,14 +176,14 @@ void ep_rt_coreclr_session_stopping (void) { STATIC_CONTRACT_NOTHROW; -#ifdef FEATURE_PGO +#if defined(FEATURE_PGO) && defined(PERFTRACING_DISABLE_THREADS) EX_TRY { PgoManager::FlushInstrumentationData (); } EX_CATCH { } EX_END_CATCH -#endif // FEATURE_PGO +#endif // FEATURE_PGO && PERFTRACING_DISABLE_THREADS } #endif /* ENABLE_PERFTRACING */ diff --git a/src/coreclr/vm/interpexec.cpp b/src/coreclr/vm/interpexec.cpp index 391c2fca399416..9ee39772a594bb 100644 --- a/src/coreclr/vm/interpexec.cpp +++ b/src/coreclr/vm/interpexec.cpp @@ -2069,7 +2069,7 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr #if defined(TARGET_BROWSER) || defined(TARGET_WASI) INTOP_CASE(INTOP_PGO_COUNT) // Increment the block-count PGO counter whose address is stored as a data item. - (*(int32_t*)pMethod->pDataItems[ip[1]])++; + (*(uint32_t*)pMethod->pDataItems[ip[1]])++; ip += 2; INTOP_NEXT; #endif // TARGET_BROWSER || TARGET_WASI diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index 56898831e858fa..8f0a194d87d694 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -13064,8 +13064,12 @@ CORJIT_FLAGS* CEECodeGenInfo::getJitFlagsInternal() #ifdef FEATURE_PGO static bool InterpreterPgoInstrumentationEnabled() { +#if defined(TARGET_BROWSER) || defined(TARGET_WASI) static ConfigDWORD s_interpPgo; return s_interpPgo.val(CLRConfig::INTERNAL_InterpPGO) != 0; +#else + return false; +#endif } #endif // FEATURE_PGO diff --git a/src/mono/wasm/Wasm.Build.Tests/Blazor/EventPipeDiagnosticsTests.cs b/src/mono/wasm/Wasm.Build.Tests/Blazor/EventPipeDiagnosticsTests.cs index 86ac62f8637d70..fc217b97267ed8 100644 --- a/src/mono/wasm/Wasm.Build.Tests/Blazor/EventPipeDiagnosticsTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/Blazor/EventPipeDiagnosticsTests.cs @@ -281,7 +281,11 @@ private void ValidateInterpPgoTrace(ProjectInfo info, Configuration config, stri { dumpCmd.ExecuteWithCapturedOutput($"exec \"{pgoTool}\" dump --input \"{mibcPath}\" --output \"{dumpPath}\"").EnsureSuccessful(); } - Assert.Contains(expectedMethod, File.ReadAllText(dumpPath)); + string dumpText = File.ReadAllText(dumpPath); + Assert.Contains(expectedMethod, dumpText); + // The method list alone can be populated by Jit method-start events; require actual block-count + // instrumentation so the test fails if no INTOP_PGO_COUNT probe ran or the counters weren't flushed. + Assert.Contains("BasicBlockIntCount", dumpText); } private string ConvertTrace(ProjectInfo info, string fileName) diff --git a/src/native/libs/Common/JavaScript/loader/dotnet.d.ts b/src/native/libs/Common/JavaScript/loader/dotnet.d.ts index 39905a6c1b4687..657625ca5202d9 100644 --- a/src/native/libs/Common/JavaScript/loader/dotnet.d.ts +++ b/src/native/libs/Common/JavaScript/loader/dotnet.d.ts @@ -340,68 +340,68 @@ interface LoadingResource { url: string; response: Promise; } -type SingleAssetBehaviors = - /** - * The binary of the .NET runtime. - */ - "dotnetwasm" - /** - * The javascript module for loader. - */ - | "js-module-dotnet" - /** - * The javascript module for diagnostic server and client. - */ - | "js-module-diagnostics" - /** - * The javascript module for runtime. - */ - | "js-module-runtime" - /** - * The javascript module for emscripten. - */ - | "js-module-native" - /** - * Typically dotnet.boot.js - */ - | "manifest" - /** - * The debugging symbols - */ - | "symbols"; -type AssetBehaviors = SingleAssetBehaviors | - /** - * Load asset as a managed resource assembly. - */ - "resource" - /** - * Load asset as a managed assembly. - */ - | "assembly" - /** - * Load asset as a managed debugging information. - */ - | "pdb" - /** - * Store asset into the native heap. - */ - | "heap" - /** - * Load asset as an ICU data archive. - */ - | "icu" - /** - * Load asset into the virtual filesystem (for fopen, File.Open, etc). - */ - | "vfs" - /** - * The javascript module that came from nuget package . - */ - | "js-module-library-initializer" - /** - * Managed assembly packaged as Webcil v 1.0 - */ - | "webcil"; +type SingleAssetBehaviors = +/** + * The binary of the .NET runtime. + */ +"dotnetwasm" +/** + * The javascript module for loader. + */ + | "js-module-dotnet" +/** + * The javascript module for diagnostic server and client. + */ + | "js-module-diagnostics" +/** + * The javascript module for runtime. + */ + | "js-module-runtime" +/** + * The javascript module for emscripten. + */ + | "js-module-native" +/** + * Typically dotnet.boot.js + */ + | "manifest" +/** + * The debugging symbols + */ + | "symbols"; +type AssetBehaviors = SingleAssetBehaviors | +/** + * Load asset as a managed resource assembly. + */ +"resource" +/** + * Load asset as a managed assembly. + */ + | "assembly" +/** + * Load asset as a managed debugging information. + */ + | "pdb" +/** + * Store asset into the native heap. + */ + | "heap" +/** + * Load asset as an ICU data archive. + */ + | "icu" +/** + * Load asset into the virtual filesystem (for fopen, File.Open, etc). + */ + | "vfs" +/** + * The javascript module that came from nuget package . + */ + | "js-module-library-initializer" +/** + * Managed assembly packaged as Webcil v 1.0 + */ + | "webcil"; declare const enum GlobalizationMode { /** * Load sharded ICU data. @@ -617,24 +617,24 @@ type MemoryAPIType = { }; type DiagnosticsAPIType = { /** - * creates diagnostic trace file. Default is 60 seconds. + * Creates diagnostic trace file. Default is 60 seconds. * It could be opened in PerfView or Visual Studio as is. */ collectCpuSamples: (options?: DiagnosticCommandOptions) => Promise; /** - * creates diagnostic trace file. Default is 60 seconds. + * Creates diagnostic trace file. Default is 60 seconds. * It could be opened in PerfView or Visual Studio as is. * It could be summarized by `dotnet-trace report xxx.nettrace topN -n 10` */ collectMetrics: (options?: DiagnosticCommandOptions) => Promise; /** - * creates diagnostic trace file. + * Creates diagnostic trace file. * It could be opened in PerfView as is. * It could be converted for Visual Studio using `dotnet-gcdump convert`. */ collectGcDump: (options?: DiagnosticCommandOptions) => Promise; /** - * creates a startup PGO trace file (method list, and block counts when the interpreter is instrumented). + * Creates a startup PGO trace file (method list, and block counts when the interpreter is instrumented). * The trace stops and downloads automatically after `durationSeconds` (default 10s). * Convert it with `dotnet-pgo create-mibc --trace xxx.nettrace ...` to drive a profile-guided crossgen2 build. */ diff --git a/src/native/libs/Common/JavaScript/types/public-api.ts b/src/native/libs/Common/JavaScript/types/public-api.ts index dec79010bda183..dfb9021356196a 100644 --- a/src/native/libs/Common/JavaScript/types/public-api.ts +++ b/src/native/libs/Common/JavaScript/types/public-api.ts @@ -645,24 +645,24 @@ export type MemoryAPIType = { export type DiagnosticsAPIType = { /** - * creates diagnostic trace file. Default is 60 seconds. + * Creates diagnostic trace file. Default is 60 seconds. * It could be opened in PerfView or Visual Studio as is. */ collectCpuSamples: (options?: DiagnosticCommandOptions) => Promise; /** - * creates diagnostic trace file. Default is 60 seconds. + * Creates diagnostic trace file. Default is 60 seconds. * It could be opened in PerfView or Visual Studio as is. * It could be summarized by `dotnet-trace report xxx.nettrace topN -n 10` */ collectMetrics: (options?: DiagnosticCommandOptions) => Promise; /** - * creates diagnostic trace file. + * Creates diagnostic trace file. * It could be opened in PerfView as is. * It could be converted for Visual Studio using `dotnet-gcdump convert`. */ collectGcDump: (options?: DiagnosticCommandOptions) => Promise; /** - * creates a startup PGO trace file (method list, and block counts when the interpreter is instrumented). + * Creates a startup PGO trace file (method list, and block counts when the interpreter is instrumented). * The trace stops and downloads automatically after `durationSeconds` (default 10s). * Convert it with `dotnet-pgo create-mibc --trace xxx.nettrace ...` to drive a profile-guided crossgen2 build. */ diff --git a/src/native/libs/System.Native.Browser/diagnostics/dotnet-pgo-trace.ts b/src/native/libs/System.Native.Browser/diagnostics/dotnet-pgo-trace.ts index 1f90a7319916d3..f904837d9884d4 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/dotnet-pgo-trace.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/dotnet-pgo-trace.ts @@ -19,16 +19,20 @@ export function collectPgoTrace(options?: DiagnosticCommandOptions, startup?: bo if (!startup && !serverSession) { throw new Error("No active JS diagnostic session"); } + if (pgoSession) { + throw new Error("A PGO trace collection is already in progress"); + } const durationSeconds = options.durationSeconds ?? DEFAULT_PGO_DURATION_SECONDS; const onClosePromise = dotnetLoaderExports.createPromiseCompletionSource(); + let stopTimeoutId: number | undefined = undefined; function onSessionStart(session: IDiagnosticSession): void { pgoSession = session; session.sendCommand(commandResumeRuntime()); // stop and flush the trace once the duration elapses - Module.safeSetTimeout(() => { - stopPgoTrace(); + stopTimeoutId = Module.safeSetTimeout(() => { + stopPgoTrace(session); }, 1000 * durationSeconds); } @@ -39,17 +43,21 @@ export function collectPgoTrace(options?: DiagnosticCommandOptions, startup?: bo onSessionStart, onClose: () => { pgoSession = undefined; + if (stopTimeoutId !== undefined) { + globalThis.clearTimeout(stopTimeoutId); + stopTimeoutId = undefined; + } }, }, startup); return onClosePromise.promise; } // stops the in-progress PGO trace when the collection duration elapses -function stopPgoTrace(): void { - if (!pgoSession) { +function stopPgoTrace(session: IDiagnosticSession): void { + // ignore a stale timer whose session was already closed or replaced + if (pgoSession !== session) { return; } - const session = pgoSession; pgoSession = undefined; session.sendCommand(commandStopTracing(session.sessionId)); } From a983705090d4a71c4ca5ce5fbf8638b1040b8fe6 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 11 Sep 2026 16:43:35 +0200 Subject: [PATCH 14/29] Correct ep_rt_session_stopping lock-contract comment The hook runs before EP_LOCK_ENTER (it emits events, which re-enter the write path); the declaration comment said the opposite. --- src/native/eventpipe/ep-rt.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/native/eventpipe/ep-rt.h b/src/native/eventpipe/ep-rt.h index d260002f68ad18..86bcda51944da2 100644 --- a/src/native/eventpipe/ep-rt.h +++ b/src/native/eventpipe/ep-rt.h @@ -240,7 +240,8 @@ ep_rt_notify_profiler_provider_created (EventPipeProvider *provider); // Invoked while a session is stopping, before its providers are disabled and its buffers are // flushed, so the runtime can emit any pending end-of-session data (e.g. block-count PGO) into -// the still-open session. Runs on the disabling thread with the EventPipe lock held. +// the still-open session. Runs on the disabling thread before the EventPipe lock is taken, because +// emitting events re-enters the write path and must not run with that lock held. static void ep_rt_session_stopping (void); From 194b6a84b569eb5c63f844d4e2e07248b6461283 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 11 Sep 2026 17:12:15 +0200 Subject: [PATCH 15/29] Address follow-up PR review on interpreter PGO - compiler: allocate block-count counters as uint32_t* to match the uint32_t increment - dotnet-pgo-trace.ts: keep pgoSession set until onClose and clear it only for the active session, avoiding a stop-vs-close race --- src/coreclr/interpreter/compiler.cpp | 2 +- .../diagnostics/dotnet-pgo-trace.ts | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/coreclr/interpreter/compiler.cpp b/src/coreclr/interpreter/compiler.cpp index a843093417664e..cf59bf4dca557c 100644 --- a/src/coreclr/interpreter/compiler.cpp +++ b/src/coreclr/interpreter/compiler.cpp @@ -8754,7 +8754,7 @@ void InterpCompiler::InstrumentBlockCounts() // Insert an INTOP_PGO_COUNT probe at the start of each block, pointing at its counter. for (int32_t i = 0; i < numBlocks; i++) { - int32_t *pCounter = (int32_t*)(pInstrumentationData + pSchema[i].Offset); + uint32_t *pCounter = (uint32_t*)(pInstrumentationData + pSchema[i].Offset); InterpInst *ins = InsertInsBB(blocks.Get(i), NULL, INTOP_PGO_COUNT); // Probe is a pure counter increment with no IL mapping; keep it out of the debug maps. ins->ilOffset = -1; diff --git a/src/native/libs/System.Native.Browser/diagnostics/dotnet-pgo-trace.ts b/src/native/libs/System.Native.Browser/diagnostics/dotnet-pgo-trace.ts index f904837d9884d4..4d7a96b0cfdbc9 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/dotnet-pgo-trace.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/dotnet-pgo-trace.ts @@ -26,8 +26,10 @@ export function collectPgoTrace(options?: DiagnosticCommandOptions, startup?: bo const durationSeconds = options.durationSeconds ?? DEFAULT_PGO_DURATION_SECONDS; const onClosePromise = dotnetLoaderExports.createPromiseCompletionSource(); + let startedSession: IDiagnosticSession | undefined = undefined; let stopTimeoutId: number | undefined = undefined; function onSessionStart(session: IDiagnosticSession): void { + startedSession = session; pgoSession = session; session.sendCommand(commandResumeRuntime()); // stop and flush the trace once the duration elapses @@ -42,7 +44,10 @@ export function collectPgoTrace(options?: DiagnosticCommandOptions, startup?: bo commandOnAdvertise: () => commandPgoTrace(options!), onSessionStart, onClose: () => { - pgoSession = undefined; + // clear only if this call's session is still the active one + if (pgoSession === startedSession) { + pgoSession = undefined; + } if (stopTimeoutId !== undefined) { globalThis.clearTimeout(stopTimeoutId); stopTimeoutId = undefined; @@ -52,12 +57,11 @@ export function collectPgoTrace(options?: DiagnosticCommandOptions, startup?: bo return onClosePromise.promise; } -// stops the in-progress PGO trace when the collection duration elapses +// stops the in-progress PGO trace when the collection duration elapses; pgoSession stays set until onClose function stopPgoTrace(session: IDiagnosticSession): void { // ignore a stale timer whose session was already closed or replaced if (pgoSession !== session) { return; } - pgoSession = undefined; session.sendCommand(commandStopTracing(session.sessionId)); } From b3ec395e6799a06c1766245da399fd8aca968b68 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Fri, 11 Sep 2026 18:20:35 +0200 Subject: [PATCH 16/29] Scope interpreter PGO flush to the stopping session stop_session now validates the session id before invoking ep_rt_session_stopping and passes it through, so a stale/invalid stop can't trigger a flush. The CoreCLR hook flushes block-count PGO only when the stopping session's mask has JitInstrumentationDataVerbose enabled, so an unrelated session's stop can't re-emit chunks into an open PGO session. --- .../nativeaot/Runtime/eventpipe/ep-rt-aot.h | 3 ++- .../vm/eventing/eventpipe/ep-rt-coreclr.cpp | 23 +++++++++++++++---- .../vm/eventing/eventpipe/ep-rt-coreclr.h | 6 ++--- src/mono/mono/eventpipe/ep-rt-mono.h | 4 ++-- src/native/eventpipe/ep-rt.h | 9 ++++---- src/native/eventpipe/ep.c | 20 +++++++++++----- 6 files changed, 44 insertions(+), 21 deletions(-) diff --git a/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h b/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h index 8ef4d373a628ec..39c8fff8a17f27 100644 --- a/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h +++ b/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h @@ -597,9 +597,10 @@ ep_rt_notify_profiler_provider_created (EventPipeProvider *provider) static inline void -ep_rt_session_stopping (void) +ep_rt_session_stopping (EventPipeSessionID session_id) { // Following mono's path of no-op + (void)session_id; } /* diff --git a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp index 9db0b21e495acc..a679f1cf5fdf7d 100644 --- a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp +++ b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp @@ -6,6 +6,8 @@ #ifdef ENABLE_PERFTRACING #include #include +#include +#include #include #include #include "threadsuspend.h" @@ -173,16 +175,27 @@ ep_rt_coreclr_sample_profiler_write_sampling_event_for_threads ( } void -ep_rt_coreclr_session_stopping (void) +ep_rt_coreclr_session_stopping (EventPipeSessionID session_id) { STATIC_CONTRACT_NOTHROW; #if defined(FEATURE_PGO) && defined(PERFTRACING_DISABLE_THREADS) - EX_TRY + // Flush block-count PGO only into the session that enabled the JitInstrumentationData events, so an + // unrelated session's stop can't re-emit chunks into an open PGO session (dotnet-pgo drops a method + // once data arrives after its final chunk). + extern EventPipeEvent *EventPipeEventJitInstrumentationDataVerbose; + EventPipeSession *session = reinterpret_cast(static_cast(session_id)); + if (EventPipeEventJitInstrumentationDataVerbose != NULL && + ep_event_is_enabled_by_mask (EventPipeEventJitInstrumentationDataVerbose, ep_session_get_mask (session))) { - PgoManager::FlushInstrumentationData (); + EX_TRY + { + PgoManager::FlushInstrumentationData (); + } + EX_CATCH { } + EX_END_CATCH } - EX_CATCH { } - EX_END_CATCH +#else + (void)session_id; #endif // FEATURE_PGO && PERFTRACING_DISABLE_THREADS } diff --git a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h index d54e561cbf9871..baaedc404cddf8 100644 --- a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h +++ b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h @@ -662,11 +662,11 @@ ep_rt_notify_profiler_provider_created (EventPipeProvider *provider) static inline void -ep_rt_session_stopping (void) +ep_rt_session_stopping (EventPipeSessionID session_id) { STATIC_CONTRACT_NOTHROW; - extern void ep_rt_coreclr_session_stopping (void); - ep_rt_coreclr_session_stopping (); + extern void ep_rt_coreclr_session_stopping (EventPipeSessionID session_id); + ep_rt_coreclr_session_stopping (session_id); } /* diff --git a/src/mono/mono/eventpipe/ep-rt-mono.h b/src/mono/mono/eventpipe/ep-rt-mono.h index 54bf3b47986502..621ad0bb8b07d8 100644 --- a/src/mono/mono/eventpipe/ep-rt-mono.h +++ b/src/mono/mono/eventpipe/ep-rt-mono.h @@ -716,9 +716,9 @@ ep_rt_notify_profiler_provider_created (EventPipeProvider *provider) static inline void -ep_rt_session_stopping (void) +ep_rt_session_stopping (EventPipeSessionID session_id) { - ; + (void)session_id; } /* diff --git a/src/native/eventpipe/ep-rt.h b/src/native/eventpipe/ep-rt.h index 86bcda51944da2..d52bddb50301ad 100644 --- a/src/native/eventpipe/ep-rt.h +++ b/src/native/eventpipe/ep-rt.h @@ -239,12 +239,13 @@ ep_rt_notify_profiler_provider_created (EventPipeProvider *provider); */ // Invoked while a session is stopping, before its providers are disabled and its buffers are -// flushed, so the runtime can emit any pending end-of-session data (e.g. block-count PGO) into -// the still-open session. Runs on the disabling thread before the EventPipe lock is taken, because -// emitting events re-enters the write path and must not run with that lock held. +// flushed, so the runtime can emit any pending end-of-session data (e.g. block-count PGO) into the +// still-open session identified by session_id (already validated as live by the caller). Runs on the +// disabling thread before the EventPipe lock is taken, because emitting events re-enters the write +// path and must not run with that lock held. static void -ep_rt_session_stopping (void); +ep_rt_session_stopping (EventPipeSessionID session_id); /* * Arrays. diff --git a/src/native/eventpipe/ep.c b/src/native/eventpipe/ep.c index 3d301ed05b3978..20d63fef9b4589 100644 --- a/src/native/eventpipe/ep.c +++ b/src/native/eventpipe/ep.c @@ -802,15 +802,23 @@ stop_session (EventPipeSessionID id) EventPipeProviderCallbackData provider_callback_data; EventPipeProviderCallbackDataQueue *provider_callback_data_queue = ep_provider_callback_data_queue_init (&callback_data_queue); - // Give the runtime a chance to emit any pending end-of-session data (e.g. block-count PGO) - // into the still-live session. This must run before taking the EventPipe lock: emitting events - // re-enters the write path, which requires the lock not be held. - ep_rt_session_stopping (); - + // Give the runtime a chance to emit any pending end-of-session data (e.g. block-count PGO) into + // the still-live session. Validate the session id first so a stale/invalid stop request doesn't + // flush into other open sessions, and pass it so the runtime can target only that session. This + // must run before the disable lock: emitting events re-enters the write path, which requires the + // lock not be held. + bool is_active_session = false; EP_LOCK_ENTER (section1) + is_active_session = is_session_id_in_collection (id); + EP_LOCK_EXIT (section1) + + if (is_active_session) + ep_rt_session_stopping (id); + + EP_LOCK_ENTER (section2) if (is_session_id_in_collection (id)) disable_holding_lock (id, provider_callback_data_queue); - EP_LOCK_EXIT (section1) + EP_LOCK_EXIT (section2) while (ep_provider_callback_data_queue_try_dequeue (provider_callback_data_queue, &provider_callback_data)) { ep_rt_prepare_provider_invoke_callback (&provider_callback_data); From 97d7a19d008b2266b74345523c5c3d838ad30b5c Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 16 Sep 2026 12:55:15 +0200 Subject: [PATCH 17/29] Scope PGO block instrumentation to header IL range; assert on MT WASM flush Bound InstrumentBlockCounts block-head filter by m_ILCodeSizeFromILHeader so probes stay within the original IL and skip synthetic finally/epilog IL appended for synchronized/async methods. Add PORTABILITY_ASSERT in the multithreaded WASM branch of ep_rt_coreclr_session_stopping so PGO-enabled threaded WASM builds fail loudly instead of silently producing traces without block-count events. --- src/coreclr/interpreter/compiler.cpp | 2 +- src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/coreclr/interpreter/compiler.cpp b/src/coreclr/interpreter/compiler.cpp index cf59bf4dca557c..2ae5ab554d1765 100644 --- a/src/coreclr/interpreter/compiler.cpp +++ b/src/coreclr/interpreter/compiler.cpp @@ -8719,7 +8719,7 @@ void InterpCompiler::InstrumentBlockCounts() TArray blocks(GetMemPoolAllocator(IMK_DataItem)); for (InterpBasicBlock *bb = m_pEntryBB; bb != NULL; bb = bb->pNextBB) { - if (bb->ilOffset < 0 || bb->ilOffset >= m_ILCodeSize || m_ppOffsetToBB[bb->ilOffset] != bb) + if (bb->ilOffset < 0 || bb->ilOffset >= m_ILCodeSizeFromILHeader || m_ppOffsetToBB[bb->ilOffset] != bb) continue; if (bb->ilOffset == 0 || isBranchTarget[bb->index]) blocks.Add(bb); diff --git a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp index a679f1cf5fdf7d..17094ca0698226 100644 --- a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp +++ b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp @@ -194,6 +194,11 @@ ep_rt_coreclr_session_stopping (EventPipeSessionID session_id) EX_CATCH { } EX_END_CATCH } +#elif defined(FEATURE_PGO) && (defined(TARGET_BROWSER) || defined(TARGET_WASI)) + // Multithreaded WASM: interpreter block-count PGO has no synchronized flush path yet, so stopping + // a PGO trace here would silently drop the block-count events. Flag the unimplemented config. + PORTABILITY_ASSERT ("Interpreter block-count PGO flush is not implemented for multithreaded WASM (requires PERFTRACING_DISABLE_THREADS)."); + (void)session_id; #else (void)session_id; #endif // FEATURE_PGO && PERFTRACING_DISABLE_THREADS From c9b1e4bc4af42abb840e0306f3e0b4a1abb9de01 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 16 Sep 2026 13:18:06 +0200 Subject: [PATCH 18/29] Route PGO flush to the stopping session via rundown thread In the single-threaded WASM PGO flush path, mark the current thread as a rundown thread bound to the stopping session so JitInstrumentationDataVerbose events emitted by FlushInstrumentationData are written only to that session instead of broadcast to every enabled session. Clears the marker after the flush, including on exception. --- .../vm/eventing/eventpipe/ep-rt-coreclr.cpp | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp index 17094ca0698226..a3634646c4d52f 100644 --- a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp +++ b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp @@ -187,12 +187,21 @@ ep_rt_coreclr_session_stopping (EventPipeSessionID session_id) if (EventPipeEventJitInstrumentationDataVerbose != NULL && ep_event_is_enabled_by_mask (EventPipeEventJitInstrumentationDataVerbose, ep_session_get_mask (session))) { - EX_TRY + // Mark this thread as a rundown thread bound to the stopping session so the events emitted by the + // flush are routed to that single session (ep_session_write_event) instead of broadcast to every + // enabled session; the marker is cleared after the flush, including on exception. + EventPipeThread *thread = ep_thread_get_or_create (); + if (thread != NULL) { - PgoManager::FlushInstrumentationData (); + ep_thread_set_as_rundown_thread (thread, session); + EX_TRY + { + PgoManager::FlushInstrumentationData (); + } + EX_CATCH { } + EX_END_CATCH + ep_thread_set_as_rundown_thread (thread, NULL); } - EX_CATCH { } - EX_END_CATCH } #elif defined(FEATURE_PGO) && (defined(TARGET_BROWSER) || defined(TARGET_WASI)) // Multithreaded WASM: interpreter block-count PGO has no synchronized flush path yet, so stopping From 3f7c7cd096617503c42a41cf74148024bf2518df Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 16 Sep 2026 13:26:26 +0200 Subject: [PATCH 19/29] Scope interpreter PGO to single-threaded WASM; mask-gate MT assert Gate the interpreter block-count PGO instrumentation (schema build, INTOP_PGO_COUNT opcode/probe, and enable flags) on PERFTRACING_DISABLE_THREADS in addition to TARGET_BROWSER/TARGET_WASI, so multithreaded WASM never emits the racy plain read-modify-write counter increment. Gate the multithreaded-WASM PORTABILITY_ASSERT in ep_rt_coreclr_session_stopping behind the stopping session's JitInstrumentationDataVerbose keyword mask, so it only trips on a genuine PGO-collection attempt and not when unrelated sessions (CPU/GC/counters) stop. --- src/coreclr/interpreter/compiler.cpp | 16 ++++++++-------- src/coreclr/interpreter/compiler.h | 12 ++++++------ src/coreclr/interpreter/eeinterp.cpp | 4 ++-- src/coreclr/interpreter/inc/intops.def | 2 +- .../vm/eventing/eventpipe/ep-rt-coreclr.cpp | 13 ++++++++++--- src/coreclr/vm/interpexec.cpp | 4 ++-- 6 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/coreclr/interpreter/compiler.cpp b/src/coreclr/interpreter/compiler.cpp index 2ae5ab554d1765..defb115e4c9034 100644 --- a/src/coreclr/interpreter/compiler.cpp +++ b/src/coreclr/interpreter/compiler.cpp @@ -51,9 +51,9 @@ bool InterpCompiler::s_samplingProfilerEnabled = false; bool InterpCompiler::s_browserProfilerEnabled = false; #endif #endif // PERFTRACING_DISABLE_THREADS -#if defined(TARGET_BROWSER) || defined(TARGET_WASI) +#if (defined(TARGET_BROWSER) || defined(TARGET_WASI)) && defined(PERFTRACING_DISABLE_THREADS) bool InterpCompiler::s_interpPgoEnabled = false; -#endif // defined(TARGET_BROWSER) || defined(TARGET_WASI) +#endif // (TARGET_BROWSER || TARGET_WASI) && PERFTRACING_DISABLE_THREADS #if MEASURE_MEM_ALLOC #include @@ -2249,11 +2249,11 @@ InterpCompiler::InterpCompiler(COMP_HANDLE compHnd, #endif #endif // PERFTRACING_DISABLE_THREADS -#if defined(TARGET_BROWSER) || defined(TARGET_WASI) +#if (defined(TARGET_BROWSER) || defined(TARGET_WASI)) && defined(PERFTRACING_DISABLE_THREADS) m_emitInterpPGO = s_interpPgoEnabled && (InterpConfig.InterpPGOMethods().isEmpty() || InterpConfig.InterpPGOMethods().contains(compHnd, m_methodHnd, m_classHnd, &m_methodInfo->args)); -#endif // defined(TARGET_BROWSER) || defined(TARGET_WASI) +#endif // (TARGET_BROWSER || TARGET_WASI) && PERFTRACING_DISABLE_THREADS #ifdef DEBUG m_methodName = ::PrintMethodName(compHnd, m_classHnd, m_methodHnd, &m_methodInfo->args, @@ -2357,10 +2357,10 @@ bool InterpCompiler::CompileMethod() } #endif -#if defined(TARGET_BROWSER) || defined(TARGET_WASI) +#if (defined(TARGET_BROWSER) || defined(TARGET_WASI)) && defined(PERFTRACING_DISABLE_THREADS) if (m_emitInterpPGO) InstrumentBlockCounts(); -#endif // defined(TARGET_BROWSER) || defined(TARGET_WASI) +#endif // (TARGET_BROWSER || TARGET_WASI) && PERFTRACING_DISABLE_THREADS AllocOffsets(); PatchInitLocals(m_methodInfo); @@ -8682,7 +8682,7 @@ void InterpCompiler::CreateSynchronizedRetValVar() INTERP_DUMP("Created ret val var V%d\n", m_synchronizedOrAsyncRetValVarIndex); } -#if defined(TARGET_BROWSER) || defined(TARGET_WASI) +#if (defined(TARGET_BROWSER) || defined(TARGET_WASI)) && defined(PERFTRACING_DISABLE_THREADS) // Instrument each basic block with a block-count PGO probe. The counters are allocated by // allocPgoInstrumentationBySchema (native PgoManager memory), so they persist independently of // EventPipe session lifetime; the accumulated profile is flushed to the trace as @@ -8761,7 +8761,7 @@ void InterpCompiler::InstrumentBlockCounts() ins->data[0] = GetDataItemIndex((void*)pCounter); } } -#endif // TARGET_BROWSER || TARGET_WASI +#endif // (TARGET_BROWSER || TARGET_WASI) && PERFTRACING_DISABLE_THREADS void InterpCompiler::GenerateCode(CORINFO_METHOD_INFO* methodInfo) { diff --git a/src/coreclr/interpreter/compiler.h b/src/coreclr/interpreter/compiler.h index ef09fc9a9bac44..861d06caffc0f4 100644 --- a/src/coreclr/interpreter/compiler.h +++ b/src/coreclr/interpreter/compiler.h @@ -641,9 +641,9 @@ class InterpCompiler #endif #endif // PERFTRACING_DISABLE_THREADS -#if defined(TARGET_BROWSER) || defined(TARGET_WASI) +#if (defined(TARGET_BROWSER) || defined(TARGET_WASI)) && defined(PERFTRACING_DISABLE_THREADS) bool m_emitInterpPGO; -#endif // defined(TARGET_BROWSER) || defined(TARGET_WASI) +#endif // (TARGET_BROWSER || TARGET_WASI) && PERFTRACING_DISABLE_THREADS void DeclarePointerIsClass(CORINFO_CLASS_HANDLE clsHnd) { @@ -772,9 +772,9 @@ class InterpCompiler void GenerateCode(CORINFO_METHOD_INFO* methodInfo); -#if defined(TARGET_BROWSER) || defined(TARGET_WASI) +#if (defined(TARGET_BROWSER) || defined(TARGET_WASI)) && defined(PERFTRACING_DISABLE_THREADS) void InstrumentBlockCounts(); -#endif // defined(TARGET_BROWSER) || defined(TARGET_WASI) +#endif // (TARGET_BROWSER || TARGET_WASI) && PERFTRACING_DISABLE_THREADS InterpBasicBlock* GenerateCodeForLeaveChainIslands(InterpBasicBlock *pNewBB, InterpBasicBlock *pPrevBB); void PatchInitLocals(CORINFO_METHOD_INFO* methodInfo); @@ -1158,9 +1158,9 @@ class InterpCompiler static bool s_browserProfilerEnabled; #endif #endif // PERFTRACING_DISABLE_THREADS -#if defined(TARGET_BROWSER) || defined(TARGET_WASI) +#if (defined(TARGET_BROWSER) || defined(TARGET_WASI)) && defined(PERFTRACING_DISABLE_THREADS) static bool s_interpPgoEnabled; -#endif // defined(TARGET_BROWSER) || defined(TARGET_WASI) +#endif // (TARGET_BROWSER || TARGET_WASI) && PERFTRACING_DISABLE_THREADS #if MEASURE_MEM_ALLOC // Memory statistics for profiling. diff --git a/src/coreclr/interpreter/eeinterp.cpp b/src/coreclr/interpreter/eeinterp.cpp index 6fb261a5071944..eeb9f6100d004c 100644 --- a/src/coreclr/interpreter/eeinterp.cpp +++ b/src/coreclr/interpreter/eeinterp.cpp @@ -43,9 +43,9 @@ extern "C" INTERP_API void jitStartup(ICorJitHost* jitHost) if (InterpConfig.InterpPGO() != 0) { -#if defined(TARGET_BROWSER) || defined(TARGET_WASI) +#if (defined(TARGET_BROWSER) || defined(TARGET_WASI)) && defined(PERFTRACING_DISABLE_THREADS) InterpCompiler::s_interpPgoEnabled = true; -#endif // defined(TARGET_BROWSER) || defined(TARGET_WASI) +#endif // (TARGET_BROWSER || TARGET_WASI) && PERFTRACING_DISABLE_THREADS } g_interpInitialized = true; diff --git a/src/coreclr/interpreter/inc/intops.def b/src/coreclr/interpreter/inc/intops.def index 8e0f454f1ea30a..f6bf2f52bcf213 100644 --- a/src/coreclr/interpreter/inc/intops.def +++ b/src/coreclr/interpreter/inc/intops.def @@ -90,7 +90,7 @@ OPDEF(INTOP_PROF_ENTER, "prof.enter", 2, 0, 0, InterpOpMethodHandle) OPDEF(INTOP_PROF_LEAVE, "prof.leave", 1, 0, 0, InterpOpNoArgs) #endif -#if defined(TARGET_BROWSER) || defined(TARGET_WASI) +#if (defined(TARGET_BROWSER) || defined(TARGET_WASI)) && defined(PERFTRACING_DISABLE_THREADS) OPDEF(INTOP_PGO_COUNT, "pgo.count", 2, 0, 0, InterpOpLdPtr) #endif diff --git a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp index a3634646c4d52f..545896afbfb565 100644 --- a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp +++ b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp @@ -204,9 +204,16 @@ ep_rt_coreclr_session_stopping (EventPipeSessionID session_id) } } #elif defined(FEATURE_PGO) && (defined(TARGET_BROWSER) || defined(TARGET_WASI)) - // Multithreaded WASM: interpreter block-count PGO has no synchronized flush path yet, so stopping - // a PGO trace here would silently drop the block-count events. Flag the unimplemented config. - PORTABILITY_ASSERT ("Interpreter block-count PGO flush is not implemented for multithreaded WASM (requires PERFTRACING_DISABLE_THREADS)."); + // Multithreaded WASM: interpreter block-count PGO has no synchronized flush path yet. This hook runs for + // every stopping session, so only trip when the stopping session actually enabled the PGO keyword (a real + // collection attempt on this unsupported config); unrelated sessions (CPU/GC/counters) are unaffected. + extern EventPipeEvent *EventPipeEventJitInstrumentationDataVerbose; + EventPipeSession *session = reinterpret_cast(static_cast(session_id)); + if (EventPipeEventJitInstrumentationDataVerbose != NULL && + ep_event_is_enabled_by_mask (EventPipeEventJitInstrumentationDataVerbose, ep_session_get_mask (session))) + { + PORTABILITY_ASSERT ("Interpreter block-count PGO flush is not implemented for multithreaded WASM (requires PERFTRACING_DISABLE_THREADS)."); + } (void)session_id; #else (void)session_id; diff --git a/src/coreclr/vm/interpexec.cpp b/src/coreclr/vm/interpexec.cpp index 9ee39772a594bb..971031d1508363 100644 --- a/src/coreclr/vm/interpexec.cpp +++ b/src/coreclr/vm/interpexec.cpp @@ -2066,13 +2066,13 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr INTOP_NEXT; #endif // TARGET_BROWSER && PERFTRACING_DISABLE_THREADS -#if defined(TARGET_BROWSER) || defined(TARGET_WASI) +#if (defined(TARGET_BROWSER) || defined(TARGET_WASI)) && defined(PERFTRACING_DISABLE_THREADS) INTOP_CASE(INTOP_PGO_COUNT) // Increment the block-count PGO counter whose address is stored as a data item. (*(uint32_t*)pMethod->pDataItems[ip[1]])++; ip += 2; INTOP_NEXT; -#endif // TARGET_BROWSER || TARGET_WASI +#endif // (TARGET_BROWSER || TARGET_WASI) && PERFTRACING_DISABLE_THREADS INTOP_CASE(INTOP_BR) ip += ip[1]; From c79ea57e3d1e080d06667ea008a62a2704883334 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 16 Sep 2026 15:32:52 +0200 Subject: [PATCH 20/29] Pass session mask to session-stopping hook; add pgo-trace to rollup sources Capture the stopping session's keyword mask under the EventPipe lock in stop_session and pass it to ep_rt_session_stopping, so ep_rt_coreclr_session_stopping tests the JitInstrumentationData keyword without dereferencing a session pointer a concurrent stop may have freed. The single-threaded flush still uses the session pointer for the rundown-thread marker, which is safe under PERFTRACING_DISABLE_THREADS. Add dotnet-pgo-trace.ts to ROLLUP_TS_SOURCES so incremental browser-bundle builds rerun Rollup when it changes. --- .../nativeaot/Runtime/eventpipe/ep-rt-aot.h | 3 ++- .../vm/eventing/eventpipe/ep-rt-coreclr.cpp | 19 +++++++++++-------- .../vm/eventing/eventpipe/ep-rt-coreclr.h | 6 +++--- src/mono/mono/eventpipe/ep-rt-mono.h | 3 ++- src/native/eventpipe/ep-rt.h | 10 ++++++---- src/native/eventpipe/ep.c | 5 ++++- .../libs/Common/JavaScript/CMakeLists.txt | 1 + 7 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h b/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h index 39c8fff8a17f27..cf1054b5f516c3 100644 --- a/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h +++ b/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h @@ -597,10 +597,11 @@ ep_rt_notify_profiler_provider_created (EventPipeProvider *provider) static inline void -ep_rt_session_stopping (EventPipeSessionID session_id) +ep_rt_session_stopping (EventPipeSessionID session_id, uint64_t session_mask) { // Following mono's path of no-op (void)session_id; + (void)session_mask; } /* diff --git a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp index 545896afbfb565..ec16804094ede6 100644 --- a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp +++ b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp @@ -175,21 +175,23 @@ ep_rt_coreclr_sample_profiler_write_sampling_event_for_threads ( } void -ep_rt_coreclr_session_stopping (EventPipeSessionID session_id) +ep_rt_coreclr_session_stopping (EventPipeSessionID session_id, uint64_t session_mask) { STATIC_CONTRACT_NOTHROW; #if defined(FEATURE_PGO) && defined(PERFTRACING_DISABLE_THREADS) // Flush block-count PGO only into the session that enabled the JitInstrumentationData events, so an // unrelated session's stop can't re-emit chunks into an open PGO session (dotnet-pgo drops a method - // once data arrives after its final chunk). + // once data arrives after its final chunk). session_mask was captured under the EventPipe lock. extern EventPipeEvent *EventPipeEventJitInstrumentationDataVerbose; - EventPipeSession *session = reinterpret_cast(static_cast(session_id)); if (EventPipeEventJitInstrumentationDataVerbose != NULL && - ep_event_is_enabled_by_mask (EventPipeEventJitInstrumentationDataVerbose, ep_session_get_mask (session))) + ep_event_is_enabled_by_mask (EventPipeEventJitInstrumentationDataVerbose, session_mask)) { // Mark this thread as a rundown thread bound to the stopping session so the events emitted by the // flush are routed to that single session (ep_session_write_event) instead of broadcast to every - // enabled session; the marker is cleared after the flush, including on exception. + // enabled session; the marker is cleared after the flush, including on exception. Dereferencing the + // session is safe here: this path is single-threaded (PERFTRACING_DISABLE_THREADS), so nothing frees + // it before section2 disables it. + EventPipeSession *session = reinterpret_cast(static_cast(session_id)); EventPipeThread *thread = ep_thread_get_or_create (); if (thread != NULL) { @@ -206,17 +208,18 @@ ep_rt_coreclr_session_stopping (EventPipeSessionID session_id) #elif defined(FEATURE_PGO) && (defined(TARGET_BROWSER) || defined(TARGET_WASI)) // Multithreaded WASM: interpreter block-count PGO has no synchronized flush path yet. This hook runs for // every stopping session, so only trip when the stopping session actually enabled the PGO keyword (a real - // collection attempt on this unsupported config); unrelated sessions (CPU/GC/counters) are unaffected. + // collection attempt on this unsupported config); unrelated sessions (CPU/GC/counters) are unaffected. Use + // the lock-captured session_mask so we never dereference a session that a concurrent stop may have freed. extern EventPipeEvent *EventPipeEventJitInstrumentationDataVerbose; - EventPipeSession *session = reinterpret_cast(static_cast(session_id)); if (EventPipeEventJitInstrumentationDataVerbose != NULL && - ep_event_is_enabled_by_mask (EventPipeEventJitInstrumentationDataVerbose, ep_session_get_mask (session))) + ep_event_is_enabled_by_mask (EventPipeEventJitInstrumentationDataVerbose, session_mask)) { PORTABILITY_ASSERT ("Interpreter block-count PGO flush is not implemented for multithreaded WASM (requires PERFTRACING_DISABLE_THREADS)."); } (void)session_id; #else (void)session_id; + (void)session_mask; #endif // FEATURE_PGO && PERFTRACING_DISABLE_THREADS } diff --git a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h index baaedc404cddf8..c40d70e7a9ee4e 100644 --- a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h +++ b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h @@ -662,11 +662,11 @@ ep_rt_notify_profiler_provider_created (EventPipeProvider *provider) static inline void -ep_rt_session_stopping (EventPipeSessionID session_id) +ep_rt_session_stopping (EventPipeSessionID session_id, uint64_t session_mask) { STATIC_CONTRACT_NOTHROW; - extern void ep_rt_coreclr_session_stopping (EventPipeSessionID session_id); - ep_rt_coreclr_session_stopping (session_id); + extern void ep_rt_coreclr_session_stopping (EventPipeSessionID session_id, uint64_t session_mask); + ep_rt_coreclr_session_stopping (session_id, session_mask); } /* diff --git a/src/mono/mono/eventpipe/ep-rt-mono.h b/src/mono/mono/eventpipe/ep-rt-mono.h index 621ad0bb8b07d8..b51f8e3bb4b1e7 100644 --- a/src/mono/mono/eventpipe/ep-rt-mono.h +++ b/src/mono/mono/eventpipe/ep-rt-mono.h @@ -716,9 +716,10 @@ ep_rt_notify_profiler_provider_created (EventPipeProvider *provider) static inline void -ep_rt_session_stopping (EventPipeSessionID session_id) +ep_rt_session_stopping (EventPipeSessionID session_id, uint64_t session_mask) { (void)session_id; + (void)session_mask; } /* diff --git a/src/native/eventpipe/ep-rt.h b/src/native/eventpipe/ep-rt.h index d52bddb50301ad..b81e38b6f83994 100644 --- a/src/native/eventpipe/ep-rt.h +++ b/src/native/eventpipe/ep-rt.h @@ -240,12 +240,14 @@ ep_rt_notify_profiler_provider_created (EventPipeProvider *provider); // Invoked while a session is stopping, before its providers are disabled and its buffers are // flushed, so the runtime can emit any pending end-of-session data (e.g. block-count PGO) into the -// still-open session identified by session_id (already validated as live by the caller). Runs on the -// disabling thread before the EventPipe lock is taken, because emitting events re-enters the write -// path and must not run with that lock held. +// still-open session identified by session_id (already validated as live by the caller). session_mask +// is the session's keyword mask captured under the EventPipe lock, so the runtime can test provider +// keywords without dereferencing the session, which a concurrent stop may free once the lock is +// released. Runs on the disabling thread before the EventPipe lock is taken, because emitting events +// re-enters the write path and must not run with that lock held. static void -ep_rt_session_stopping (EventPipeSessionID session_id); +ep_rt_session_stopping (EventPipeSessionID session_id, uint64_t session_mask); /* * Arrays. diff --git a/src/native/eventpipe/ep.c b/src/native/eventpipe/ep.c index 20d63fef9b4589..8e2120b065dfbc 100644 --- a/src/native/eventpipe/ep.c +++ b/src/native/eventpipe/ep.c @@ -808,12 +808,15 @@ stop_session (EventPipeSessionID id) // must run before the disable lock: emitting events re-enters the write path, which requires the // lock not be held. bool is_active_session = false; + uint64_t session_mask = 0; EP_LOCK_ENTER (section1) is_active_session = is_session_id_in_collection (id); + if (is_active_session) + session_mask = ep_session_get_mask ((EventPipeSession *)(uintptr_t)id); EP_LOCK_EXIT (section1) if (is_active_session) - ep_rt_session_stopping (id); + ep_rt_session_stopping (id, session_mask); EP_LOCK_ENTER (section2) if (is_session_id_in_collection (id)) diff --git a/src/native/libs/Common/JavaScript/CMakeLists.txt b/src/native/libs/Common/JavaScript/CMakeLists.txt index a569cd95dfd5fc..eafcfbb4ee38af 100644 --- a/src/native/libs/Common/JavaScript/CMakeLists.txt +++ b/src/native/libs/Common/JavaScript/CMakeLists.txt @@ -68,6 +68,7 @@ set(ROLLUP_TS_SOURCES "${CLR_SRC_NATIVE_DIR}/libs/System.Native.Browser/diagnostics/dotnet-counters.ts" "${CLR_SRC_NATIVE_DIR}/libs/System.Native.Browser/diagnostics/dotnet-cpu-profiler.ts" "${CLR_SRC_NATIVE_DIR}/libs/System.Native.Browser/diagnostics/dotnet-gcdump.ts" + "${CLR_SRC_NATIVE_DIR}/libs/System.Native.Browser/diagnostics/dotnet-pgo-trace.ts" "${CLR_SRC_NATIVE_DIR}/libs/System.Native.Browser/diagnostics/exit.ts" "${CLR_SRC_NATIVE_DIR}/libs/System.Native.Browser/diagnostics/index.ts" "${CLR_SRC_NATIVE_DIR}/libs/System.Native.Browser/diagnostics/per-module.ts" From 0426f3d847de0e727ca96b7e32501c606b177f27 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 16 Sep 2026 16:24:15 +0200 Subject: [PATCH 21/29] Drop 'desktop' from PGO keyword comment Per review feedback, avoid the ambiguous term 'desktop' (glossary-synonymous with .NET Framework) in the JitInstrumentationData keyword comment. --- .../libs/System.Native.Browser/diagnostics/client-commands.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/native/libs/System.Native.Browser/diagnostics/client-commands.ts b/src/native/libs/System.Native.Browser/diagnostics/client-commands.ts index 361336b6934bdb..2603ef27449f1d 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/client-commands.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/client-commands.ts @@ -123,8 +123,7 @@ export function commandPgoTrace(options: DiagnosticCommandOptions) { requestRundown: true, providers: [ { - // Microsoft-Windows-DotNETRuntime keyword 0x1F000080018, level 5 (Verbose) - matches the - // desktop dotnet-optimization IBC collection so dotnet-pgo sees the same event set: + // Microsoft-Windows-DotNETRuntime keyword 0x1F000080018, level 5 (Verbose): // 0x8 Loader (ModuleLoadUnload - module identity) // 0x10 Jit (MethodJittingStarted/MethodLoadVerbose - the method list) // 0x80000 Type (GCBulkType - type identity for generics) From 97276405b469d941f8cfa9b3507ac09f6e37d775 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 17 Sep 2026 12:51:01 +0200 Subject: [PATCH 22/29] Split EventPipe PGO emission from text-file export Extract EmitInstrumentationDataToEventPipe from WritePgoData so FlushInstrumentationData (the on-demand session-stop flush) emits only to EventPipe and never triggers the DOTNET_WritePGOData text-file dump. Guard the shutdown WritePgoData EventPipe emission with !PERFTRACING_DISABLE_THREADS so single-threaded WASM does not emit each method a second time into a session EventPipe stops during shutdown (which dotnet-pgo rejects as a duplicate chunk); threaded desktop still emits at shutdown as before. --- src/coreclr/vm/pgo.cpp | 17 +++++++++++++++-- src/coreclr/vm/pgo.h | 1 + 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/coreclr/vm/pgo.cpp b/src/coreclr/vm/pgo.cpp index 65872e89206585..c0aa78aa89692e 100644 --- a/src/coreclr/vm/pgo.cpp +++ b/src/coreclr/vm/pgo.cpp @@ -80,7 +80,9 @@ void PgoManager::Shutdown() void PgoManager::FlushInstrumentationData() { WRAPPER_NO_CONTRACT; - WritePgoData(); + // Only emit to EventPipe here. The text-file export (DOTNET_WritePGOData) is a separate concern owned + // by the process-shutdown path, not by on-demand trace collection. + EmitInstrumentationDataToEventPipe(); } void PgoManager::VerifyAddress(void* address) @@ -167,7 +169,7 @@ void CallFClose(FILE* file) typedef Holder FILEHolder; -void PgoManager::WritePgoData() +void PgoManager::EmitInstrumentationDataToEventPipe() { if (ETW_EVENT_ENABLED(MICROSOFT_WINDOWS_DOTNETRUNTIME_PROVIDER_DOTNET_Context, JitInstrumentationDataVerbose)) { @@ -188,6 +190,17 @@ void PgoManager::WritePgoData() return true; }); } +} + +void PgoManager::WritePgoData() +{ +#ifndef PERFTRACING_DISABLE_THREADS + // On single-threaded WASM the session-stopping flush (FlushInstrumentationData) is the EventPipe + // emitter, so emitting here too would deliver each method twice to a session that EventPipe stops + // during shutdown, which dotnet-pgo rejects as a duplicate chunk after a method's final chunk. + // Threaded builds have no such flush, so the shutdown path emits the EventPipe data. + EmitInstrumentationDataToEventPipe(); +#endif if (CLRConfig::GetConfigValue(CLRConfig::INTERNAL_WritePGOData) == 0) { diff --git a/src/coreclr/vm/pgo.h b/src/coreclr/vm/pgo.h index 451486100df96b..c43241567740d6 100644 --- a/src/coreclr/vm/pgo.h +++ b/src/coreclr/vm/pgo.h @@ -164,6 +164,7 @@ class PgoManager static void ReadPgoData(); static void WritePgoData(); + static void EmitInstrumentationDataToEventPipe(); private: From 6055010246f117b85049598193797c49e2b02450 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 17 Sep 2026 13:48:09 +0200 Subject: [PATCH 23/29] Make browser PGO trace collection one-shot per process collectPgoTrace now rejects a second collection after the first has run and flushed, since the interpreter block-count counters are cumulative and re-emitting them would produce a duplicate chunk sequence that dotnet-pgo drops. The latch is set only once a session actually started, so a setup that fails before starting still allows a retry. --- .../diagnostics/dotnet-pgo-trace.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/native/libs/System.Native.Browser/diagnostics/dotnet-pgo-trace.ts b/src/native/libs/System.Native.Browser/diagnostics/dotnet-pgo-trace.ts index 4d7a96b0cfdbc9..c6b4e04a266721 100644 --- a/src/native/libs/System.Native.Browser/diagnostics/dotnet-pgo-trace.ts +++ b/src/native/libs/System.Native.Browser/diagnostics/dotnet-pgo-trace.ts @@ -14,6 +14,10 @@ const DEFAULT_PGO_DURATION_SECONDS = 10; // the session currently collecting a PGO trace; stopped internally when the duration elapses let pgoSession: IDiagnosticSession | undefined = undefined; +// one-shot latch: the interpreter's block-count counters are cumulative and flushed once per collection, +// so a second collection would re-emit data dotnet-pgo rejects as duplicate chunks; allow one per process. +let pgoCollected = false; + export function collectPgoTrace(options?: DiagnosticCommandOptions, startup?: boolean): Promise { if (!options) options = {}; if (!startup && !serverSession) { @@ -22,6 +26,9 @@ export function collectPgoTrace(options?: DiagnosticCommandOptions, startup?: bo if (pgoSession) { throw new Error("A PGO trace collection is already in progress"); } + if (pgoCollected) { + throw new Error("PGO trace collection is one-shot per process; the counters were already flushed. Restart the app to collect again."); + } const durationSeconds = options.durationSeconds ?? DEFAULT_PGO_DURATION_SECONDS; @@ -48,6 +55,10 @@ export function collectPgoTrace(options?: DiagnosticCommandOptions, startup?: bo if (pgoSession === startedSession) { pgoSession = undefined; } + // latch one-shot once a collection actually ran, and therefore flushed the counters + if (startedSession !== undefined) { + pgoCollected = true; + } if (stopTimeoutId !== undefined) { globalThis.clearTimeout(stopTimeoutId); stopTimeoutId = undefined; From dd1081a438599f286b4d4553b40ce395d0b87ced Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Thu, 17 Sep 2026 14:51:15 +0200 Subject: [PATCH 24/29] cleanup --- src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp | 10 ++-------- src/coreclr/vm/pgo.cpp | 12 ------------ src/coreclr/vm/pgo.h | 3 +-- 3 files changed, 3 insertions(+), 22 deletions(-) diff --git a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp index ec16804094ede6..75d0300235dcca 100644 --- a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp +++ b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp @@ -179,9 +179,7 @@ ep_rt_coreclr_session_stopping (EventPipeSessionID session_id, uint64_t session_ { STATIC_CONTRACT_NOTHROW; #if defined(FEATURE_PGO) && defined(PERFTRACING_DISABLE_THREADS) - // Flush block-count PGO only into the session that enabled the JitInstrumentationData events, so an - // unrelated session's stop can't re-emit chunks into an open PGO session (dotnet-pgo drops a method - // once data arrives after its final chunk). session_mask was captured under the EventPipe lock. + // Flush block-count PGO only into the session that enabled the JitInstrumentationData events extern EventPipeEvent *EventPipeEventJitInstrumentationDataVerbose; if (EventPipeEventJitInstrumentationDataVerbose != NULL && ep_event_is_enabled_by_mask (EventPipeEventJitInstrumentationDataVerbose, session_mask)) @@ -198,7 +196,7 @@ ep_rt_coreclr_session_stopping (EventPipeSessionID session_id, uint64_t session_ ep_thread_set_as_rundown_thread (thread, session); EX_TRY { - PgoManager::FlushInstrumentationData (); + PgoManager::EmitInstrumentationDataToEventPipe (); } EX_CATCH { } EX_END_CATCH @@ -206,10 +204,6 @@ ep_rt_coreclr_session_stopping (EventPipeSessionID session_id, uint64_t session_ } } #elif defined(FEATURE_PGO) && (defined(TARGET_BROWSER) || defined(TARGET_WASI)) - // Multithreaded WASM: interpreter block-count PGO has no synchronized flush path yet. This hook runs for - // every stopping session, so only trip when the stopping session actually enabled the PGO keyword (a real - // collection attempt on this unsupported config); unrelated sessions (CPU/GC/counters) are unaffected. Use - // the lock-captured session_mask so we never dereference a session that a concurrent stop may have freed. extern EventPipeEvent *EventPipeEventJitInstrumentationDataVerbose; if (EventPipeEventJitInstrumentationDataVerbose != NULL && ep_event_is_enabled_by_mask (EventPipeEventJitInstrumentationDataVerbose, session_mask)) diff --git a/src/coreclr/vm/pgo.cpp b/src/coreclr/vm/pgo.cpp index c0aa78aa89692e..a255a648a371ba 100644 --- a/src/coreclr/vm/pgo.cpp +++ b/src/coreclr/vm/pgo.cpp @@ -77,14 +77,6 @@ void PgoManager::Shutdown() } } -void PgoManager::FlushInstrumentationData() -{ - WRAPPER_NO_CONTRACT; - // Only emit to EventPipe here. The text-file export (DOTNET_WritePGOData) is a separate concern owned - // by the process-shutdown path, not by on-demand trace collection. - EmitInstrumentationDataToEventPipe(); -} - void PgoManager::VerifyAddress(void* address) { // TODO Insert an assert to check that an address is a valid pgo address @@ -195,10 +187,6 @@ void PgoManager::EmitInstrumentationDataToEventPipe() void PgoManager::WritePgoData() { #ifndef PERFTRACING_DISABLE_THREADS - // On single-threaded WASM the session-stopping flush (FlushInstrumentationData) is the EventPipe - // emitter, so emitting here too would deliver each method twice to a session that EventPipe stops - // during shutdown, which dotnet-pgo rejects as a duplicate chunk after a method's final chunk. - // Threaded builds have no such flush, so the shutdown path emits the EventPipe data. EmitInstrumentationDataToEventPipe(); #endif diff --git a/src/coreclr/vm/pgo.h b/src/coreclr/vm/pgo.h index c43241567740d6..8593a0ed980188 100644 --- a/src/coreclr/vm/pgo.h +++ b/src/coreclr/vm/pgo.h @@ -17,7 +17,7 @@ class PgoManager public: static void Initialize(); - static void FlushInstrumentationData(); + static void EmitInstrumentationDataToEventPipe(); static void Shutdown(); #endif // FEATURE_PGO @@ -164,7 +164,6 @@ class PgoManager static void ReadPgoData(); static void WritePgoData(); - static void EmitInstrumentationDataToEventPipe(); private: From 4dae43335e7c23be3ea0c0ee8fd7616bf261a09f Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 21 Sep 2026 13:51:24 +0200 Subject: [PATCH 25/29] Move PGO flush rundown-binding into EventPipe session_stopping helper Per EventPipe-owner feedback: bind the stopping session as the current thread's rundown session inside a new single-threaded-only session_stopping helper in ep.c (save/restore the previous binding under the config lock), and add a shared ep_event_is_enabled_for_current_thread helper. The CoreCLR session-stopping hook now just gates on that helper and emits, with session routing and validation owned by EventPipe; the hook reverts to a single session_id parameter. Rename PgoManager::EmitInstrumentationDataToEventPipe to LogInstrumentationData to match LogMethodInstrumentationData. --- .../nativeaot/Runtime/eventpipe/ep-rt-aot.h | 3 +- .../vm/eventing/eventpipe/ep-rt-coreclr.cpp | 38 +++------ .../vm/eventing/eventpipe/ep-rt-coreclr.h | 6 +- src/coreclr/vm/pgo.cpp | 4 +- src/coreclr/vm/pgo.h | 2 +- src/mono/mono/eventpipe/ep-rt-mono.h | 3 +- src/native/eventpipe/ep-rt.h | 15 ++-- src/native/eventpipe/ep.c | 85 +++++++++++++++---- src/native/eventpipe/ep.h | 6 ++ 9 files changed, 101 insertions(+), 61 deletions(-) diff --git a/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h b/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h index cf1054b5f516c3..39c8fff8a17f27 100644 --- a/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h +++ b/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h @@ -597,11 +597,10 @@ ep_rt_notify_profiler_provider_created (EventPipeProvider *provider) static inline void -ep_rt_session_stopping (EventPipeSessionID session_id, uint64_t session_mask) +ep_rt_session_stopping (EventPipeSessionID session_id) { // Following mono's path of no-op (void)session_id; - (void)session_mask; } /* diff --git a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp index 75d0300235dcca..e168686e8fb96a 100644 --- a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp +++ b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp @@ -175,45 +175,27 @@ ep_rt_coreclr_sample_profiler_write_sampling_event_for_threads ( } void -ep_rt_coreclr_session_stopping (EventPipeSessionID session_id, uint64_t session_mask) +ep_rt_coreclr_session_stopping (EventPipeSessionID session_id) { STATIC_CONTRACT_NOTHROW; #if defined(FEATURE_PGO) && defined(PERFTRACING_DISABLE_THREADS) - // Flush block-count PGO only into the session that enabled the JitInstrumentationData events + // The EventPipe session_stopping helper has bound this thread to the stopping session as its rundown + // session, so ep_event_is_enabled_for_current_thread tests that session's mask and the events emitted by + // the flush route only to it (dotnet-pgo drops a method once data arrives after its final chunk, so a + // single destination is required). extern EventPipeEvent *EventPipeEventJitInstrumentationDataVerbose; if (EventPipeEventJitInstrumentationDataVerbose != NULL && - ep_event_is_enabled_by_mask (EventPipeEventJitInstrumentationDataVerbose, session_mask)) + ep_event_is_enabled_for_current_thread (EventPipeEventJitInstrumentationDataVerbose)) { - // Mark this thread as a rundown thread bound to the stopping session so the events emitted by the - // flush are routed to that single session (ep_session_write_event) instead of broadcast to every - // enabled session; the marker is cleared after the flush, including on exception. Dereferencing the - // session is safe here: this path is single-threaded (PERFTRACING_DISABLE_THREADS), so nothing frees - // it before section2 disables it. - EventPipeSession *session = reinterpret_cast(static_cast(session_id)); - EventPipeThread *thread = ep_thread_get_or_create (); - if (thread != NULL) + EX_TRY { - ep_thread_set_as_rundown_thread (thread, session); - EX_TRY - { - PgoManager::EmitInstrumentationDataToEventPipe (); - } - EX_CATCH { } - EX_END_CATCH - ep_thread_set_as_rundown_thread (thread, NULL); + PgoManager::LogInstrumentationData (); } + EX_CATCH { } + EX_END_CATCH } -#elif defined(FEATURE_PGO) && (defined(TARGET_BROWSER) || defined(TARGET_WASI)) - extern EventPipeEvent *EventPipeEventJitInstrumentationDataVerbose; - if (EventPipeEventJitInstrumentationDataVerbose != NULL && - ep_event_is_enabled_by_mask (EventPipeEventJitInstrumentationDataVerbose, session_mask)) - { - PORTABILITY_ASSERT ("Interpreter block-count PGO flush is not implemented for multithreaded WASM (requires PERFTRACING_DISABLE_THREADS)."); - } - (void)session_id; #else (void)session_id; - (void)session_mask; #endif // FEATURE_PGO && PERFTRACING_DISABLE_THREADS } diff --git a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h index c40d70e7a9ee4e..baaedc404cddf8 100644 --- a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h +++ b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h @@ -662,11 +662,11 @@ ep_rt_notify_profiler_provider_created (EventPipeProvider *provider) static inline void -ep_rt_session_stopping (EventPipeSessionID session_id, uint64_t session_mask) +ep_rt_session_stopping (EventPipeSessionID session_id) { STATIC_CONTRACT_NOTHROW; - extern void ep_rt_coreclr_session_stopping (EventPipeSessionID session_id, uint64_t session_mask); - ep_rt_coreclr_session_stopping (session_id, session_mask); + extern void ep_rt_coreclr_session_stopping (EventPipeSessionID session_id); + ep_rt_coreclr_session_stopping (session_id); } /* diff --git a/src/coreclr/vm/pgo.cpp b/src/coreclr/vm/pgo.cpp index a255a648a371ba..05250ece8b99fd 100644 --- a/src/coreclr/vm/pgo.cpp +++ b/src/coreclr/vm/pgo.cpp @@ -161,7 +161,7 @@ void CallFClose(FILE* file) typedef Holder FILEHolder; -void PgoManager::EmitInstrumentationDataToEventPipe() +void PgoManager::LogInstrumentationData() { if (ETW_EVENT_ENABLED(MICROSOFT_WINDOWS_DOTNETRUNTIME_PROVIDER_DOTNET_Context, JitInstrumentationDataVerbose)) { @@ -187,7 +187,7 @@ void PgoManager::EmitInstrumentationDataToEventPipe() void PgoManager::WritePgoData() { #ifndef PERFTRACING_DISABLE_THREADS - EmitInstrumentationDataToEventPipe(); + LogInstrumentationData(); #endif if (CLRConfig::GetConfigValue(CLRConfig::INTERNAL_WritePGOData) == 0) diff --git a/src/coreclr/vm/pgo.h b/src/coreclr/vm/pgo.h index 8593a0ed980188..470804661226b2 100644 --- a/src/coreclr/vm/pgo.h +++ b/src/coreclr/vm/pgo.h @@ -17,7 +17,7 @@ class PgoManager public: static void Initialize(); - static void EmitInstrumentationDataToEventPipe(); + static void LogInstrumentationData(); static void Shutdown(); #endif // FEATURE_PGO diff --git a/src/mono/mono/eventpipe/ep-rt-mono.h b/src/mono/mono/eventpipe/ep-rt-mono.h index b51f8e3bb4b1e7..621ad0bb8b07d8 100644 --- a/src/mono/mono/eventpipe/ep-rt-mono.h +++ b/src/mono/mono/eventpipe/ep-rt-mono.h @@ -716,10 +716,9 @@ ep_rt_notify_profiler_provider_created (EventPipeProvider *provider) static inline void -ep_rt_session_stopping (EventPipeSessionID session_id, uint64_t session_mask) +ep_rt_session_stopping (EventPipeSessionID session_id) { (void)session_id; - (void)session_mask; } /* diff --git a/src/native/eventpipe/ep-rt.h b/src/native/eventpipe/ep-rt.h index b81e38b6f83994..10c849a7085c01 100644 --- a/src/native/eventpipe/ep-rt.h +++ b/src/native/eventpipe/ep-rt.h @@ -238,16 +238,15 @@ ep_rt_notify_profiler_provider_created (EventPipeProvider *provider); * Session lifecycle notifications. */ -// Invoked while a session is stopping, before its providers are disabled and its buffers are -// flushed, so the runtime can emit any pending end-of-session data (e.g. block-count PGO) into the -// still-open session identified by session_id (already validated as live by the caller). session_mask -// is the session's keyword mask captured under the EventPipe lock, so the runtime can test provider -// keywords without dereferencing the session, which a concurrent stop may free once the lock is -// released. Runs on the disabling thread before the EventPipe lock is taken, because emitting events -// re-enters the write path and must not run with that lock held. +// Invoked while a session is stopping, before its providers are disabled and its buffers are flushed, so the +// runtime can emit any pending end-of-session data (e.g. block-count PGO) into the still-open session +// identified by session_id. On single-threaded runtimes the caller binds the current thread to this session +// as its rundown session before invoking, so the runtime can gate on ep_event_is_enabled_for_current_thread +// and its emitted events route only to that session. Runs before the EventPipe lock is taken, because +// emitting events re-enters the write path and must not run with that lock held. static void -ep_rt_session_stopping (EventPipeSessionID session_id, uint64_t session_mask); +ep_rt_session_stopping (EventPipeSessionID session_id); /* * Arrays. diff --git a/src/native/eventpipe/ep.c b/src/native/eventpipe/ep.c index 8e2120b065dfbc..924f1a204588ea 100644 --- a/src/native/eventpipe/ep.c +++ b/src/native/eventpipe/ep.c @@ -779,6 +779,72 @@ disable_holding_lock ( return; } +bool +ep_event_is_enabled_for_current_thread (EventPipeEvent *ep_event) +{ + EP_ASSERT (ep_event != NULL); + + // A thread scoped to a single session (rundown, or an end-of-session flush) consults only that session's + // mask, so the caller emits exactly what routes to it; otherwise fall back to global enablement. + EventPipeThread *thread = ep_thread_get (); + if (thread != NULL) { + EventPipeSession *rundown_session = ep_thread_get_rundown_session (thread); + if (rundown_session != NULL) + return ep_event_is_enabled_by_mask (ep_event, ep_session_get_mask (rundown_session)); + } + + return ep_event_is_enabled (ep_event); +} + +#ifdef PERFTRACING_DISABLE_THREADS +// Give the runtime a chance to emit end-of-session data (e.g. block-count PGO) into the stopping session +// before it is disabled. Bind the current thread to that session as its rundown session so the runtime's +// emitted events route only to it (see ep_event_is_enabled_for_current_thread and the rundown path in +// write_event_2), invoke the hook, then restore the previous binding. Runs before the disable lock because +// emitting events re-enters the write path, which must not run with the lock held. +static +void +session_stopping (EventPipeSessionID id) +{ + EventPipeThread *thread = ep_thread_get_or_create (); + if (thread == NULL) + return; + + EventPipeSession *prev_rundown_session = NULL; + bool bound = false; + + EP_LOCK_ENTER (section1) + if (is_session_id_in_collection (id)) { + prev_rundown_session = ep_thread_get_rundown_session (thread); + ep_thread_set_rundown_session (thread, (EventPipeSession *)(uintptr_t)id); + bound = true; + } + EP_LOCK_EXIT (section1) + + if (bound) { + ep_rt_session_stopping (id); + + EP_LOCK_ENTER (section2) + ep_thread_set_rundown_session (thread, prev_rundown_session); + EP_LOCK_EXIT (section2) + } + +ep_on_exit: + return; + +ep_on_error: + ep_exit_error_handler (); +} +#else +static +inline +void +session_stopping (EventPipeSessionID id) +{ + (void)id; +} +#endif + // Disable driver, entered without the lock from ep_disable and the deferred-disable replay in ep_finish_init: // take the lock and run the teardown, then dispatch the balanced provider-disable callbacks outside the lock. // This is the disable-side counterpart to the enable driver, which does the same take-lock / enable / dispatch @@ -802,21 +868,10 @@ stop_session (EventPipeSessionID id) EventPipeProviderCallbackData provider_callback_data; EventPipeProviderCallbackDataQueue *provider_callback_data_queue = ep_provider_callback_data_queue_init (&callback_data_queue); - // Give the runtime a chance to emit any pending end-of-session data (e.g. block-count PGO) into - // the still-live session. Validate the session id first so a stale/invalid stop request doesn't - // flush into other open sessions, and pass it so the runtime can target only that session. This - // must run before the disable lock: emitting events re-enters the write path, which requires the - // lock not be held. - bool is_active_session = false; - uint64_t session_mask = 0; - EP_LOCK_ENTER (section1) - is_active_session = is_session_id_in_collection (id); - if (is_active_session) - session_mask = ep_session_get_mask ((EventPipeSession *)(uintptr_t)id); - EP_LOCK_EXIT (section1) - - if (is_active_session) - ep_rt_session_stopping (id, session_mask); + // Give the runtime a chance to emit end-of-session data (e.g. block-count PGO) into the still-live + // session before it is disabled; session_stopping binds this thread to that session so the emitted + // events route only to it, and is a no-op on multithreaded runtimes. + session_stopping (id); EP_LOCK_ENTER (section2) if (is_session_id_in_collection (id)) diff --git a/src/native/eventpipe/ep.h b/src/native/eventpipe/ep.h index d9b661be0d44b3..8c8eff8a9669c0 100644 --- a/src/native/eventpipe/ep.h +++ b/src/native/eventpipe/ep.h @@ -207,6 +207,12 @@ ep_get_session (EventPipeSessionID session_id); bool ep_is_session_enabled (EventPipeSessionID session_id); +// Returns whether ep_event would be written by the current thread. If the thread is scoped to a single +// session (e.g. during rundown or an end-of-session flush), only that session's mask is consulted, so the +// caller emits exactly what routes to that session; otherwise falls back to global enablement. +bool +ep_event_is_enabled_for_current_thread (EventPipeEvent *ep_event); + void ep_start_session (EventPipeSessionID session_id); From 7a648be1bdd21bf21fdd5bf8434595b19714a0cb Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 21 Sep 2026 15:14:38 +0200 Subject: [PATCH 26/29] Instrument interpreter PGO at method entry and loop heads only Per review feedback, probe the same points as the sampling profiler - method entry and targets of backward branches - instead of every branch/switch/leave target, using a bit set on the target basic block in EmitBranch. Counters remain exact (bumped every execution, not sampled), giving exact method invocation and loop trip counts. Acyclic branch structure is deliberately left unprofiled: mapping interpreter blocks onto the JIT's is approximate, and block-count schemas get no flow reconstruction in the consumer, so that precision is deferred to the planned R2R-side instrumentation. Removes the per-instruction branch-target scan. --- src/coreclr/interpreter/compiler.cpp | 40 ++++++++-------------------- src/coreclr/interpreter/compiler.h | 4 +++ 2 files changed, 15 insertions(+), 29 deletions(-) diff --git a/src/coreclr/interpreter/compiler.cpp b/src/coreclr/interpreter/compiler.cpp index defb115e4c9034..fea62e6fb46701 100644 --- a/src/coreclr/interpreter/compiler.cpp +++ b/src/coreclr/interpreter/compiler.cpp @@ -3016,6 +3016,9 @@ void InterpCompiler::EmitBranch(InterpOpcode opcode, int32_t ilOffset) if (pTargetBB == NULL) BADCODE("code jumps to invalid offset"); + if (ilOffset < 0) + pTargetBB->isBackwardBranchTarget = true; + EmitBranchToBB(opcode, pTargetBB); } @@ -8683,45 +8686,24 @@ void InterpCompiler::CreateSynchronizedRetValVar() } #if (defined(TARGET_BROWSER) || defined(TARGET_WASI)) && defined(PERFTRACING_DISABLE_THREADS) -// Instrument each basic block with a block-count PGO probe. The counters are allocated by +// Instrument the method entry and loop heads with block-count PGO probes. The counters are allocated by // allocPgoInstrumentationBySchema (native PgoManager memory), so they persist independently of // EventPipe session lifetime; the accumulated profile is flushed to the trace as // JitInstrumentationDataVerbose events, which dotnet-pgo consumes to build an .mibc. void InterpCompiler::InstrumentBlockCounts() { - // Mark blocks that are the target of a branch or switch (loop and branch heads). Together with - // the method entry, these are the block heads whose execution count can't be inferred from a - // single predecessor, so only they are worth a counter; the precompiler reconstructs the rest of - // the flow graph from them. This deliberately avoids the JIT's edge/spanning-tree scheme (#130517). - bool *isBranchTarget = getAllocator(IMK_BasicBlock).allocateZeroed(m_BBCount); - for (InterpBasicBlock *bb = m_pEntryBB; bb != NULL; bb = bb->pNextBB) - { - for (InterpInst *ins = bb->pFirstIns; ins != NULL; ins = ins->pNext) - { - if (ins->opcode == INTOP_SWITCH) - { - int32_t n = ins->data[0]; - for (int32_t i = 0; i < n; i++) - isBranchTarget[ins->info.ppTargetBBTable[i]->index] = true; - } - else if (InterpOpIsUncondBranch(ins->opcode) || InterpOpIsCondBranch(ins->opcode) || - ins->opcode == INTOP_LEAVE_CATCH || ins->opcode == INTOP_CALL_FINALLY) - { - isBranchTarget[ins->info.pTargetBB->index] = true; - } - } - } - - // Collect the canonical block for each real IL offset that is the method entry (IL offset 0) or - // a branch/loop target. Clones (funclet / leave-chain islands) and blocks removed by optimization - // are skipped, so every schema entry carries a unique IL offset, matching what - // getPgoInstrumentationResults and dotnet-pgo expect. + // Probe the same points as the sampling profiler - method entry and loop heads (targets of backward + // branches) - but with an exact counter rather than a sample. This yields exact method invocation and + // loop trip counts; acyclic branch structure is deliberately not profiled here, since mapping the + // interpreter's blocks onto the JIT's is approximate at best (#130517). + // Clones (funclet / leave-chain islands) and blocks removed by optimization are skipped, so every schema + // entry carries a unique IL offset, matching what getPgoInstrumentationResults and dotnet-pgo expect. TArray blocks(GetMemPoolAllocator(IMK_DataItem)); for (InterpBasicBlock *bb = m_pEntryBB; bb != NULL; bb = bb->pNextBB) { if (bb->ilOffset < 0 || bb->ilOffset >= m_ILCodeSizeFromILHeader || m_ppOffsetToBB[bb->ilOffset] != bb) continue; - if (bb->ilOffset == 0 || isBranchTarget[bb->index]) + if (bb->ilOffset == 0 || bb->isBackwardBranchTarget) blocks.Add(bb); } diff --git a/src/coreclr/interpreter/compiler.h b/src/coreclr/interpreter/compiler.h index 861d06caffc0f4..333fdb67556e66 100644 --- a/src/coreclr/interpreter/compiler.h +++ b/src/coreclr/interpreter/compiler.h @@ -355,6 +355,9 @@ struct InterpBasicBlock // Is a leave chain island basic block bool isLeaveChainIsland; + // True if this block is the target of a backward branch, i.e. a loop head. + bool isBackwardBranchTarget; + // If this basic block is a catch or filter funclet entry, this is the index of the variable // that holds the exception object. int clauseVarIndex; @@ -387,6 +390,7 @@ struct InterpBasicBlock isFilterOrCatchFuncletEntry = false; isFinallyCallIsland = false; isLeaveChainIsland = false; + isBackwardBranchTarget = false; clauseVarIndex = -1; overlappingEHClauseCount = 0; enclosingTryBlockCount = -1; From bd19e3b5247eb4787c49a65d3d9d8f47ac9a1490 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 21 Sep 2026 17:17:39 +0200 Subject: [PATCH 27/29] Drop session id from session-stopping hook; harden rundown restore The hook no longer takes a session id: the caller validates the session and binds the current thread to it, so the runtime identifies the target via ep_event_is_enabled_for_current_thread. Use the ep_thread_set_as_rundown_thread wrapper for both bind and restore, and unbind on the error path so a failed restore-lock acquisition cannot leave the thread scoped to a session. --- src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h | 3 +-- src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp | 4 +--- src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h | 6 +++--- src/mono/mono/eventpipe/ep-rt-mono.h | 3 +-- src/native/eventpipe/ep-rt.h | 12 ++++++------ src/native/eventpipe/ep.c | 10 +++++++--- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h b/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h index 39c8fff8a17f27..8ef4d373a628ec 100644 --- a/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h +++ b/src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h @@ -597,10 +597,9 @@ ep_rt_notify_profiler_provider_created (EventPipeProvider *provider) static inline void -ep_rt_session_stopping (EventPipeSessionID session_id) +ep_rt_session_stopping (void) { // Following mono's path of no-op - (void)session_id; } /* diff --git a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp index e168686e8fb96a..17859b17988bd4 100644 --- a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp +++ b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp @@ -175,7 +175,7 @@ ep_rt_coreclr_sample_profiler_write_sampling_event_for_threads ( } void -ep_rt_coreclr_session_stopping (EventPipeSessionID session_id) +ep_rt_coreclr_session_stopping (void) { STATIC_CONTRACT_NOTHROW; #if defined(FEATURE_PGO) && defined(PERFTRACING_DISABLE_THREADS) @@ -194,8 +194,6 @@ ep_rt_coreclr_session_stopping (EventPipeSessionID session_id) EX_CATCH { } EX_END_CATCH } -#else - (void)session_id; #endif // FEATURE_PGO && PERFTRACING_DISABLE_THREADS } diff --git a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h index baaedc404cddf8..d54e561cbf9871 100644 --- a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h +++ b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h @@ -662,11 +662,11 @@ ep_rt_notify_profiler_provider_created (EventPipeProvider *provider) static inline void -ep_rt_session_stopping (EventPipeSessionID session_id) +ep_rt_session_stopping (void) { STATIC_CONTRACT_NOTHROW; - extern void ep_rt_coreclr_session_stopping (EventPipeSessionID session_id); - ep_rt_coreclr_session_stopping (session_id); + extern void ep_rt_coreclr_session_stopping (void); + ep_rt_coreclr_session_stopping (); } /* diff --git a/src/mono/mono/eventpipe/ep-rt-mono.h b/src/mono/mono/eventpipe/ep-rt-mono.h index 621ad0bb8b07d8..15849ebca635b6 100644 --- a/src/mono/mono/eventpipe/ep-rt-mono.h +++ b/src/mono/mono/eventpipe/ep-rt-mono.h @@ -716,9 +716,8 @@ ep_rt_notify_profiler_provider_created (EventPipeProvider *provider) static inline void -ep_rt_session_stopping (EventPipeSessionID session_id) +ep_rt_session_stopping (void) { - (void)session_id; } /* diff --git a/src/native/eventpipe/ep-rt.h b/src/native/eventpipe/ep-rt.h index 10c849a7085c01..20f9729df329d6 100644 --- a/src/native/eventpipe/ep-rt.h +++ b/src/native/eventpipe/ep-rt.h @@ -239,14 +239,14 @@ ep_rt_notify_profiler_provider_created (EventPipeProvider *provider); */ // Invoked while a session is stopping, before its providers are disabled and its buffers are flushed, so the -// runtime can emit any pending end-of-session data (e.g. block-count PGO) into the still-open session -// identified by session_id. On single-threaded runtimes the caller binds the current thread to this session -// as its rundown session before invoking, so the runtime can gate on ep_event_is_enabled_for_current_thread -// and its emitted events route only to that session. Runs before the EventPipe lock is taken, because -// emitting events re-enters the write path and must not run with that lock held. +// runtime can emit any pending end-of-session data (e.g. block-count PGO) into the still-open session. The +// caller has already validated the session and bound the current thread to it as its rundown session, so the +// runtime gates on ep_event_is_enabled_for_current_thread and its emitted events route only to that session. +// Runs before the EventPipe lock is taken, because emitting events re-enters the write path and must not run +// with that lock held. static void -ep_rt_session_stopping (EventPipeSessionID session_id); +ep_rt_session_stopping (void); /* * Arrays. diff --git a/src/native/eventpipe/ep.c b/src/native/eventpipe/ep.c index 924f1a204588ea..b4e5483a62b8c6 100644 --- a/src/native/eventpipe/ep.c +++ b/src/native/eventpipe/ep.c @@ -816,16 +816,17 @@ session_stopping (EventPipeSessionID id) EP_LOCK_ENTER (section1) if (is_session_id_in_collection (id)) { prev_rundown_session = ep_thread_get_rundown_session (thread); - ep_thread_set_rundown_session (thread, (EventPipeSession *)(uintptr_t)id); + ep_thread_set_as_rundown_thread (thread, (EventPipeSession *)(uintptr_t)id); bound = true; } EP_LOCK_EXIT (section1) if (bound) { - ep_rt_session_stopping (id); + ep_rt_session_stopping (); EP_LOCK_ENTER (section2) - ep_thread_set_rundown_session (thread, prev_rundown_session); + ep_thread_set_as_rundown_thread (thread, prev_rundown_session); + bound = false; EP_LOCK_EXIT (section2) } @@ -833,6 +834,9 @@ session_stopping (EventPipeSessionID id) return; ep_on_error: + // The restore lock could not be acquired; unbind anyway rather than leave the thread scoped to a session. + if (bound) + ep_thread_set_as_rundown_thread (thread, prev_rundown_session); ep_exit_error_handler (); } #else From b0dfe43bd4729920e579446673be1868f3a349ba Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 21 Sep 2026 18:15:33 +0200 Subject: [PATCH 28/29] Mark backward switch and leave targets as loop heads The isBackwardBranchTarget bit was only set in EmitBranch, so loop heads reached via CEE_SWITCH (which links targets directly) or EmitLeave (which calls EmitBranchToBB directly) received no PGO probe and their execution counts were absent from the profile. Mark both. For leave, mark before the finally-call-island redirection so the bit lands on the real IL block rather than an island that shares its IL offset. --- src/coreclr/interpreter/compiler.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/coreclr/interpreter/compiler.cpp b/src/coreclr/interpreter/compiler.cpp index fea62e6fb46701..63fa9503420429 100644 --- a/src/coreclr/interpreter/compiler.cpp +++ b/src/coreclr/interpreter/compiler.cpp @@ -3181,6 +3181,11 @@ void InterpCompiler::EmitLeave(int32_t ilOffset, int32_t target) } InterpBasicBlock *pTargetBB = m_ppOffsetToBB[target]; + // Mark a backward leave target as a loop head here, while pTargetBB is still the real IL block: below it + // may be redirected to a finally call island, which shares its IL offset with another block. + if (target < ilOffset && pTargetBB != NULL) + pTargetBB->isBackwardBranchTarget = true; + m_pStackPointer = m_pStackBase; // The leave will jump: @@ -10311,6 +10316,10 @@ void InterpCompiler::GenerateCode(CORINFO_METHOD_INFO* methodInfo) uint32_t target = (uint32_t)(nextIp - m_pILCode + offset); InterpBasicBlock *targetBB = m_ppOffsetToBB[target]; assert(targetBB); + // Offsets are relative to the instruction after the switch, so a negative one is a + // backward branch, i.e. a loop head. + if (offset < 0) + targetBB->isBackwardBranchTarget = true; targetOffsets[i] = target; targetBBTable[i] = targetBB; m_ip += 4; From 3712dc246aa6f7b91f67d74bb498062abd5dbaf5 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 21 Sep 2026 18:40:00 +0200 Subject: [PATCH 29/29] Enable interpreter PGO instrumentation on all platforms Per review feedback, drop the browser/WASI single-threaded ifdefs around the interpreter block-count instrumentation so the feature can be exercised on desktop, where it is much easier to debug. This includes the VM-side InterpreterPgoInstrumentationEnabled gate, without which allocPgoInstrumentationBySchema returns E_NOTIMPL and no probes are emitted off-WASM. The feature stays opt-in behind DOTNET_InterpPGO, and the counter increment is now interlocked so concurrent executions of an instrumented method do not lose counts. The EventPipe session-stopping flush remains single-threaded-only; on other platforms the counters are collected through the existing DOTNET_WritePGOData text export at shutdown. --- src/coreclr/interpreter/compiler.cpp | 8 -------- src/coreclr/interpreter/compiler.h | 6 ------ src/coreclr/interpreter/eeinterp.cpp | 2 -- src/coreclr/interpreter/inc/intops.def | 2 -- src/coreclr/vm/interpexec.cpp | 6 ++---- src/coreclr/vm/jitinterface.cpp | 4 ---- 6 files changed, 2 insertions(+), 26 deletions(-) diff --git a/src/coreclr/interpreter/compiler.cpp b/src/coreclr/interpreter/compiler.cpp index 63fa9503420429..6c04e6ccd6e350 100644 --- a/src/coreclr/interpreter/compiler.cpp +++ b/src/coreclr/interpreter/compiler.cpp @@ -51,9 +51,7 @@ bool InterpCompiler::s_samplingProfilerEnabled = false; bool InterpCompiler::s_browserProfilerEnabled = false; #endif #endif // PERFTRACING_DISABLE_THREADS -#if (defined(TARGET_BROWSER) || defined(TARGET_WASI)) && defined(PERFTRACING_DISABLE_THREADS) bool InterpCompiler::s_interpPgoEnabled = false; -#endif // (TARGET_BROWSER || TARGET_WASI) && PERFTRACING_DISABLE_THREADS #if MEASURE_MEM_ALLOC #include @@ -2249,11 +2247,9 @@ InterpCompiler::InterpCompiler(COMP_HANDLE compHnd, #endif #endif // PERFTRACING_DISABLE_THREADS -#if (defined(TARGET_BROWSER) || defined(TARGET_WASI)) && defined(PERFTRACING_DISABLE_THREADS) m_emitInterpPGO = s_interpPgoEnabled && (InterpConfig.InterpPGOMethods().isEmpty() || InterpConfig.InterpPGOMethods().contains(compHnd, m_methodHnd, m_classHnd, &m_methodInfo->args)); -#endif // (TARGET_BROWSER || TARGET_WASI) && PERFTRACING_DISABLE_THREADS #ifdef DEBUG m_methodName = ::PrintMethodName(compHnd, m_classHnd, m_methodHnd, &m_methodInfo->args, @@ -2357,10 +2353,8 @@ bool InterpCompiler::CompileMethod() } #endif -#if (defined(TARGET_BROWSER) || defined(TARGET_WASI)) && defined(PERFTRACING_DISABLE_THREADS) if (m_emitInterpPGO) InstrumentBlockCounts(); -#endif // (TARGET_BROWSER || TARGET_WASI) && PERFTRACING_DISABLE_THREADS AllocOffsets(); PatchInitLocals(m_methodInfo); @@ -8690,7 +8684,6 @@ void InterpCompiler::CreateSynchronizedRetValVar() INTERP_DUMP("Created ret val var V%d\n", m_synchronizedOrAsyncRetValVarIndex); } -#if (defined(TARGET_BROWSER) || defined(TARGET_WASI)) && defined(PERFTRACING_DISABLE_THREADS) // Instrument the method entry and loop heads with block-count PGO probes. The counters are allocated by // allocPgoInstrumentationBySchema (native PgoManager memory), so they persist independently of // EventPipe session lifetime; the accumulated profile is flushed to the trace as @@ -8748,7 +8741,6 @@ void InterpCompiler::InstrumentBlockCounts() ins->data[0] = GetDataItemIndex((void*)pCounter); } } -#endif // (TARGET_BROWSER || TARGET_WASI) && PERFTRACING_DISABLE_THREADS void InterpCompiler::GenerateCode(CORINFO_METHOD_INFO* methodInfo) { diff --git a/src/coreclr/interpreter/compiler.h b/src/coreclr/interpreter/compiler.h index 333fdb67556e66..a84e4ebd774b39 100644 --- a/src/coreclr/interpreter/compiler.h +++ b/src/coreclr/interpreter/compiler.h @@ -645,9 +645,7 @@ class InterpCompiler #endif #endif // PERFTRACING_DISABLE_THREADS -#if (defined(TARGET_BROWSER) || defined(TARGET_WASI)) && defined(PERFTRACING_DISABLE_THREADS) bool m_emitInterpPGO; -#endif // (TARGET_BROWSER || TARGET_WASI) && PERFTRACING_DISABLE_THREADS void DeclarePointerIsClass(CORINFO_CLASS_HANDLE clsHnd) { @@ -776,9 +774,7 @@ class InterpCompiler void GenerateCode(CORINFO_METHOD_INFO* methodInfo); -#if (defined(TARGET_BROWSER) || defined(TARGET_WASI)) && defined(PERFTRACING_DISABLE_THREADS) void InstrumentBlockCounts(); -#endif // (TARGET_BROWSER || TARGET_WASI) && PERFTRACING_DISABLE_THREADS InterpBasicBlock* GenerateCodeForLeaveChainIslands(InterpBasicBlock *pNewBB, InterpBasicBlock *pPrevBB); void PatchInitLocals(CORINFO_METHOD_INFO* methodInfo); @@ -1162,9 +1158,7 @@ class InterpCompiler static bool s_browserProfilerEnabled; #endif #endif // PERFTRACING_DISABLE_THREADS -#if (defined(TARGET_BROWSER) || defined(TARGET_WASI)) && defined(PERFTRACING_DISABLE_THREADS) static bool s_interpPgoEnabled; -#endif // (TARGET_BROWSER || TARGET_WASI) && PERFTRACING_DISABLE_THREADS #if MEASURE_MEM_ALLOC // Memory statistics for profiling. diff --git a/src/coreclr/interpreter/eeinterp.cpp b/src/coreclr/interpreter/eeinterp.cpp index eeb9f6100d004c..111dca4b03dc89 100644 --- a/src/coreclr/interpreter/eeinterp.cpp +++ b/src/coreclr/interpreter/eeinterp.cpp @@ -43,9 +43,7 @@ extern "C" INTERP_API void jitStartup(ICorJitHost* jitHost) if (InterpConfig.InterpPGO() != 0) { -#if (defined(TARGET_BROWSER) || defined(TARGET_WASI)) && defined(PERFTRACING_DISABLE_THREADS) InterpCompiler::s_interpPgoEnabled = true; -#endif // (TARGET_BROWSER || TARGET_WASI) && PERFTRACING_DISABLE_THREADS } g_interpInitialized = true; diff --git a/src/coreclr/interpreter/inc/intops.def b/src/coreclr/interpreter/inc/intops.def index f6bf2f52bcf213..88001273a29194 100644 --- a/src/coreclr/interpreter/inc/intops.def +++ b/src/coreclr/interpreter/inc/intops.def @@ -90,9 +90,7 @@ OPDEF(INTOP_PROF_ENTER, "prof.enter", 2, 0, 0, InterpOpMethodHandle) OPDEF(INTOP_PROF_LEAVE, "prof.leave", 1, 0, 0, InterpOpNoArgs) #endif -#if (defined(TARGET_BROWSER) || defined(TARGET_WASI)) && defined(PERFTRACING_DISABLE_THREADS) OPDEF(INTOP_PGO_COUNT, "pgo.count", 2, 0, 0, InterpOpLdPtr) -#endif OPDEF(INTOP_BR, "br", 2, 0, 0, InterpOpBranch) diff --git a/src/coreclr/vm/interpexec.cpp b/src/coreclr/vm/interpexec.cpp index ed66238ccf00e0..9d8b8df4905bc5 100644 --- a/src/coreclr/vm/interpexec.cpp +++ b/src/coreclr/vm/interpexec.cpp @@ -2088,13 +2088,11 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr INTOP_NEXT; #endif // TARGET_BROWSER && PERFTRACING_DISABLE_THREADS -#if (defined(TARGET_BROWSER) || defined(TARGET_WASI)) && defined(PERFTRACING_DISABLE_THREADS) INTOP_CASE(INTOP_PGO_COUNT) - // Increment the block-count PGO counter whose address is stored as a data item. - (*(uint32_t*)pMethod->pDataItems[ip[1]])++; + // Interlocked so concurrent executions of an instrumented method don't lose counts. + InterlockedIncrement((LONG*)pMethod->pDataItems[ip[1]]); ip += 2; INTOP_NEXT; -#endif // (TARGET_BROWSER || TARGET_WASI) && PERFTRACING_DISABLE_THREADS INTOP_CASE(INTOP_BR) ip += ip[1]; diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index 3da90deba91fe3..ee33ce15f37600 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -13065,12 +13065,8 @@ CORJIT_FLAGS* CEECodeGenInfo::getJitFlagsInternal() #ifdef FEATURE_PGO static bool InterpreterPgoInstrumentationEnabled() { -#if defined(TARGET_BROWSER) || defined(TARGET_WASI) static ConfigDWORD s_interpPgo; return s_interpPgo.val(CLRConfig::INTERNAL_InterpPGO) != 0; -#else - return false; -#endif } #endif // FEATURE_PGO