Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
fe5bb85
wip
pavelsavara Aug 25, 2026
ab76425
Define ep_rt_session_stopping no-op for NativeAOT eventpipe
pavelsavara Aug 25, 2026
1d4dd47
Flush PGO data before taking the EventPipe lock on session stop
pavelsavara Aug 25, 2026
47d4eab
enable BlazorEventPipeTestWithCpuSamples
pavelsavara Aug 25, 2026
b7df191
WBT test for PGO trace
pavelsavara Aug 26, 2026
38ef900
feedback
pavelsavara Aug 26, 2026
0b4176a
fix event pipe early collection
pavelsavara Aug 26, 2026
62594a2
Fix WBT dotnet-pgo copy: pre-create nested output dirs (amd64/arm64)
pavelsavara Aug 26, 2026
9e738b6
fix WBT linux
pavelsavara Sep 9, 2026
3f302aa
Deploy dotnet-pgo into WBT Helix payload via CopyToOutputDirectory
pavelsavara Sep 10, 2026
6c453c9
Restore dotnet-pgo for WBT via Private=false ProjectReference
pavelsavara Sep 10, 2026
9c215f5
Fix dotnet-pgo payload glob dropping runtimeconfig.json
pavelsavara Sep 11, 2026
1abefcb
Address PR review feedback for interpreter PGO
pavelsavara Sep 11, 2026
a983705
Correct ep_rt_session_stopping lock-contract comment
pavelsavara Sep 11, 2026
194b6a8
Address follow-up PR review on interpreter PGO
pavelsavara Sep 11, 2026
b3ec395
Scope interpreter PGO flush to the stopping session
pavelsavara Sep 11, 2026
97d7a19
Scope PGO block instrumentation to header IL range; assert on MT WASM…
pavelsavara Sep 16, 2026
c9b1e4b
Route PGO flush to the stopping session via rundown thread
pavelsavara Sep 16, 2026
3f7c7cd
Scope interpreter PGO to single-threaded WASM; mask-gate MT assert
pavelsavara Sep 16, 2026
c79ea57
Pass session mask to session-stopping hook; add pgo-trace to rollup s…
pavelsavara Sep 16, 2026
0426f3d
Drop 'desktop' from PGO keyword comment
pavelsavara Sep 16, 2026
3c52a09
Merge branch 'main' into wasm_collect_PGO
pavelsavara Sep 16, 2026
9727640
Split EventPipe PGO emission from text-file export
pavelsavara Sep 17, 2026
6055010
Make browser PGO trace collection one-shot per process
pavelsavara Sep 17, 2026
a47e597
Merge branch 'main' into wasm_collect_PGO
pavelsavara Sep 17, 2026
dd1081a
cleanup
pavelsavara Sep 17, 2026
0a32546
Merge branch 'main' into wasm_collect_PGO
pavelsavara Sep 21, 2026
4dae433
Move PGO flush rundown-binding into EventPipe session_stopping helper
pavelsavara Sep 21, 2026
7a648be
Instrument interpreter PGO at method entry and loop heads only
pavelsavara Sep 21, 2026
bd19e3b
Drop session id from session-stopping hook; harden rundown restore
pavelsavara Sep 21, 2026
b0dfe43
Mark backward switch and leave targets as loop heads
pavelsavara Sep 21, 2026
3712dc2
Enable interpreter PGO instrumentation on all platforms
pavelsavara Sep 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/coreclr/clrfeatures.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions src/coreclr/inc/clrconfigvalues.h
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,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:
Expand Down
78 changes: 78 additions & 0 deletions src/coreclr/interpreter/compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ bool InterpCompiler::s_samplingProfilerEnabled = false;
bool InterpCompiler::s_browserProfilerEnabled = false;
#endif
#endif // PERFTRACING_DISABLE_THREADS
bool InterpCompiler::s_interpPgoEnabled = false;

