Repository navigation
[browser][CoreCLR] Enable EventPipe CPU sampling for RyuJIT/R2R on WebAssembly #132788
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
fe010f9
8fbf7a6
0e67f86
785a74e
f213f25
a5d680b
30508c9
7796eed
d0024e1
7afc2b2
0c9f1f0
d3afb33
7033c2c
d6c0e9e
83bb941
194da0c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2466,7 +2466,7 @@ PhaseStatus Compiler::fgWasmSpillRefs() | |
|
|
||
| for (GenTree* tree : LIR::AsRange(block)) | ||
| { | ||
| if (tree->IsCall()) | ||
| if (tree->IsCall() && IsPotentialGCSafePoint(tree)) | ||
| { | ||
| // If we have a call, consume the call's operands from the defs list BEFORE spilling | ||
| // any live refs. This is because a GC-ref node which is only live INTO (and not across) another call | ||
|
|
@@ -3503,6 +3503,80 @@ void Compiler::fgWasmEhTransformTry(ArrayStack<BasicBlock*>* 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 call to CORINFO_HELP_WASM_PROF_SAMPLEPOINT. The later | ||
| // fgWasmVirtualIP phase inserts its per-block Virtual IP store ahead of the | ||
| // samplepoint, giving the cooperative stack walk a correct Virtual IP. | ||
| // | ||
| 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; | ||
| } | ||
|
|
||
| auto insertSamplepoint = [this](BasicBlock* block) { | ||
| GenTree* samplepoint = gtNewHelperCallNode(CORINFO_HELP_WASM_PROF_SAMPLEPOINT, TYP_VOID); | ||
| samplepoint = fgMorphCall(samplepoint->AsCall()); | ||
| gtSetEvalOrder(samplepoint); | ||
| LIR::AsRange(block).InsertAtBeginning(LIR::SeqTree(this, samplepoint)); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For methods compiled with When a sampling session is active, Would it make sense to insert the entry samplepoint after the reverse P/Invoke enter call, or simply skip the entry samplepoint for reverse P/Invoke methods? (I found this by reading the code; I haven't reproduced it.) Note This comment was generated with GitHub Copilot assistance. |
||
| }; | ||
|
|
||
| unsigned samplepointsAdded = 0; | ||
|
|
||
| // A DFS back edge identifies its ancestor target as a loop header. Sample at | ||
| // every unique loop header, including loops formed through EH flow. | ||
| // 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. | ||
| FlowGraphDfsTree* const dfsTree = fgComputeDfs(); | ||
| BitVecTraits traits = dfsTree->PostOrderTraits(); | ||
| BitVec samplepointBlocks(BitVecOps::MakeEmpty(&traits)); | ||
|
|
||
| // Method entry. | ||
| BitVecOps::AddElemD(&traits, samplepointBlocks, fgFirstBB->bbPostorderNum); | ||
|
|
||
| for (unsigned i = 0; i < dfsTree->GetPostOrderCount(); i++) | ||
| { | ||
| BasicBlock* const source = dfsTree->GetPostOrder(i); | ||
| source->VisitAllSuccs(this, [&](BasicBlock* target) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Depending on the DFS order, either the filter entry or the finally entry becomes a "back-edge target". If it's the filter entry, Would walking the wasm control-flow graph instead ( Note This comment was generated with GitHub Copilot assistance. |
||
| assert(dfsTree->Contains(target)); | ||
| if (dfsTree->IsAncestor(target, source)) | ||
| { | ||
| BitVecOps::AddElemD(&traits, samplepointBlocks, target->bbPostorderNum); | ||
| } | ||
|
|
||
| return BasicBlockVisit::Continue; | ||
| }); | ||
| } | ||
|
|
||
| BitVecOps::VisitBits(&traits, samplepointBlocks, [&](unsigned postorderNum) { | ||
| insertSamplepoint(dfsTree->GetPostOrder(postorderNum)); | ||
| samplepointsAdded++; | ||
| return true; | ||
| }); | ||
|
|
||
| JITDUMP("Added %u Wasm profiler samplepoint(s)\n", samplepointsAdded); | ||
| return (samplepointsAdded > 0) ? PhaseStatus::MODIFIED_EVERYTHING : PhaseStatus::MODIFIED_NOTHING; | ||
| } | ||
|
pavelsavara marked this conversation as resolved.
|
||
|
|
||
| //----------------------------------------------------------------------------- | ||
| // fgWasmVirtualIP: set up virtual IP mapping for EH and calls | ||
| // | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this guard skips more than intended. Because the operand-removal
VisitOperands(removeUses)lives inside thisif, and the fallback removal further down is guarded byif (!tree->IsCall()), the ref/byref operands of a NoGC helper call are now never removed fromdefs.One concrete case is
CORINFO_HELP_BULK_WRITEBARRIER_SMALLfromLowering::LowerBlockStoreAsGcBulkCopyCall: its destination is typically a non-invariant byref (e.g.ADD(obj, offset)), so it gets pushed intodefsand stays there. In a checked JIT that should tripassert(defs.empty())at the start of the next block; in release it would lead to unnecessary pinned spills at the next GC-safe call. This affects all wasm methods, not only instrumented ones.As far as I can tell, the samplepoints themselves don't need this change: they're inserted at the start of a block, where no LIR values are live, so they never cause spills anyway. Would it work to either revert this line, or keep operand removal unconditional for all calls and gate only the spilling? Something like:
(I found this by reading the code; I haven't reproduced it.)
Note
This comment was generated with GitHub Copilot assistance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1, the code here is meant to consume all outstanding refs for the closest parent call, regardless of whether it's a safepoint or not. It's true that we only need to spill not-yet-consumed refs if the tree is a safe point though, so I think it is sound to change the code to what @lewing is suggesting.