-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Runtime-async Exception.ToString() #122722
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?
Conversation
- Add new JIT-EE API to report back debug information about the generated state machine and continuations - Refactor debug info storage on VM side to be more easily extensible. The new format has either a thin or fat header. The fat header is used when we have either uninstrumented bounds, patchpoint info, rich debug info or async debug info, and stores the blob sizes of all of those components in addition to the bounds and vars. - Add new async debug information to the storage on the VM side - Set get target method desc for async resumption stubs, to be used for mapping from continuations back to the async IL function that it will resume.
…mental APIs in async tests
ed25f87 to
d5f6337
Compare
|
Tagging subscribers to this area: @agocke, @MichalStrehovsky, @jkotas |
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.
Pull request overview
This PR implements exception stack trace collection for runtime-async (async v2) methods in both CoreCLR and NativeAOT. The implementation enables proper exception stack traces to include frames from runtime-async async-await chains, providing parity with AsyncV1 exception reporting.
Key changes:
- Adds
ThrowExacthelper that preserves existing stack traces when rethrowing exceptions from continuations by settingExKind.RethrowFlag - Augments stack unwinding to append continuation frames from runtime-async chains using DiagnosticIP
- Updates ILC to emit stack trace records for runtime-async methods while hiding resumption stubs
Reviewed changes
Copilot reviewed 39 out of 39 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/libraries/System.Diagnostics.StackTrace/tests/System.Diagnostics.StackTrace.Tests.csproj | Adds reference to new AsyncAssembly test project |
| src/libraries/System.Diagnostics.StackTrace/tests/StackTraceTests.cs | Adds test infrastructure for validating runtime-async exception stack traces |
| src/libraries/System.Diagnostics.StackTrace/tests/AsyncV1Assembly/Program.cs | New test assembly with AsyncV1 methods for testing V1/V2 interop scenarios |
| src/libraries/System.Diagnostics.StackTrace/tests/AsyncV1Assembly/AsyncV1Assembly.csproj | Project file for AsyncV1 test assembly |
| src/libraries/System.Diagnostics.StackTrace/tests/AsyncAssembly/Program.cs | New test assembly with runtime-async methods that chain with V1 methods |
| src/libraries/System.Diagnostics.StackTrace/tests/AsyncAssembly/AsyncAssembly.csproj | Project file for runtime-async test assembly with preview features enabled |
| src/libraries/Common/tests/TestUtilities/System/PlatformDetection.cs | Adds platform detection for runtime-async support |
| src/coreclr/vm/qcallentrypoints.cpp | Registers new QCall entry point for adding continuations to exceptions |
| src/coreclr/vm/jithelpers.cpp | Modifies IL_ThrowExact to use RethrowFlag instead of marking as foreign exception |
| src/coreclr/vm/exkind.h | Extracts ExKind enum to separate header for reusability |
| src/coreclr/vm/exinfo.h | Removes ExKind definition (moved to exkind.h) |
| src/coreclr/vm/exinfo.cpp | Includes new exkind.h header |
| src/coreclr/vm/exceptionhandling.h | Adds ExKind parameter to DispatchManagedException |
| src/coreclr/vm/exceptionhandling.cpp | Implements ExKind parameter handling in exception dispatch |
| src/coreclr/vm/excep.cpp | Adds STEF_CONTINUATION flag and guards Watson setup for continuation frames |
| src/coreclr/vm/debugdebugger.h | Declares new AsyncHelpers_AddContinuationToExInternal QCall |
| src/coreclr/vm/debugdebugger.cpp | Implements continuation frame appending for exceptions and special offset handling |
| src/coreclr/vm/common.h | Adds required C++ standard library includes |
| src/coreclr/vm/clrex.h | Adds STEF_CONTINUATION flag for stack trace elements |
| src/coreclr/tools/aot/ILCompiler.RyuJit/JitInterface/CorInfoImpl.RyuJit.cs | Maps CORINFO_HELP_THROWEXACT to ReadyToRunHelper.ThrowExact |
| src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/ReadyToRunSignature.cs | Adds ThrowExact helper name for signature parsing |
| src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs | Maps ThrowExact helper for ReadyToRun |
| src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/StackTraceEmissionPolicy.cs | Adds RuntimeAsync visibility flag for async resumption stubs |
| src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/MetadataManager.cs | Emits stack trace records for runtime-async methods and hides resumption stubs |
| src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/JitHelper.cs | Maps ThrowExact to RhpThrowExact runtime helper |
| src/coreclr/tools/Common/TypeSystem/IL/Stubs/AsyncResumptionStub.cs | Exposes TargetMethod property for accessing original async method |
| src/coreclr/tools/Common/Internal/Runtime/ReadyToRunConstants.cs | Adds ThrowExact to ReadyToRunHelper enum |
| src/coreclr/nativeaot/System.Private.CoreLib/src/System/Exception.NativeAot.cs | Adds RH_EH_RUNTIME_ASYNC_FRAME flag and handles runtime-async frames |
| src/coreclr/nativeaot/Runtime/riscv64/ExceptionHandling.S | Implements RhpThrowExact with rethrow flag for RISC-V 64-bit |
| src/coreclr/nativeaot/Runtime/loongarch64/ExceptionHandling.S | Implements RhpThrowExact with rethrow flag for LoongArch 64-bit |
| src/coreclr/nativeaot/Runtime/i386/ExceptionHandling.asm | Implements RhpThrowExact with rethrow flag for x86 |
| src/coreclr/nativeaot/Runtime/arm64/ExceptionHandling.asm | Implements RhpThrowExact with rethrow flag for ARM64 (Windows) |
| src/coreclr/nativeaot/Runtime/arm64/ExceptionHandling.S | Implements RhpThrowExact with rethrow flag for ARM64 (Unix) |
| src/coreclr/nativeaot/Runtime/arm/ExceptionHandling.S | Implements RhpThrowExact with rethrow flag for ARM |
| src/coreclr/nativeaot/Runtime/amd64/ExceptionHandling.asm | Implements RhpThrowExact with rethrow flag for AMD64 (Windows) |
| src/coreclr/nativeaot/Runtime/amd64/ExceptionHandling.S | Implements RhpThrowExact with rethrow flag for AMD64 (Unix) |
| src/coreclr/inc/readytorun.h | Adds READYTORUN_HELPER_ThrowExact constant |
| src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs | Implements continuation stack trace appending during exception unwinding |
| docs/design/coreclr/botr/readytorun-format.md | Documents ThrowExact helper in ReadyToRun format |
| { | ||
| method = ((ILCompiler.AsyncResumptionStub)method).TargetMethod; | ||
| } | ||
| MethodDesc methodToGenerateMetadataFor = method.GetTypicalMethodDefinition(); |
Copilot
AI
Dec 28, 2025
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.
There's an extra space after the equals sign on this line. The formatting should be 'MethodDesc methodToGenerateMetadataFor = method.GetTypicalMethodDefinition();' with a single space on each side of the equals sign, consistent with C# coding conventions and the rest of the codebase.
| MethodDesc methodToGenerateMetadataFor = method.GetTypicalMethodDefinition(); | |
| MethodDesc methodToGenerateMetadataFor = method.GetTypicalMethodDefinition(); |
src/libraries/System.Diagnostics.StackTrace/tests/System.Diagnostics.StackTrace.Tests.csproj
Show resolved
Hide resolved
| { | ||
| IntPtr ip = (IntPtr)continuation.ResumeInfo->DiagnosticIP; | ||
| int flags = (int)RhEHFrameType.RH_EH_RUNTIME_ASYNC_FRAME; | ||
| IntPtr pAppendStackFrame = (IntPtr)InternalCalls.RhpGetClasslibFunctionFromCodeAddress(ip, | ||
| ClassLibFunctionId.AppendExceptionStackFrame); | ||
|
|
||
| if (pAppendStackFrame != IntPtr.Zero) | ||
| { | ||
| try | ||
| { | ||
| ((delegate*<object, IntPtr, int, void>)pAppendStackFrame)(ex, ip, flags); | ||
| } | ||
| catch | ||
| { | ||
| // disallow all exceptions leaking out of callbacks | ||
| } | ||
| } | ||
| } |
Copilot
AI
Dec 28, 2025
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.
The indentation of the opening brace on line 543 and closing brace on line 560 in the #else block for NATIVEAOT is inconsistent with the rest of the code. These braces should be indented to align with the conditional compilation directive context, typically at the same level as the if statement on line 539.
| { | |
| IntPtr ip = (IntPtr)continuation.ResumeInfo->DiagnosticIP; | |
| int flags = (int)RhEHFrameType.RH_EH_RUNTIME_ASYNC_FRAME; | |
| IntPtr pAppendStackFrame = (IntPtr)InternalCalls.RhpGetClasslibFunctionFromCodeAddress(ip, | |
| ClassLibFunctionId.AppendExceptionStackFrame); | |
| if (pAppendStackFrame != IntPtr.Zero) | |
| { | |
| try | |
| { | |
| ((delegate*<object, IntPtr, int, void>)pAppendStackFrame)(ex, ip, flags); | |
| } | |
| catch | |
| { | |
| // disallow all exceptions leaking out of callbacks | |
| } | |
| } | |
| } | |
| { | |
| IntPtr ip = (IntPtr)continuation.ResumeInfo->DiagnosticIP; | |
| int flags = (int)RhEHFrameType.RH_EH_RUNTIME_ASYNC_FRAME; | |
| IntPtr pAppendStackFrame = (IntPtr)InternalCalls.RhpGetClasslibFunctionFromCodeAddress(ip, | |
| ClassLibFunctionId.AppendExceptionStackFrame); | |
| if (pAppendStackFrame != IntPtr.Zero) | |
| { | |
| try | |
| { | |
| ((delegate*<object, IntPtr, int, void>)pAppendStackFrame)(ex, ip, flags); | |
| } | |
| catch | |
| { | |
| // disallow all exceptions leaking out of callbacks | |
| } | |
| } | |
| } |
| @@ -0,0 +1,25 @@ | |||
| // Licensed to the .NET Foundation under one or more agreements. | |||
| // The .NET Foundation licenses this file to you under the MIT license. | |||
| // RuntimeExceptionKind.h | |||
Copilot
AI
Dec 28, 2025
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.
The comment on line 3 refers to "RuntimeExceptionKind.h" but the actual file is named "exkind.h". This is likely a copy-paste artifact from when the enum was extracted from exinfo.h. The comment should be updated to match the actual filename.
| // RuntimeExceptionKind.h | |
| // exkind.h |
| if (continuation != null && continuation.ResumeInfo != null && continuation.ResumeInfo->DiagnosticIP != null) | ||
| #if !NATIVEAOT | ||
| AddContinuationToExInternal(continuation.ResumeInfo->DiagnosticIP, ex); | ||
| #else | ||
| { | ||
| IntPtr ip = (IntPtr)continuation.ResumeInfo->DiagnosticIP; | ||
| int flags = (int)RhEHFrameType.RH_EH_RUNTIME_ASYNC_FRAME; | ||
| IntPtr pAppendStackFrame = (IntPtr)InternalCalls.RhpGetClasslibFunctionFromCodeAddress(ip, | ||
| ClassLibFunctionId.AppendExceptionStackFrame); |
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'm curious what these changes are necessary for. Isn't the original async stackwalk when the exception was first thrown sufficient?
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.
We are catching any exception in DispatchContinuations and unwinding the continuation chain here until we find a continuation that resumes inside a try block; this does not go through the normal exception unwind stack walk so would not update the stack trace here.
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.
The code in DispatchContinuations handles propagating exceptions thrown in one async frame to the next async frame, but it is not the only time I would expect us to want a nice async stack trace. For example, you can imagine code like:
async Task Foo()
{
await Task.Yield();
await Bar();
}
async Task Bar()
{
await Task.Yield();
try
{
SyncCallThatThrows();
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}The exception propagation in DispatchContinuations is not going to be called for the exception thrown by SyncCallThatThrows, but I still think we would want a stack trace that shows Foo -> Bar -> SyncCallThatThrows(). So I think we would want the async stack walking integrated at a slightly lower level, and if we do that I wouldn't expect the DispatchContinuations handling to be necessary.
MichalStrehovsky
left a comment
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.
| writer.AdditionalRootRecords.Add(record.MethodName); | ||
| writer.AdditionalRootRecords.Add(record.MethodSignature); | ||
| writer.AdditionalRootRecords.Add(record.MethodInstantiationArgumentCollection); | ||
| // hide the resumption stubs |
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.
If we have a runtime-async method, we potentially have 3 method bodies:
- The task-callable variant of the method (this is the variant of the method that doesn't have async calling convention and simply returns a Task)
- The async-callable variant of the method (this is the actual method body; needs to be called with asynccall calling convention)
- The resumption thunk that calls the async-callable variant
Before this PR, since we don't special case async in any way right now, I assume we're generating stack trace metadata for the first flavor and no metadata for the others (not even instructions to hide them from StackTrace.ToString).
With what's in this PR, we're still going to generate metadata for first flavor, then we're going to generate a hidden stack trace record for the resumption thunk and a record for the async-callable flavor, but only if there was a resumption thunk.
I'm not sure if this is what we want (we definitely do not want the "only if there was a resumption thunk" part, and I'm not sure we want to see the Task-callable thunk either).
I think what we want instead of the changes in this file is:
- Update
EcmaMethodStackTraceEmissionPolicy.GetMethodVisibilityto check for the resumption thunk case. If it's a resumption thunk, returnMethodStackTraceVisibilityFlags.IsHidden - In the same method, add special casing when
method.IsAsyncistrue: make sure that ifmethod.IsAsyncis true andmethod.IsAsyncCall()is false (i.e. this is the Task-callable thunk), report.IsHidden. If bothmethod.IsAsyncandmethod.IsAsyncCall()is true, report.HasMetadata.
This should ensure that flavor 1 is hidden, flavor 2 is visible, and flavor 3 is hidden.
I assume that's what we want.
|
|
||
| public static class RuntimeAsyncAndRemoteExecutor | ||
| { | ||
| public static bool IsSupported => RemoteExecutor.IsSupported && PlatformDetection.IsRuntimeAsyncSupported; |
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.
RemoteExecutor.IsSupported is false for native AOT so this test will be currently skipped on native AOT (and some other platforms). Is RemoteExecutor still needed? I believe as of #121732 it should no longer be needed.
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 believe we would be able to remove the assemblies as per your suggestions; do you think this is the best place to put these tests?
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.
do you think this is the best place to put these tests?
Yes, I think so.
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.
We should also add a test where we have the Task-returning variant of the method on the stack. It will be on the stack if a runtime-async method is not awaited (i.e. we take the Task that was returned from it and e.g. store it in a static field).
| ContinueOnCapturedTaskScheduler = 64, | ||
| } | ||
|
|
||
| internal enum RhEHFrameType |
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.
This should not be needed if you take my other feedback.
| { | ||
| IntPtr ip = (IntPtr)continuation.ResumeInfo->DiagnosticIP; | ||
| int flags = (int)RhEHFrameType.RH_EH_RUNTIME_ASYNC_FRAME; | ||
| IntPtr pAppendStackFrame = (IntPtr)InternalCalls.RhpGetClasslibFunctionFromCodeAddress(ip, |
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.
This doesn't need to go through the RhpGetClasslibFunctionFromCodeAddress indirection. That requirement to go through indirection only applies to code under Runtime.Base subdirectory (that is not technically in Corelib, but we compile it as such right now).
RhpGetClasslibFunctionFromCodeAddress will eventually return the address of Exception.AppendExceptionStackFrame (defined in Exception.NativeAot.cs). This can call Exception.AppendExceptionStackFrame directly. Or better yet, add a new method to Exception.NativeAot.cs (AppendContinuationStackFrame?) that doesn't require passing flags and call that so that we don't have to deal with RhEHFrameType here.
|
|
||
| <PropertyGroup> | ||
| <OutputType>Library</OutputType> | ||
| <TargetFramework>net11.0</TargetFramework> |
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.
Either do this, or target $(NetCoreAppCurrent) and leverage RuntimeAsyncMethodGenerationAttribute attributes to selectively force disable runtime async (then we don't even need the extra assembly). The latter is what we do in src/tests/async.
| <TargetFramework>net11.0</TargetFramework> | |
| <!-- We need a TFM that doesn't support runtime-async! --> | |
| <TargetFramework>netstandard2.0</TargetFramework> |
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net11.0</TargetFramework> |
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.
Targeting hardcoded TFMs gets complicated a couple years down the line because a few years from now this is going to generate warnings about unsupported frameworks and there are other engineering challenges as well.
| <TargetFramework>net11.0</TargetFramework> | |
| <TargetFramework>$(NetCoreAppCurrent)</TargetFramework> |
| } | ||
| } | ||
|
|
||
| public static bool IsRuntimeAsyncSupported => !IsCoreClrInterpreter && !IsMonoRuntime && !IsMonoAOT && !IsMonoInterpreter; |
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.
This is incorrect. The coreclr interpreter DOES support runtime async, and any issues you encounter should be fixed as you work, or issues should be filed. (For async stuff, please tag me in particular).
|
|
||
| #ifdef FEATURE_EH_FUNCLETS | ||
| DispatchManagedException(oref, exceptionFrame.GetContext()); | ||
| DispatchManagedException(oref, exceptionFrame.GetContext(), NULL, ExKind::RethrowFlag); |
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.
runtime/src/coreclr/vm/interpexec.cpp
Line 4127 in 841eb85
| GetThread()->GetExceptionState()->SetRaisingForeignException(); |
This PR implements exception stack trace collection in runtime-async stacks for both coreclr and nativeAOT.
Augmenting stack unwinding logic in AsyncHelpers.CoreCLR.cs to append frames from runtime-async async-await chain to exception stacktrace using DiagnosticIP of Continuation.
Implements ThrowExact helper to throw exception that has been stored in Continuation, while keeping its existing stack trace intact. Accomplished by creating throw helpers that set ExKind.RethrowFlag. Would be open to rename "RethrowFlag" and companions to "NoEraseFlag" or something similar, or leave as is.
ILC changes: Ensuring that stack trace records are emitted for runtime-async methods, and hidden for resumption stubs.
Testing: Would like input on where to put them. The current test location seems to be more about constructor and other basic tests. Does naot tests in src\tests\nativeaot\SmokeTests\UnitTests\StackTraces.cs and coreclr tests in src\tests\baseservices\exceptions\exceptionstacktrace\exceptionstacktrace.cs make sense?
There are some line attribution errors in native AOT; I believe these can be fixed by emitting additional sequence points.
Most of the test programs test some combination of runtime-async and asyncv1 chaining. Here are other test programs where there is parity between runtime-async and asyncv1 exception strings.
@dotnet/ilc-contrib