#if MEASURE_MEM_ALLOC
#include <minipal/mutex.h>
Expand Down Expand Up @@ -2246,6 +2247,10 @@ InterpCompiler::InterpCompiler(COMP_HANDLE compHnd,
#endif
#endif // PERFTRACING_DISABLE_THREADS

m_emitInterpPGO = s_interpPgoEnabled
&& (InterpConfig.InterpPGOMethods().isEmpty()
|| InterpConfig.InterpPGOMethods().contains(compHnd, m_methodHnd, m_classHnd, &m_methodInfo->args));

#ifdef DEBUG
m_methodName = ::PrintMethodName(compHnd, m_classHnd, m_methodHnd, &m_methodInfo->args,
/* includeAssembly */ false,
Expand Down Expand Up @@ -2348,6 +2353,9 @@ bool InterpCompiler::CompileMethod()
}
#endif

if (m_emitInterpPGO)
InstrumentBlockCounts();

AllocOffsets();
PatchInitLocals(m_methodInfo);

Expand Down Expand Up @@ -3002,6 +3010,9 @@ void InterpCompiler::EmitBranch(InterpOpcode opcode, int32_t ilOffset)
if (pTargetBB == NULL)
BADCODE("code jumps to invalid offset");

if (ilOffset < 0)
pTargetBB->isBackwardBranchTarget = true;
Comment thread
pavelsavara marked this conversation as resolved.

EmitBranchToBB(opcode, pTargetBB);
}

Expand Down Expand Up @@ -3164,6 +3175,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:
Expand Down Expand Up @@ -8668,6 +8684,64 @@ void InterpCompiler::CreateSynchronizedRetValVar()
INTERP_DUMP("Created ret val var V%d\n", m_synchronizedOrAsyncRetValVarIndex);
}

// 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()
{
Comment thread
pavelsavara marked this conversation as resolved.
// 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<InterpBasicBlock*, MemPoolAllocator> 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 || bb->isBackwardBranchTarget)
blocks.Add(bb);
}

int32_t numBlocks = blocks.GetSize();
if (numBlocks == 0)
return;

// One 4-byte block counter per block.
TArray<ICorJitInfo::PgoInstrumentationSchema, MemPoolAllocator> 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++)
{
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;
ins->data[0] = GetDataItemIndex((void*)pCounter);
Comment thread
pavelsavara marked this conversation as resolved.
}
}

void InterpCompiler::GenerateCode(CORINFO_METHOD_INFO* methodInfo)
{
bool readonly = false;
Expand Down Expand Up @@ -10234,6 +10308,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;
Expand Down
9 changes: 9 additions & 0 deletions src/coreclr/interpreter/compiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -387,6 +390,7 @@ struct InterpBasicBlock
isFilterOrCatchFuncletEntry = false;
isFinallyCallIsland = false;
isLeaveChainIsland = false;
isBackwardBranchTarget = false;
clauseVarIndex = -1;
overlappingEHClauseCount = 0;
enclosingTryBlockCount = -1;
Expand Down Expand Up @@ -641,6 +645,8 @@ class InterpCompiler
#endif
#endif // PERFTRACING_DISABLE_THREADS

bool m_emitInterpPGO;

void DeclarePointerIsClass(CORINFO_CLASS_HANDLE clsHnd)
{
#ifdef DEBUG
Expand Down Expand Up @@ -767,6 +773,8 @@ class InterpCompiler
void CreateSynchronizedRetValVar();

void GenerateCode(CORINFO_METHOD_INFO* methodInfo);

void InstrumentBlockCounts();
InterpBasicBlock* GenerateCodeForLeaveChainIslands(InterpBasicBlock *pNewBB, InterpBasicBlock *pPrevBB);
void PatchInitLocals(CORINFO_METHOD_INFO* methodInfo);

Expand Down Expand Up @@ -1150,6 +1158,7 @@ class InterpCompiler
static bool s_browserProfilerEnabled;
#endif
#endif // PERFTRACING_DISABLE_THREADS
static bool s_interpPgoEnabled;

#if MEASURE_MEM_ALLOC
// Memory statistics for profiling.
Expand Down
5 changes: 5 additions & 0 deletions src/coreclr/interpreter/eeinterp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ extern "C" INTERP_API void jitStartup(ICorJitHost* jitHost)
#endif // PERFTRACING_DISABLE_THREADS
}

if (InterpConfig.InterpPGO() != 0)
{
InterpCompiler::s_interpPgoEnabled = true;
}

g_interpInitialized = true;
}
/*****************************************************************************/
Expand Down
2 changes: 2 additions & 0 deletions src/coreclr/interpreter/inc/intops.def
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ OPDEF(INTOP_PROF_ENTER, "prof.enter", 2, 0, 0, InterpOpMethodHandle)
OPDEF(INTOP_PROF_LEAVE, "prof.leave", 1, 0, 0, InterpOpNoArgs)
#endif

