Skip to content

[Profiler] Fix DoStackSnapshot calls in ELT hooks - #132301

Open
mdh1418 wants to merge 2 commits into
dotnet:mainfrom
mdh1418:fix-elt-stack-snapshots
Open

[Profiler] Fix DoStackSnapshot calls in ELT hooks#132301
mdh1418 wants to merge 2 commits into
dotnet:mainfrom
mdh1418:fix-elt-stack-snapshots

Conversation

@mdh1418

@mdh1418 mdh1418 commented Aug 14, 2026

Copy link
Copy Markdown
Member

Fixes #130220

Platform scope

The reported Windows scenario returns CORPROF_E_STACKSNAPSHOT_UNSAFE, rather than the Linux regression's E_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 DoStackSnapshot calls 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 HelperMethodFrame that supplied the managed-side machine state needed by an unseeded stack walk.

Without that frame, an unseeded DoStackSnapshot call 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 320 E_FAIL results 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:

  1. Capture a native context before invoking arbitrary profiler code.
  2. Publish it on the current Thread for the callback lifetime.
  3. Restore the previous context after the callback, including for nested ELT callbacks.

For a same-thread DoStackSnapshot call 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 is GC_NOTRIGGER, receives the starting state it needs.

The profiler regression test verifies that ForceGC still returns CORPROF_E_UNSUPPORTED_CALL_SEQUENCE.

Testing

  • Checked CoreCLR build.
  • Slow-path ELT enter and leave profiler tests.
  • Demonstrated that the enter test fails with E_FAIL on the unfixed runtime.
  • Original issue harness: ten runs with 32 of 32 successful snapshots per run, for 320 successes and no failures.

Performance

Native virtual unwinding is deferred until DoStackSnapshot is 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:

Configuration Baseline Change Ratio
Slow ELT with stack-snapshot flag 48.53 ns 84.33 ns 1.74x
Slow ELT without stack-snapshot flag 47.65 ns 48.59 ns 1.02x

The snapshot-enabled path adds approximately 36 ns per callback. The path without the snapshot flag remains within benchmark noise.

mdh1418 and others added 2 commits August 14, 2026 01:46
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@azure-pipelines

Copy link
Copy Markdown
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.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 DoStackSnapshot to 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 DoStackSnapshot success (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.

Comment on lines +8722 to +8731
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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@jkotas jkotas Aug 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you have some benchmark results in the PR description. What was the benchmark source?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

@jkotas jkotas Aug 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@jkotas jkotas Aug 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@mdh1418 mdh1418 Aug 14, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DoStackSnapshot fails on .NET 10 when invoked from ELT hook

3 participants