From fe010f9c0d41d97900f67b11c5966e5b99b925c9 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Wed, 26 Aug 2026 17:22:13 +0200 Subject: [PATCH] [browser][coreCLR] EventPipe CPU sampling for RyuJIT/R2R on WASM Adds cooperative, self-triggered EventPipe CPU sampling for RyuJIT-generated R2R (native wasm) code on single-threaded browser/WASI, complementing the interpreter sampling from #126324. On single-threaded WASM there is no sampling thread, so sampling is driven by the instrumented code itself. - New JIT helper CORINFO_HELP_WASM_PROF_SAMPLEPOINT and R2R helper READYTORUN_HELPER_WasmProfSamplepoint (0x119, R2R minor 27.1). Native shim JIT_WasmProfSamplepoint in vm/wasm/helpers.cpp (modeled on JIT_PollGC) anchors a cooperative managed stack walk via an InlinedCallFrame / INLINED_PINVOKE_FROM_R2R derived from the caller's shadow SP, then delegates to the shared SamplingProfiler_OnSamplepoint. No GC transition; only walks on an actual sample. - New GT_WASM_PROF_SAMPLEPOINT node and PHASE_WASM_PROF_INSTRUMENT: emit a samplepoint at method entry and every loop back-edge for methods matching the WasmPerformanceInstrumentation MethodSet filter (same key and shared skip counter as the interpreter). Codegen emits it via genEmitHelperCall with the managed (sp, pep) ABI, like the throw helpers. - crossgen2 maps the helper to its R2R cell; the WebAssembly SDK forwards the filter to crossgen (PublishReadyToRunCrossgen2ExtraArgs) and warns that framework and BCL frames are absent from samples (the runtime-pack R2R images are not instrumented). v1 uses an unconditional helper call (skip counter inside the helper). The inline countdown fast path and the browser DevTools flame-chart profiler (PROF_ENTER/LEAVE plus an EH-dispatcher unwind hook) are deferred; see the PR description TODO. Validated: browser CoreCLR clr+host build clean; crossgen2 with WasmPerformanceInstrumentation=* over all of System.Private.CoreLib completes cleanly and the instrumented R2R image is ~6% larger than baseline (samplepoints emitted); the SDK warning fires on R2R publish. End-to-end runtime .nettrace capture is blocked by pre-existing experimental CoreCLR-browser sample-publish infrastructure issues (see TODO), not by this change. --- src/coreclr/inc/corinfo.h | 2 + src/coreclr/inc/jiteeversionguid.h | 10 +- src/coreclr/inc/jithelpers.h | 8 ++ src/coreclr/inc/readytorun.h | 4 +- src/coreclr/inc/readytorunhelpers.h | 1 + src/coreclr/jit/codegenwasm.cpp | 9 ++ src/coreclr/jit/compiler.cpp | 5 + src/coreclr/jit/compiler.h | 1 + src/coreclr/jit/compphases.h | 1 + src/coreclr/jit/fgwasm.cpp | 117 ++++++++++++++++++ src/coreclr/jit/gentree.cpp | 2 + src/coreclr/jit/gtlist.h | 1 + src/coreclr/jit/jitconfigvalues.h | 3 + src/coreclr/jit/liveness.cpp | 1 + .../Internal/Runtime/ReadyToRunConstants.cs | 1 + .../Common/JitInterface/CorInfoHelpFunc.cs | 2 + .../JitInterface/CorInfoImpl.ReadyToRun.cs | 4 + .../ReadyToRunSignature.cs | 4 + .../eventpipe/ep-rt-coreclr-wasm-sampling.cpp | 8 ++ src/coreclr/vm/jitinterface.h | 5 + src/coreclr/vm/wasm/helpers.cpp | 52 ++++++++ ...ET.Sdk.WebAssembly.Browser.CoreCLR.targets | 7 ++ 22 files changed, 242 insertions(+), 6 deletions(-) diff --git a/src/coreclr/inc/corinfo.h b/src/coreclr/inc/corinfo.h index d47ff3cf0a63db..60002abd421f77 100644 --- a/src/coreclr/inc/corinfo.h +++ b/src/coreclr/inc/corinfo.h @@ -589,6 +589,8 @@ enum CorInfoHelpFunc CORINFO_HELP_ALLOC_CONTINUATION_METHOD, CORINFO_HELP_ALLOC_CONTINUATION_CLASS, + CORINFO_HELP_WASM_PROF_SAMPLEPOINT, // WASM only: cooperative EventPipe CPU-sampling samplepoint + CORINFO_HELP_COUNT, }; diff --git a/src/coreclr/inc/jiteeversionguid.h b/src/coreclr/inc/jiteeversionguid.h index f3efb75127fd67..c88fa94ce3b401 100644 --- a/src/coreclr/inc/jiteeversionguid.h +++ b/src/coreclr/inc/jiteeversionguid.h @@ -37,11 +37,11 @@ #include -constexpr GUID JITEEVersionIdentifier = { /* 0470bb0a-8e19-446c-a494-04f35ca0404c */ - 0x0470bb0a, - 0x8e19, - 0x446c, - {0xa4, 0x94, 0x04, 0xf3, 0x5c, 0xa0, 0x40, 0x4c} +constexpr GUID JITEEVersionIdentifier = { /* 1f960272-4271-4a63-8613-309367a41370 */ + 0x1f960272, + 0x4271, + 0x4a63, + {0x86, 0x13, 0x30, 0x93, 0x67, 0xa4, 0x13, 0x70} }; #endif // JIT_EE_VERSIONING_GUID_H diff --git a/src/coreclr/inc/jithelpers.h b/src/coreclr/inc/jithelpers.h index 3ba323077d060d..a6031ac280b3e5 100644 --- a/src/coreclr/inc/jithelpers.h +++ b/src/coreclr/inc/jithelpers.h @@ -367,6 +367,14 @@ DYNAMICJITHELPER(CORINFO_HELP_ALLOC_CONTINUATION_METHOD, NULL, METHOD__ASYNC_HELPERS__ALLOC_CONTINUATION_METHOD) DYNAMICJITHELPER(CORINFO_HELP_ALLOC_CONTINUATION_CLASS, NULL, METHOD__ASYNC_HELPERS__ALLOC_CONTINUATION_CLASS) + // WASM EventPipe CPU-sampling samplepoint. Native helper (no managed fallback), auto-wrapped in a + // PortableEntryPoint by getHelperFtnAddr. Never emitted on other targets; NULL keeps enum/table aligned. +#ifdef TARGET_WASM + JITHELPER(CORINFO_HELP_WASM_PROF_SAMPLEPOINT, JIT_WasmProfSamplepoint, METHOD__NIL) +#else + JITHELPER(CORINFO_HELP_WASM_PROF_SAMPLEPOINT, NULL, METHOD__NIL) +#endif + #undef JITHELPER #undef DYNAMICJITHELPER #undef JITHELPER diff --git a/src/coreclr/inc/readytorun.h b/src/coreclr/inc/readytorun.h index c81c19a6017005..da497d123e0278 100644 --- a/src/coreclr/inc/readytorun.h +++ b/src/coreclr/inc/readytorun.h @@ -20,7 +20,7 @@ // If you update this, ensure you run `git grep MINIMUM_READYTORUN_MAJOR_VERSION` // and handle pending work. #define READYTORUN_MAJOR_VERSION 27 -#define READYTORUN_MINOR_VERSION 0x0000 +#define READYTORUN_MINOR_VERSION 0x0001 #define MINIMUM_READYTORUN_MAJOR_VERSION 26 @@ -68,6 +68,7 @@ // R2R Version 26 changes ARM64 NativeVarInfo register encoding to include V0-V31 // R2R Version 26.1 adds READYTORUN_FIXUP_StoreMultiCallableAddrOfCode for storing a method's MultiCallableAddrOfCode into a location in the R2R image (used on WebAssembly) // R2R Version 27 redefines READYTORUN_FIXUP_DeclaringTypeHandle to be encoded as a method signature instead of a pair of type signatures +// R2R Version 27.1 adds READYTORUN_HELPER_WasmProfSamplepoint (used on WebAssembly) struct READYTORUN_CORE_HEADER { @@ -507,6 +508,7 @@ enum ReadyToRunHelper READYTORUN_HELPER_InitClass = 0x116, READYTORUN_HELPER_InitInstClass = 0x117, READYTORUN_HELPER_R2RToInterpreter = 0x118, + READYTORUN_HELPER_WasmProfSamplepoint = 0x119, }; #include "readytoruninstructionset.h" diff --git a/src/coreclr/inc/readytorunhelpers.h b/src/coreclr/inc/readytorunhelpers.h index 4c3fca575d0ae5..27ce2a6fb3874b 100644 --- a/src/coreclr/inc/readytorunhelpers.h +++ b/src/coreclr/inc/readytorunhelpers.h @@ -117,6 +117,7 @@ HELPER(READYTORUN_HELPER_CheckedWriteBarrier_EBP, CORINFO_HELP_CHECKED_ASSIGN_ HELPER(READYTORUN_HELPER_PInvokeBegin, CORINFO_HELP_JIT_PINVOKE_BEGIN, ) HELPER(READYTORUN_HELPER_PInvokeEnd, CORINFO_HELP_JIT_PINVOKE_END, ) HELPER(READYTORUN_HELPER_GCPoll, CORINFO_HELP_POLL_GC, ) +HELPER(READYTORUN_HELPER_WasmProfSamplepoint, CORINFO_HELP_WASM_PROF_SAMPLEPOINT, ) HELPER(READYTORUN_HELPER_ReversePInvokeEnter, CORINFO_HELP_JIT_REVERSE_PINVOKE_ENTER, ) HELPER(READYTORUN_HELPER_ReversePInvokeExit, CORINFO_HELP_JIT_REVERSE_PINVOKE_EXIT, ) diff --git a/src/coreclr/jit/codegenwasm.cpp b/src/coreclr/jit/codegenwasm.cpp index 8c637ad25a3119..88f7a2098317dd 100644 --- a/src/coreclr/jit/codegenwasm.cpp +++ b/src/coreclr/jit/codegenwasm.cpp @@ -1014,6 +1014,12 @@ void CodeGen::genCodeForTreeNode(GenTree* treeNode) // no codegen needed here. break; + case GT_WASM_PROF_SAMPLEPOINT: + // Managed helper ABI: push the shadow stack pointer, then call the samplepoint helper. + GetEmitter()->emitIns_I(INS_local_get, EA_PTRSIZE, GetStackPointerRegIndex()); + genEmitHelperCall(CORINFO_HELP_WASM_PROF_SAMPLEPOINT, 0, EA_UNKNOWN); + break; + case GT_WASM_THROW_REF: // Reload and rethrow the exnref stashed at the catch_ref landing. { @@ -3344,6 +3350,9 @@ void CodeGen::genEmitHelperCall(unsigned helper, int argSize, emitAttr retSize, // RhBulkMoveWithWriteBarrier HELPER_SIG(CORINFO_HELP_BULK_WRITEBARRIER, UNMANAGED, CORINFO_WASM_TYPE_VOID /* retval */, CORINFO_WASM_TYPE_I, CORINFO_WASM_TYPE_I, CORINFO_WASM_TYPE_I); + // EventPipe CPU-sampling samplepoint (native helper wrapped in a PortableEntryPoint; managed ABI) + HELPER_SIG(CORINFO_HELP_WASM_PROF_SAMPLEPOINT, MANAGED, CORINFO_WASM_TYPE_VOID /* retval */, + CORINFO_WASM_TYPE_I /* sp */, CORINFO_WASM_TYPE_I /* pep */); default: JITDUMP("Helper '%s' has no hard-coded signature\n", m_compiler->eeGetMethodFullName(params.methHnd)); unreached(); diff --git a/src/coreclr/jit/compiler.cpp b/src/coreclr/jit/compiler.cpp index c6a3bd1a7810fa..b81f427e6e58a1 100644 --- a/src/coreclr/jit/compiler.cpp +++ b/src/coreclr/jit/compiler.cpp @@ -4983,6 +4983,11 @@ void Compiler::compCompile(void** methodCodePtr, uint32_t* methodCodeSize, JitFl m_pLowering->FinalizeOutgoingArgSpace(); #ifdef TARGET_WASM + // Insert EventPipe CPU-sampling samplepoints before the Virtual IP phase, so the + // per-block Virtual IP stores land ahead of each samplepoint. + // + DoPhase(this, PHASE_WASM_PROF_INSTRUMENT, &Compiler::fgWasmProfInstrument); + // Determine if a Virtual IP is needed and add code as needed to // keep the Virtual IP updated. // diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h index 22b96b23bb12e2..bfbeb762c47bff 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -6971,6 +6971,7 @@ class Compiler PhaseStatus fgWasmRepairTryEntries(); PhaseStatus fgWasmVirtualIP(); PhaseStatus fgWasmSpillRefs(); + PhaseStatus fgWasmProfInstrument(); #ifdef DEBUG void fgDumpWasmControlFlow(); void fgDumpWasmControlFlowDot(); diff --git a/src/coreclr/jit/compphases.h b/src/coreclr/jit/compphases.h index 497608425b7d03..6c51c872925b10 100644 --- a/src/coreclr/jit/compphases.h +++ b/src/coreclr/jit/compphases.h @@ -134,6 +134,7 @@ CompPhaseNameMacro(PHASE_WASM_TRANSFORM_SCCS, "Wasm transform sccs", CompPhaseNameMacro(PHASE_WASM_CONTROL_FLOW, "Wasm control flow", false, -1, false) CompPhaseNameMacro(PHASE_WASM_SPILL_REFS, "Wasm spill refs", false, -1, false) CompPhaseNameMacro(PHASE_WASM_VIRTUAL_IP, "Wasm virtual IP", false, -1, false) +CompPhaseNameMacro(PHASE_WASM_PROF_INSTRUMENT, "Wasm profiler instrumentation", false, -1, false) CompPhaseNameMacro(PHASE_ASYNC, "Transform async", false, -1, true) CompPhaseNameMacro(PHASE_LCLVARLIVENESS, "Local var liveness", true, -1, false) diff --git a/src/coreclr/jit/fgwasm.cpp b/src/coreclr/jit/fgwasm.cpp index 63feee17649d37..f345e4cbc82885 100644 --- a/src/coreclr/jit/fgwasm.cpp +++ b/src/coreclr/jit/fgwasm.cpp @@ -3350,6 +3350,123 @@ void Compiler::fgWasmEhTransformTry(ArrayStack* catchRetBlocks, } } +//----------------------------------------------------------------------------- +// fgWasmProfInstrument: insert EventPipe CPU-sampling samplepoints +// +// Returns: +// suitable phase status +// +// Notes: +// Mirrors the interpreter's INTOP_PROF_SAMPLEPOINT placement (see +// src/coreclr/interpreter/compiler.cpp): one samplepoint at method entry and +// one on every loop back-edge. Emitted only for methods matching the +// WasmPerformanceInstrumentation MethodSet filter (same key the interpreter +// uses, so both engines share the filter and the runtime skip counter). +// +// Each samplepoint is a GT_WASM_PROF_SAMPLEPOINT leaf that codegen lowers to a +// call to CORINFO_HELP_WASM_PROF_SAMPLEPOINT. We run just before fgWasmVirtualIP +// so the per-block Virtual IP store is inserted ahead of the samplepoint, giving +// the cooperative stack walk a correct Virtual IP for the sampled frame. +// +PhaseStatus Compiler::fgWasmProfInstrument() +{ + // Codegen only supports single-threaded wasm today; the shared runtime skip + // counter and cooperative sampling both assume PERFTRACING_DISABLE_THREADS. + assert(!WASM_THREAD_SUPPORT); + + if (!JitConfig.WasmPerformanceInstrumentation().contains(info.compMethodHnd, info.compClassHnd, + &info.compMethodInfo->args)) + { + return PhaseStatus::MODIFIED_NOTHING; + } + + // Insert a samplepoint at the beginning of a block. Placed after any Virtual IP + // store that fgWasmVirtualIP will later insert at the block's beginning. + auto insertSamplepoint = [this](BasicBlock* block) { + GenTree* const samplepoint = new (this, GT_WASM_PROF_SAMPLEPOINT) GenTree(GT_WASM_PROF_SAMPLEPOINT, TYP_VOID); + LIR::AsRange(block).InsertAtBeginning(samplepoint); + }; + + unsigned samplepointsAdded = 0; + + // Method entry. + insertSamplepoint(fgFirstBB); + samplepointsAdded++; + + // Loop back-edges: a DFS back edge (source -> target where target is an + // ancestor still on the DFS stack) identifies target as a loop header. + // We sample at each unique loop header, which executes on every iteration. + // Unlike fgHasCycleWithoutGCSafePoint we do NOT skip BBF_GC_SAFE_POINT blocks: + // the framework is not instrumented, so a loop calling only BCL methods would + // otherwise never sample. + BitVecTraits traits(fgBBNumMax + 1, this); + BitVec visited(BitVecOps::MakeEmpty(&traits)); + BitVec onStack(BitVecOps::MakeEmpty(&traits)); + BitVec headers(BitVecOps::MakeEmpty(&traits)); + + struct DfsFrame + { + BasicBlock* block; + unsigned nextSucc; + }; + + ArrayStack stack(getAllocator(CMK_ArrayStack)); + + for (BasicBlock* const start : Blocks()) + { + if (BitVecOps::IsMember(&traits, visited, start->bbNum)) + { + continue; + } + + BitVecOps::AddElemD(&traits, visited, start->bbNum); + BitVecOps::AddElemD(&traits, onStack, start->bbNum); + stack.Push(DfsFrame{start, 0}); + + while (stack.Height() > 0) + { + DfsFrame& top = stack.TopRef(); + BasicBlock* const block = top.block; + const unsigned numSuccs = block->NumSucc(); + + if (top.nextSucc < numSuccs) + { + BasicBlock* const succ = block->GetSucc(top.nextSucc++); + + if (BitVecOps::IsMember(&traits, onStack, succ->bbNum)) + { + // Back edge: succ is a loop header. + BitVecOps::AddElemD(&traits, headers, succ->bbNum); + } + else if (!BitVecOps::IsMember(&traits, visited, succ->bbNum)) + { + BitVecOps::AddElemD(&traits, visited, succ->bbNum); + BitVecOps::AddElemD(&traits, onStack, succ->bbNum); + stack.Push(DfsFrame{succ, 0}); + } + } + else + { + BitVecOps::RemoveElemD(&traits, onStack, block->bbNum); + stack.Pop(); + } + } + } + + for (BasicBlock* const block : Blocks()) + { + // fgFirstBB already got an entry samplepoint above. + if ((block != fgFirstBB) && BitVecOps::IsMember(&traits, headers, block->bbNum)) + { + insertSamplepoint(block); + samplepointsAdded++; + } + } + + JITDUMP("Added %u Wasm profiler samplepoint(s)\n", samplepointsAdded); + return (samplepointsAdded > 0) ? PhaseStatus::MODIFIED_EVERYTHING : PhaseStatus::MODIFIED_NOTHING; +} + //----------------------------------------------------------------------------- // fgWasmVirtualIP: set up virtual IP mapping for EH and calls // diff --git a/src/coreclr/jit/gentree.cpp b/src/coreclr/jit/gentree.cpp index c29cd4f987e7d9..9bd8a66b3e85d1 100644 --- a/src/coreclr/jit/gentree.cpp +++ b/src/coreclr/jit/gentree.cpp @@ -12062,6 +12062,7 @@ GenTreeUseEdgeIterator::GenTreeUseEdgeIterator(GenTree* node) case GT_GCPOLL: case GT_WASM_THROW_REF: case GT_WASM_JEXCEPT: + case GT_WASM_PROF_SAMPLEPOINT: m_state = -1; return; @@ -14252,6 +14253,7 @@ void Compiler::gtDispLeaf(GenTree* tree, IndentStack* indentStack) case GT_GCPOLL: case GT_WASM_THROW_REF: case GT_WASM_JEXCEPT: + case GT_WASM_PROF_SAMPLEPOINT: break; case GT_CONTINUATION_MEMBER_OFFSET: diff --git a/src/coreclr/jit/gtlist.h b/src/coreclr/jit/gtlist.h index 622a9643e7db0b..0ae9aad9bbf72e 100644 --- a/src/coreclr/jit/gtlist.h +++ b/src/coreclr/jit/gtlist.h @@ -366,6 +366,7 @@ GTNODE(SWIFT_ERROR_RET , GenTreeOp ,0,1,GTK_BINOP|GTK_NOVALUE) // Retu GTNODE(WASM_JEXCEPT , GenTree ,0,0,GTK_LEAF|GTK_NOVALUE|DBK_NOTHIR) // Special jump for Wasm exception handling GTNODE(WASM_THROW_REF , GenTree ,0,0,GTK_LEAF|GTK_NOVALUE|DBK_NOTHIR) // Wasm rethrow host exception (exception is an implicit operand) +GTNODE(WASM_PROF_SAMPLEPOINT, GenTree ,0,0,GTK_LEAF|GTK_NOVALUE|DBK_NOTHIR) // Wasm EventPipe CPU-sampling samplepoint //----------------------------------------------------------------------------- // Nodes used by Lower to generate a closer CPU representation of other nodes diff --git a/src/coreclr/jit/jitconfigvalues.h b/src/coreclr/jit/jitconfigvalues.h index 3b220e7a93ab03..847133de299da3 100644 --- a/src/coreclr/jit/jitconfigvalues.h +++ b/src/coreclr/jit/jitconfigvalues.h @@ -914,6 +914,9 @@ RELEASE_CONFIG_INTEGER(JitWasmNyiToR2RUnsupported, "JitWasmNyiToR2RUnsupported", CONFIG_STRING(JitR2RUnsupportedRange, "JitR2RUnsupportedRange") // Enable processing methods with funclets. Set to 0 to bail to R2R unsupported before codegen. RELEASE_CONFIG_INTEGER(JitWasmFunclets, "JitWasmFunclets", 1) +// Method filter for WASM performance instrumentation profiler. Uses standard MethodSet pattern format. +// Same key as the interpreter's InterpConfig.WasmPerformanceInstrumentation so both engines share it. +RELEASE_CONFIG_METHODSET(WasmPerformanceInstrumentation, "WasmPerformanceInstrumentation") #endif // defined(TARGET_WASM) // Allow to enregister locals with struct type. diff --git a/src/coreclr/jit/liveness.cpp b/src/coreclr/jit/liveness.cpp index 9198ab6c529d0e..ddad179862151e 100644 --- a/src/coreclr/jit/liveness.cpp +++ b/src/coreclr/jit/liveness.cpp @@ -2482,6 +2482,7 @@ void Liveness::ComputeLifeLIR(VARSET_TP& life, BasicBlock* block, VAR case GT_SWIFT_ERROR_RET: case GT_GCPOLL: case GT_WASM_JEXCEPT: + case GT_WASM_PROF_SAMPLEPOINT: // Never remove these nodes, as they are always side-effecting. // // NOTE: the only side-effect of some of these nodes (GT_CMP, GT_SUB_HI) is a write to the flags diff --git a/src/coreclr/tools/Common/Internal/Runtime/ReadyToRunConstants.cs b/src/coreclr/tools/Common/Internal/Runtime/ReadyToRunConstants.cs index dac7b8fe8bd2c6..2999d161c904a8 100644 --- a/src/coreclr/tools/Common/Internal/Runtime/ReadyToRunConstants.cs +++ b/src/coreclr/tools/Common/Internal/Runtime/ReadyToRunConstants.cs @@ -377,6 +377,7 @@ public enum ReadyToRunHelper InitClass = 0x116, InitInstClass = 0x117, R2RToInterpreter = 0x118, + WasmProfSamplepoint = 0x119, // ********************************************************************************************** // diff --git a/src/coreclr/tools/Common/JitInterface/CorInfoHelpFunc.cs b/src/coreclr/tools/Common/JitInterface/CorInfoHelpFunc.cs index ab7ca8480c7a54..e8589c774d5ccd 100644 --- a/src/coreclr/tools/Common/JitInterface/CorInfoHelpFunc.cs +++ b/src/coreclr/tools/Common/JitInterface/CorInfoHelpFunc.cs @@ -286,6 +286,8 @@ which is the right helper to use to allocate an object of a given type. */ CORINFO_HELP_ALLOC_CONTINUATION_METHOD, CORINFO_HELP_ALLOC_CONTINUATION_CLASS, + CORINFO_HELP_WASM_PROF_SAMPLEPOINT, // WASM only: cooperative EventPipe CPU-sampling samplepoint + CORINFO_HELP_COUNT, } } diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index 358466276e92cd..1b6e69017d054e 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -1281,6 +1281,10 @@ private ISymbolNode GetHelperFtnUncached(CorInfoHelpFunc ftnNum) id = ReadyToRunHelper.GCPoll; break; + case CorInfoHelpFunc.CORINFO_HELP_WASM_PROF_SAMPLEPOINT: + id = ReadyToRunHelper.WasmProfSamplepoint; + break; + case CorInfoHelpFunc.CORINFO_HELP_GETCURRENTMANAGEDTHREADID: id = ReadyToRunHelper.GetCurrentManagedThreadId; break; diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunSignature.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunSignature.cs index 1323adec086780..646250a301ce1f 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunSignature.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunSignature.cs @@ -1801,6 +1801,10 @@ private void ParseHelper(StringBuilder builder) builder.Append("GCPOLL"); break; + case ReadyToRunHelper.WasmProfSamplepoint: + builder.Append("WASM_PROF_SAMPLEPOINT"); + break; + case ReadyToRunHelper.GetCurrentManagedThreadId: builder.Append("GET_CURRENT_MANAGED_THREAD_ID"); break; diff --git a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr-wasm-sampling.cpp b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr-wasm-sampling.cpp index 2f2c04a0cde250..e29e1c82c1b523 100644 --- a/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr-wasm-sampling.cpp +++ b/src/coreclr/vm/eventing/eventpipe/ep-rt-coreclr-wasm-sampling.cpp @@ -140,6 +140,14 @@ void ep_rt_coreclr_sample_profiler_disabled(void) s_skipsPerPeriod = 1; } +// True only while an EventPipe CPU-sampling session is active. R2R samplepoints use this to +// skip the InlinedCallFrame anchor push on the overwhelmingly common no-session path. +extern "C" bool SamplingProfiler_IsActive() +{ + LIMITED_METHOD_CONTRACT; + return s_currentSamplingEvent != nullptr && s_currentSamplingThread != nullptr; +} + // Called from the interpreter's INTOP_PROF_SAMPLEPOINT handler. // On single-threaded WASM this is the cooperative sampling entry point. // On multi-threaded platforms the opcode is never emitted. diff --git a/src/coreclr/vm/jitinterface.h b/src/coreclr/vm/jitinterface.h index 7727e7d8114832..27d41e6ebf53ca 100644 --- a/src/coreclr/vm/jitinterface.h +++ b/src/coreclr/vm/jitinterface.h @@ -101,6 +101,11 @@ EXTERN_C FCDECL1(PCODE, JIT_PatchpointForced, int ilOffset); EXTERN_C FCDECL0(void, JIT_PollGC); +#ifdef TARGET_WASM +// WASM EventPipe CPU-sampling samplepoint helper (defined in vm/wasm/helpers.cpp). +EXTERN_C void JIT_WasmProfSamplepoint(uintptr_t callersStackPointer, PCODE portableEntryPointContext); +#endif // TARGET_WASM + #ifndef JIT_GetGCStaticBase #define JIT_GetGCStaticBase NULL #else diff --git a/src/coreclr/vm/wasm/helpers.cpp b/src/coreclr/vm/wasm/helpers.cpp index eae917dc48a636..99207ea936f986 100644 --- a/src/coreclr/vm/wasm/helpers.cpp +++ b/src/coreclr/vm/wasm/helpers.cpp @@ -831,6 +831,58 @@ EXTERN_C __attribute__((naked)) void F_CALL_CONV JIT_PollGC(uintptr_t callersSta [JIT_PollGCRarePath] "i" (JIT_PollGCRarePath)); } +// EventPipe CPU-sampling samplepoint helper for R2R (native wasm) code on single-threaded WASM. +// Emitted by the JIT at method entry and loop back-edges when a method matches the +// WasmPerformanceInstrumentation filter. Cooperative throughout; on an active sampling session it +// anchors a managed stack walk with an InlinedCallFrame derived from the caller's shadow SP, then +// delegates to the shared SamplingProfiler_OnSamplepoint (which owns the adaptive skip counter). +EXTERN_C void JIT_WasmProfSamplepointImpl(uintptr_t callersStackPointer) +{ +#if defined(ENABLE_PERFTRACING) && defined(PERFTRACING_DISABLE_THREADS) + extern bool SamplingProfiler_IsActive(); + extern void SamplingProfiler_OnSamplepoint(); + + if (!SamplingProfiler_IsActive()) + return; + + Thread* pThread = GetThread(); + + // Anchor the walk with a cooperative InlinedCallFrame (JIT_PInvokeBeginImpl's setup minus the + // preemptive transition): StackWalkFrames starts from a zeroed context and walks the Frame chain. + InlinedCallFrame inlinedCallFrame; + ::new ((void*)&inlinedCallFrame) InlinedCallFrame(); + inlinedCallFrame.m_pCallSiteSP = (void*)callersStackPointer; + inlinedCallFrame.m_pCallerReturnAddress = INLINED_PINVOKE_FROM_R2R; + inlinedCallFrame.m_pCalleeSavedFP = 0; + inlinedCallFrame.m_pThread = pThread; + inlinedCallFrame.Push(); + + SamplingProfiler_OnSamplepoint(); + + inlinedCallFrame.Pop(); +#else + UNREFERENCED_PARAMETER(callersStackPointer); +#endif // ENABLE_PERFTRACING && PERFTRACING_DISABLE_THREADS +} + +// Naked shim modeled on JIT_PollGC: publish the caller's shadow SP to the __stack_pointer global +// before any native code runs, then restore it. The pep parameter is unused scratch (as in JIT_PollGC). +EXTERN_C void JIT_WasmProfSamplepoint(uintptr_t callersStackPointer, PCODE portableEntryPointContext); +EXTERN_C __attribute__((naked)) void F_CALL_CONV JIT_WasmProfSamplepoint(uintptr_t callersStackPointer, PCODE portableEntryPointContext) +{ + asm( + "global.get __stack_pointer\n" + "local.set 1\n" /* save previous __stack_pointer into the unused pep local */ + "local.get 0\n" /* callersStackPointer */ + "global.set __stack_pointer\n" + "local.get 0\n" /* sp argument for the impl */ + "call %[JIT_WasmProfSamplepointImpl]\n" + "local.get 1\n" /* restore previous __stack_pointer */ + "global.set __stack_pointer\n" + "return\n" + :: [JIT_WasmProfSamplepointImpl] "i" (JIT_WasmProfSamplepointImpl)); +} + void InitJITHelpers1() { /* no-op WASM-TODO do we need to do anything for the interpreter? */ 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 7b267b85ae92ac..3ba7171346a316 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 @@ -28,6 +28,9 @@ Copyright (c) .NET Foundation. All rights reserved. $(PublishReadyToRunCrossgen2ExtraArgs);--opt-cross-module:* $(PublishReadyToRunCrossgen2ExtraArgs);--codegenopt:JitWasmNyiToR2RUnsupported=1;--codegenopt:JitWasmSimdNyiToR2RUnsupported=1 + + $(PublishReadyToRunCrossgen2ExtraArgs);--codegenopt:WasmPerformanceInstrumentation=$(WasmPerformanceInstrumentation) + <_WasmRuntimePackR2RDir Condition="'$(_WasmRuntimePackR2RDir)' == '' and '$(_RuntimePackNativeDir)' != ''">$([MSBuild]::NormalizeDirectory('$(_RuntimePackNativeDir)', 'r2r'))