OPDEF(INTOP_PGO_COUNT, "pgo.count", 2, 0, 0, InterpOpLdPtr)

OPDEF(INTOP_BR, "br", 2, 0, 0, InterpOpBranch)

OPDEF(INTOP_BRFALSE_I4, "brfalse.i4", 3, 0, 1, InterpOpBranch)
Expand Down
2 changes: 2 additions & 0 deletions src/coreclr/interpreter/interpconfigvalues.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions src/coreclr/nativeaot/Runtime/eventpipe/ep-rt-aot.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
28 changes: 28 additions & 0 deletions src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@
#ifdef ENABLE_PERFTRACING
#include <eventpipe/ep-types.h>
#include <eventpipe/ep.h>
#include <eventpipe/ep-event.h>
#include <eventpipe/ep-session.h>
#include <eventpipe/ep-stack-contents.h>
#include <eventpipe/ep-rt.h>
#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;
Expand Down Expand Up @@ -169,4 +174,27 @@ ep_rt_coreclr_sample_profiler_write_sampling_event_for_threads (
return;
}

void
ep_rt_coreclr_session_stopping (void)
{
STATIC_CONTRACT_NOTHROW;
#if defined(FEATURE_PGO) && defined(PERFTRACING_DISABLE_THREADS)
// 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_for_current_thread (EventPipeEventJitInstrumentationDataVerbose))
{
EX_TRY
{
Comment thread
pavelsavara marked this conversation as resolved.
PgoManager::LogInstrumentationData ();
}
EX_CATCH { }
EX_END_CATCH
}
Comment thread
pavelsavara marked this conversation as resolved.
#endif // FEATURE_PGO && PERFTRACING_DISABLE_THREADS
}

#endif /* ENABLE_PERFTRACING */
19 changes: 17 additions & 2 deletions src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -1093,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");
Expand Down
6 changes: 6 additions & 0 deletions src/coreclr/vm/interpexec.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2088,6 +2088,12 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr
INTOP_NEXT;
#endif // TARGET_BROWSER && PERFTRACING_DISABLE_THREADS

INTOP_CASE(INTOP_PGO_COUNT)
// Interlocked so concurrent executions of an instrumented method don't lose counts.
InterlockedIncrement((LONG*)pMethod->pDataItems[ip[1]]);
ip += 2;
INTOP_NEXT;

INTOP_CASE(INTOP_BR)
ip += ip[1];
INTOP_NEXT;
Expand Down
23 changes: 16 additions & 7 deletions src/coreclr/vm/jitinterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13062,7 +13062,15 @@ CORJIT_FLAGS* CEECodeGenInfo::getJitFlagsInternal()
}

/*********************************************************************/
HRESULT CEEJitInfo::allocPgoInstrumentationBySchema(
#ifdef FEATURE_PGO
static bool InterpreterPgoInstrumentationEnabled()
{
static ConfigDWORD s_interpPgo;
return s_interpPgo.val(CLRConfig::INTERNAL_InterpPGO) != 0;
}
#endif // FEATURE_PGO

HRESULT CEECodeGenInfo::allocPgoInstrumentationBySchema(
CORINFO_METHOD_HANDLE ftnHnd, /* IN */
PgoInstrumentationSchema* pSchema, /* IN/OUT */
uint32_t countSchemaItems, /* IN */
Expand All @@ -13081,9 +13089,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())
{
Comment thread
pavelsavara marked this conversation as resolved.
hr = PgoManager::allocPgoInstrumentationBySchema(pMD, m_ILHeader, pSchema, countSchemaItems, pInstrumentationData);
}
Expand All @@ -13092,7 +13101,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

Expand All @@ -13101,9 +13110,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
Expand Down Expand Up @@ -13164,7 +13173,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

Expand Down
Loading
Loading