diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/Augments/StackTraceMetadataCallbacks.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/Augments/StackTraceMetadataCallbacks.cs index 32031ecab7c892..c423404ac4c330 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/Augments/StackTraceMetadataCallbacks.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/Augments/StackTraceMetadataCallbacks.cs @@ -17,7 +17,7 @@ namespace Internal.Runtime.Augments [CLSCompliant(false)] public abstract class StackTraceMetadataCallbacks { - public abstract string TryGetMethodStackFrameInfo(IntPtr methodStartAddress, int offset, bool needsFileInfo, out string owningType, out string genericArgs, out string methodSignature, out bool isStackTraceHidden, out string fileName, out int lineNumber); + public abstract string TryGetMethodStackFrameInfo(IntPtr methodStartAddress, int offset, bool needsFileInfo, out string owningType, out string genericArgs, out string methodSignature, out bool isStackTraceHidden, out bool isAsync, out string fileName, out int lineNumber); public abstract DiagnosticMethodInfo TryGetDiagnosticMethodInfoFromStartAddress(IntPtr methodStartAddress); } diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/StackFrame.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/StackFrame.NativeAot.cs index 71f3eb24181068..d66c20ea5aec51 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/StackFrame.NativeAot.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Diagnostics/StackFrame.NativeAot.cs @@ -40,6 +40,13 @@ public partial class StackFrame private bool _isStackTraceHidden; + /// + /// Will be true if this frame corresponds to a runtime-async method or the MoveNext method of a + /// compiler-generated async state machine. Used to suppress the "--- End of stack trace from + /// previous location ---" delimiter. + /// + private bool _isAsync; + // If stack trace metadata is available, _methodOwningType is the namespace-qualified name of the owning type, // _methodName is the name of the method, _methodGenericArgs are generic arguments, and _methodSignature is the list of parameters // without braces. StackTrace will format this as `{_methodOwningType}.{_methodName}<{_genericArgs}>({_methodSignature}). @@ -122,7 +129,7 @@ private void InitializeForIpAddress(IntPtr ipAddress, bool needFileInfo) StackTraceMetadataCallbacks stackTraceCallbacks = RuntimeAugments.StackTraceCallbacksIfAvailable; if (stackTraceCallbacks != null) { - _methodName = stackTraceCallbacks.TryGetMethodStackFrameInfo(methodStartAddress, _nativeOffset, needFileInfo, out _methodOwningType, out _methodGenericArgs, out _methodSignature, out _isStackTraceHidden, out _fileName, out _lineNumber); + _methodName = stackTraceCallbacks.TryGetMethodStackFrameInfo(methodStartAddress, _nativeOffset, needFileInfo, out _methodOwningType, out _methodGenericArgs, out _methodSignature, out _isStackTraceHidden, out _isAsync, out _fileName, out _lineNumber); } if (_methodName == null) @@ -230,7 +237,7 @@ internal void AppendToStackTrace(StringBuilder builder) builder.AppendLine(); } } - if (_isLastFrameFromForeignExceptionStackTrace) + if (_isLastFrameFromForeignExceptionStackTrace && !_isAsync) { // Passing default for Exception_EndStackTraceFromPreviousThrow in case SR.UsingResourceKeys is set. builder.AppendLine(SR.UsingResourceKeys() ? diff --git a/src/coreclr/nativeaot/System.Private.StackTraceMetadata/src/Internal/StackTraceMetadata/StackTraceMetadata.cs b/src/coreclr/nativeaot/System.Private.StackTraceMetadata/src/Internal/StackTraceMetadata/StackTraceMetadata.cs index b845aaac54a3f0..b67e691bee2442 100644 --- a/src/coreclr/nativeaot/System.Private.StackTraceMetadata/src/Internal/StackTraceMetadata/StackTraceMetadata.cs +++ b/src/coreclr/nativeaot/System.Private.StackTraceMetadata/src/Internal/StackTraceMetadata/StackTraceMetadata.cs @@ -50,7 +50,7 @@ internal static void Initialize() /// /// Locate the containing module for a method and try to resolve its name based on start address. /// - public static unsafe string GetMethodNameFromStartAddressIfAvailable(IntPtr methodStartAddress, out string owningTypeName, out string genericArgs, out string methodSignature, out bool isStackTraceHidden, out int hashCodeForLineInfo) + public static unsafe string GetMethodNameFromStartAddressIfAvailable(IntPtr methodStartAddress, out string owningTypeName, out string genericArgs, out string methodSignature, out bool isStackTraceHidden, out bool isAsync, out int hashCodeForLineInfo) { IntPtr moduleStartAddress = RuntimeAugments.GetOSModuleFromPointer(methodStartAddress); int rva = (int)((byte*)methodStartAddress - (byte*)moduleStartAddress); @@ -62,6 +62,7 @@ public static unsafe string GetMethodNameFromStartAddressIfAvailable(IntPtr meth if (resolver.TryGetStackTraceData(rva, out var data)) { isStackTraceHidden = data.IsHidden; + isAsync = data.IsAsync; if (data.OwningType.IsNil) { Debug.Assert(data.Name.IsNil && data.Signature.IsNil); @@ -80,6 +81,7 @@ public static unsafe string GetMethodNameFromStartAddressIfAvailable(IntPtr meth } isStackTraceHidden = false; + isAsync = false; // We haven't found information in the stack trace metadata tables, but maybe reflection will have this if (ReflectionExecution.TryGetMethodMetadataFromStartAddress(methodStartAddress, @@ -331,9 +333,9 @@ public override DiagnosticMethodInfo TryGetDiagnosticMethodInfoFromStartAddress( return GetDiagnosticMethodInfoFromStartAddressIfAvailable(methodStartAddress); } - public override string TryGetMethodStackFrameInfo(IntPtr methodStartAddress, int offset, bool needsFileInfo, out string owningType, out string genericArgs, out string methodSignature, out bool isStackTraceHidden, out string fileName, out int lineNumber) + public override string TryGetMethodStackFrameInfo(IntPtr methodStartAddress, int offset, bool needsFileInfo, out string owningType, out string genericArgs, out string methodSignature, out bool isStackTraceHidden, out bool isAsync, out string fileName, out int lineNumber) { - string methodName = GetMethodNameFromStartAddressIfAvailable(methodStartAddress, out owningType, out genericArgs, out methodSignature, out isStackTraceHidden, out int hashCode); + string methodName = GetMethodNameFromStartAddressIfAvailable(methodStartAddress, out owningType, out genericArgs, out methodSignature, out isStackTraceHidden, out isAsync, out int hashCode); if (needsFileInfo) { @@ -474,6 +476,7 @@ private unsafe void PopulateRvaToTokenMap(TypeManagerHandle handle, byte* pMap, { Rva = methodRva, IsHidden = (command & StackTraceDataCommand.IsStackTraceHidden) != 0, + IsAsync = (command & StackTraceDataCommand.IsAsync) != 0, OwningType = currentOwningType, Name = currentName, Signature = currentSignature, @@ -516,25 +519,36 @@ public bool TryGetStackTraceData(int rva, out StackTraceData data) public struct StackTraceData : IComparable { private const int IsHiddenFlag = 0x2; + private const int IsAsyncFlag = 0x1; + private const int FlagsMask = IsHiddenFlag | IsAsyncFlag; - private readonly int _rvaAndIsHiddenBit; + private readonly int _rvaAndFlags; public int Rva { - get => _rvaAndIsHiddenBit & ~IsHiddenFlag; + get => _rvaAndFlags & ~FlagsMask; init { - Debug.Assert((value & IsHiddenFlag) == 0); - _rvaAndIsHiddenBit = value | (_rvaAndIsHiddenBit & IsHiddenFlag); + Debug.Assert((value & FlagsMask) == 0); + _rvaAndFlags = value | (_rvaAndFlags & FlagsMask); } } public bool IsHidden { - get => (_rvaAndIsHiddenBit & IsHiddenFlag) != 0; + get => (_rvaAndFlags & IsHiddenFlag) != 0; init { if (value) - _rvaAndIsHiddenBit |= IsHiddenFlag; + _rvaAndFlags |= IsHiddenFlag; + } + } + public bool IsAsync + { + get => (_rvaAndFlags & IsAsyncFlag) != 0; + init + { + if (value) + _rvaAndFlags |= IsAsyncFlag; } } public Handle OwningType { get; init; } diff --git a/src/coreclr/tools/Common/Internal/Runtime/StackTraceData.cs b/src/coreclr/tools/Common/Internal/Runtime/StackTraceData.cs index 46fb51450a9d81..ad344da31af07c 100644 --- a/src/coreclr/tools/Common/Internal/Runtime/StackTraceData.cs +++ b/src/coreclr/tools/Common/Internal/Runtime/StackTraceData.cs @@ -11,5 +11,6 @@ internal static class StackTraceDataCommand public const byte UpdateGenericSignature = 0x08; // Just a shortcut - sig metadata has the info public const byte IsStackTraceHidden = 0x10; + public const byte IsAsync = 0x20; } } diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/StackTraceMethodMappingNode.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/StackTraceMethodMappingNode.cs index 2e94b1cddcf8a8..4b83c3f0cacc04 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/StackTraceMethodMappingNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/StackTraceMethodMappingNode.cs @@ -124,6 +124,11 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) command |= StackTraceDataCommand.IsStackTraceHidden; } + if ((entry.Flags & StackTraceRecordFlags.IsAsync) != 0) + { + command |= StackTraceDataCommand.IsAsync; + } + objData.EmitByte(commandReservation, command); objData.EmitReloc(factory.MethodEntrypoint(entry.Method), RelocType.IMAGE_REL_BASED_RELPTR32); } diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/MetadataManager.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/MetadataManager.cs index 6d99ee84ba317c..ac310a8a51e583 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/MetadataManager.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/MetadataManager.cs @@ -780,6 +780,8 @@ protected void ComputeMetadata( flags |= StackTraceRecordFlags.IsHidden; if ((stackVisibility & MethodStackTraceVisibilityFlags.HasLineNumbers) != 0) flags |= StackTraceRecordFlags.HasLineNumbers; + if ((stackVisibility & MethodStackTraceVisibilityFlags.IsAsync) != 0) + flags |= StackTraceRecordFlags.IsAsync; if ((stackVisibility & MethodStackTraceVisibilityFlags.HasMetadata) != 0) { @@ -1351,6 +1353,7 @@ public enum StackTraceRecordFlags None = 0, IsHidden = 1, HasLineNumbers = 2, + IsAsync = 4, } public readonly struct StackTraceRecordData diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/StackTraceEmissionPolicy.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/StackTraceEmissionPolicy.cs index 67576210ec50ab..78e74b35d9c2bd 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/StackTraceEmissionPolicy.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/StackTraceEmissionPolicy.cs @@ -29,6 +29,8 @@ public override MethodStackTraceVisibilityFlags GetMethodVisibility(MethodDesc m public class EcmaMethodStackTraceEmissionPolicy : StackTraceEmissionPolicy { private readonly MethodStackTraceVisibilityFlags _flags; + private MetadataType _iAsyncStateMachineType; + private bool _iAsyncStateMachineTypeComputed; public EcmaMethodStackTraceEmissionPolicy(bool includeLineNumbers) { @@ -46,10 +48,54 @@ public override MethodStackTraceVisibilityFlags GetMethodVisibility(MethodDesc m result |= MethodStackTraceVisibilityFlags.IsHidden; } + if (IsAsyncFrame(method)) + { + result |= MethodStackTraceVisibilityFlags.IsAsync; + } + return (method.GetTypicalMethodDefinition() is Internal.TypeSystem.Ecma.EcmaMethod || (method.IsAsync && method.IsAsyncCall())) ? result | MethodStackTraceVisibilityFlags.HasMetadata : result; } + + // Determines whether a frame for this method should be treated as "async" when formatting a + // stack trace. Async frames suppress the "--- End of stack trace from previous location ---" + // delimiter. This covers both runtime-async (V2) methods and the MoveNext method of a + // compiler-generated (V1) async state machine. + private bool IsAsyncFrame(MethodDesc method) + { + if (method.IsAsync) + { + return true; + } + + // Async state machines only expose their exception-throwing code through IAsyncStateMachine.MoveNext. + if (method.Name != "MoveNext"u8) + { + return false; + } + + if (!_iAsyncStateMachineTypeComputed) + { + _iAsyncStateMachineType = method.Context.SystemModule.GetType("System.Runtime.CompilerServices"u8, "IAsyncStateMachine"u8, throwIfNotFound: false); + _iAsyncStateMachineTypeComputed = true; + } + + if (_iAsyncStateMachineType == null) + { + return false; + } + + foreach (DefType interfaceType in method.OwningType.RuntimeInterfaces) + { + if (interfaceType == _iAsyncStateMachineType) + { + return true; + } + } + + return false; + } } [Flags] @@ -58,5 +104,6 @@ public enum MethodStackTraceVisibilityFlags HasMetadata = 0x1, IsHidden = 0x2, HasLineNumbers = 0x4, + IsAsync = 0x8, } } diff --git a/src/libraries/System.Diagnostics.StackTrace/tests/StackTraceTests.cs b/src/libraries/System.Diagnostics.StackTrace/tests/StackTraceTests.cs index 687392cf998a58..08c97fe5e1ca87 100644 --- a/src/libraries/System.Diagnostics.StackTrace/tests/StackTraceTests.cs +++ b/src/libraries/System.Diagnostics.StackTrace/tests/StackTraceTests.cs @@ -709,11 +709,7 @@ public async Task ToString_Async(Func asyncMethod, string[] expectedPatter startIndex = match.Index + match.Length; } - // [ActiveIssue("https://github.com/dotnet/runtime/issues/129155", typeof(PlatformDetection), nameof(PlatformDetection.IsNativeAot))] - if (!PlatformDetection.IsNativeAot) - { - Assert.DoesNotContain("--- End of stack trace from previous location ---", exceptionText); - } + Assert.DoesNotContain("--- End of stack trace from previous location ---", exceptionText); } [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]