[Profiler] Fix DoStackSnapshot calls in ELT hooks - #132301
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag |
There was a problem hiding this comment.
Pull request overview
This PR updates CoreCLR’s profiler ELT (enter/leave/tailcall) slow-path helpers so same-thread DoStackSnapshot calls can be reliably seeded on Unix by capturing a native context for the callback’s duration and using it when no profiler seed context is provided.
Changes:
- Add per-thread storage for an ELT-captured native context and RAII to publish/restore it across (including nested) ELT callbacks.
- Teach
DoStackSnapshotto use the ELT-captured context (same-thread + unseeded) by copying and virtually unwinding it to the first managed frame before proceeding through existing seed handling. - Extend the slow-path ELT native profiler test to enable stack snapshots and validate
DoStackSnapshotsuccess (non-Windows) while ensuring GC-triggering APIs (e.g.,ForceGC) remain rejected from hooks.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/tests/profiler/native/eltprofiler/slowpatheltprofiler.h | Adds state and declarations for new ELT-hook restriction tests (snapshot + GC restriction). |
| src/tests/profiler/native/eltprofiler/slowpatheltprofiler.cpp | Enables COR_PRF_ENABLE_STACK_SNAPSHOT and adds an ELT-hook-time DoStackSnapshot + ForceGC restriction check. |
| src/coreclr/vm/threads.h | Introduces Thread::m_pProfilerELTContext plus accessor methods for ELT snapshot seeding. |
| src/coreclr/vm/threads.cpp | Initializes the new per-thread ELT context pointer. |
| src/coreclr/vm/proftoeeinterfaceimpl.cpp | Captures ELT contexts in hooks and consumes them in DoStackSnapshot for same-thread unseeded walks. |
| src/coreclr/vm/profilinghelper.inl | Implements ProfilerELTContextHolder to publish/restore the ELT context on the current thread. |
| src/coreclr/vm/profilinghelper.h | Declares the ProfilerELTContextHolder RAII type. |
| if ((pctxSeed == nullptr) && (pThreadToSnapshot == pCurrentThread)) | ||
| { | ||
| T_CONTEXT *pELTContext = pCurrentThread->GetProfilerELTContext(); | ||
| if (pELTContext != nullptr) | ||
| { | ||
| CopyOSContext(&ctxELT, pELTContext); | ||
| Thread::VirtualUnwindToFirstManagedCallFrame(&ctxELT); | ||
| pctxSeed = &ctxELT; | ||
| } | ||
| } |
| } | ||
| CONTRACTL_END; | ||
|
|
||
| ClrCaptureContext(pContext); |
There was a problem hiding this comment.
This is very expensive. What is the performance impact of this change when the ETL hooks are enabled? I suspect that it will extremely slow to the point of being unusuable.
There was a problem hiding this comment.
I see you have some benchmark results in the PR description. What was the benchmark source?
There was a problem hiding this comment.
I used an adhoc BenchmarkDotNet microbenchmark in my local repo in conjunction with a profiler with a barebones EnterCallback. Since the context saving and restoration surround the callback in ProfilerEnter, I ran the benchmark against the baseline repo and changed corerun builds to measure the overhead introduced in this PR.
| if (pELTContext != nullptr) | ||
| { | ||
| CopyOSContext(&ctxELT, pELTContext); | ||
| Thread::VirtualUnwindToFirstManagedCallFrame(&ctxELT); |
There was a problem hiding this comment.
This is indirectly introducing new libunwind dependency. It is factory for problems. For example, this won't work on Windows x86 at all.
I think if we wanted to fix this, it would be better to fix the codegen for these hooks so that there is proper managed->unmanaged transition.
There was a problem hiding this comment.
I'm not sure DoStackSnapshot works within ELT hooks on Windows x86, as it returned CORPROF_E_STACKSNAPSHOT_UNSAFE on Windows x64 both in .NET 9 and .NET 10.
It seemed like the scenario only worked on Linux x64 in .NET 9.
Thanks, I'll look into fixing the codegen if this overhead is going in the wrong direction.
There was a problem hiding this comment.
If this never worked on Windows x64, was it actually working reliably on Linux? If it never worked well, can we block it everywhere instead like it is blocked on Windows?
I do not think it is worth it to try to make these callbacks worth better. They are slow, and I believe most profilers moved away from them.
There was a problem hiding this comment.
Is there a particular reason for DoStackSnapshot to not be supported within these callbacks?
I don't fully understand what DoStackSnapshot within these callbacks would achieve, but for the user of the original issue, what would an alternative be?
It sounds like the scenario was working on Linux until #107152, from the issue author it worked on .NET 8 and .NET 9. I confirmed it worked on .NET 9, didn't try .NET 8 yet.
There was a problem hiding this comment.
https://learn.microsoft.com/en-us/dotnet/framework/unmanaged-api/profiling/icorprofilerinfo2-dostacksnapshot-method documentation says that the synchronous DoStackSnapshot stackwalk only works from ICorProfilerCallback callbacks. ETL hook is not ICorProfilerCallback callback. I do not think this was ever meant to work.
Fixes #130220
Platform scope
The reported Windows scenario returns
CORPROF_E_STACKSNAPSHOT_UNSAFE, rather than the Linux regression'sE_FAIL. That indicates a separate snapshot-safety boundary and is not changed by this fix. The new context is used only after the existing safety checks allow the walk.Problem
Same-thread
DoStackSnapshotcalls from slow enter, leave, and tailcall profiler hooks no longer have the managed machine state needed to begin a stack walk on Linux.#107152 intentionally removed GC-trigger authorization from ELT hooks, but it also removed the
HelperMethodFramethat supplied the managed-side machine state needed by an unseeded stack walk.Without that frame, an unseeded
DoStackSnapshotcall from an ELT callback has no complete managed instruction pointer, stack pointer, and preserved-register state from which to start walking. On Linux x64, the issue reproduction consequently changed from 320 successful snapshots on the parent commit to 320E_FAILresults after #107152.A counterfactual build restored only the helper frame while leaving GC-trigger authorization removed. All 320 snapshots succeeded, showing that the missing stack-walk state caused the regression.
Fix
When stack snapshots are enabled, the slow ELT helpers now:
Threadfor the callback lifetime.For a same-thread
DoStackSnapshotcall without a profiler-supplied seed, CoreCLR copies that context, virtually unwinds it through the CoreCLR ELT helpers to the first managed frame, and uses the resulting managed context through the existing snapshot-seed path.The ELT context is separate from
m_pProfilerFilterContext, so ordinary runtime and GC stack walks do not observe it. This change does not add a public API, exported ABI, explicit runtime frame, GC poll, or trigger-scope authorization.Safety
ELT callback state remains
COR_PRF_CALLBACKSTATE_INCALLBACK. GC-triggering profiler APIs therefore remain prohibited while the stack snapshot, which isGC_NOTRIGGER, receives the starting state it needs.The profiler regression test verifies that
ForceGCstill returnsCORPROF_E_UNSUPPORTED_CALL_SEQUENCE.Testing
E_FAILon the unfixed runtime.Performance
Native virtual unwinding is deferred until
DoStackSnapshotis called. ELT callbacks capture only the raw context.The relative increase is large because the benchmark uses an intentionally empty hook with a roughly 49 ns baseline. The added absolute cost is approximately 36 ns and is paid only when stack snapshots are enabled.
Linux x64 BenchmarkDotNet results:
The snapshot-enabled path adds approximately 36 ns per callback. The path without the snapshot flag remains within benchmark noise.