diff --git a/src/coreclr/System.Private.CoreLib/src/System/RuntimeHandles.cs b/src/coreclr/System.Private.CoreLib/src/System/RuntimeHandles.cs index 7fc5dc96f17b0f..1b7134247e61c5 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/RuntimeHandles.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/RuntimeHandles.cs @@ -1292,6 +1292,16 @@ static RuntimeMethodHandleInternal GetStubIfNeededWorker(RuntimeMethodHandleInte [MethodImpl(MethodImplOptions.InternalCall)] internal static extern RuntimeMethodHandleInternal GetMethodFromCanonical(RuntimeMethodHandleInternal method, RuntimeType declaringType); + [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "RuntimeMethodHandle_GetNativeCode")] + private static partial IntPtr GetNativeCode(RuntimeMethodHandleInternal method); + + internal static IntPtr GetNativeCodeInternal(IRuntimeMethodInfo method) + { + IntPtr value = GetNativeCode(method.Value); + GC.KeepAlive(method); + return value; + } + [MethodImpl(MethodImplOptions.InternalCall)] internal static extern bool IsGenericMethodDefinition(RuntimeMethodHandleInternal method); diff --git a/src/coreclr/vm/qcallentrypoints.cpp b/src/coreclr/vm/qcallentrypoints.cpp index f3cc5ef9ab1ebd..6f2dcf3af63bb7 100644 --- a/src/coreclr/vm/qcallentrypoints.cpp +++ b/src/coreclr/vm/qcallentrypoints.cpp @@ -176,6 +176,7 @@ static const Entry s_QCall[] = DllImportEntry(RuntimeMethodHandle_IsCAVisibleFromDecoratedType) DllImportEntry(RuntimeMethodHandle_Destroy) DllImportEntry(RuntimeMethodHandle_GetStubIfNeededSlow) + DllImportEntry(RuntimeMethodHandle_GetNativeCode) DllImportEntry(RuntimeMethodHandle_GetMethodBody) DllImportEntry(RuntimeModule_GetScopeName) DllImportEntry(RuntimeModule_GetFullyQualifiedName) diff --git a/src/coreclr/vm/runtimehandles.cpp b/src/coreclr/vm/runtimehandles.cpp index 1e1fb0c0222cae..30f7bc42853377 100644 --- a/src/coreclr/vm/runtimehandles.cpp +++ b/src/coreclr/vm/runtimehandles.cpp @@ -2005,6 +2005,33 @@ FCIMPL2(MethodDesc*, RuntimeMethodHandle::GetMethodFromCanonical, MethodDesc *pM } FCIMPLEND +extern "C" PCODE QCALLTYPE RuntimeMethodHandle_GetNativeCode(MethodDesc* pMethod) +{ + QCALL_CONTRACT; + + PCODE result = (PCODE)NULL; + + BEGIN_QCALL; + + _ASSERTE(pMethod != NULL); + + while (pMethod->IsWrapperStub()) + { + MethodDesc* pWrapped = pMethod->GetWrappedMethodDesc(); + if (pWrapped == NULL || pWrapped == pMethod) + { + break; + } + pMethod = pWrapped; + } + + result = GetInterpreterCodeFromEntryPointIfPresent(pMethod->GetNativeCodeAnyVersion()); + + END_QCALL; + + return result; +} + extern "C" void QCALLTYPE RuntimeMethodHandle_GetMethodBody(MethodDesc* pMethod, QCall::TypeHandle pDeclaringType, QCall::ObjectHandleOnStack result) { QCALL_CONTRACT; diff --git a/src/coreclr/vm/runtimehandles.h b/src/coreclr/vm/runtimehandles.h index 855e414a457a6d..1ac8932778ef8a 100644 --- a/src/coreclr/vm/runtimehandles.h +++ b/src/coreclr/vm/runtimehandles.h @@ -237,6 +237,7 @@ extern "C" void QCALLTYPE RuntimeMethodHandle_GetTypicalMethodDefinition(MethodD extern "C" void QCALLTYPE RuntimeMethodHandle_StripMethodInstantiation(MethodDesc * pMethod, QCall::ObjectHandleOnStack refMethod); extern "C" void QCALLTYPE RuntimeMethodHandle_Destroy(MethodDesc * pMethod); extern "C" MethodDesc* QCALLTYPE RuntimeMethodHandle_GetStubIfNeededSlow(MethodDesc* pMethod, QCall::TypeHandle declaringTypeHandle, QCall::ObjectHandleOnStack methodInstantiation); +extern "C" PCODE QCALLTYPE RuntimeMethodHandle_GetNativeCode(MethodDesc* pMethod); extern "C" void QCALLTYPE RuntimeMethodHandle_GetMethodBody(MethodDesc* pMethod, QCall::TypeHandle pDeclaringType, QCall::ObjectHandleOnStack result); class RuntimeFieldHandle diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs index c646443c910719..bfe2002bee9a77 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncProfiler.cs @@ -1230,7 +1230,7 @@ internal static partial class ContinuationWrapper #if !RUNTIME_ASYNC_SUPPORTED public static void InitInfo(ref Info info) { - info.ContinuationTable = 0; + info.ContinuationTable = ref Unsafe.NullRef(); info.ContinuationIndex = 0; } #endif diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncStateMachineDiagnostics.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncStateMachineDiagnostics.cs index 59f5e2c481c7d0..08c454c9d81977 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncStateMachineDiagnostics.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncStateMachineDiagnostics.cs @@ -67,7 +67,14 @@ private static ulong ResolveMethodId() MethodInfo? methodInfo = typeof(TStateMachine).GetMethod("MoveNext", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (methodInfo is not null) { - return (ulong)methodInfo.MethodHandle.Value; +#if MONO + return (ulong)(nuint)RuntimeMethodHandle.GetNativeCodeInternal(methodInfo.MethodHandle.Value); +#else + if (methodInfo is IRuntimeMethodInfo runtimeMethodInfo) + { + return (ulong)(nuint)RuntimeMethodHandle.GetNativeCodeInternal(runtimeMethodInfo); + } +#endif } return 0; diff --git a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs index 1ebe969e4ca011..5cb822508f39f0 100644 --- a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs +++ b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerTests.cs @@ -112,20 +112,26 @@ public partial class AsyncProfilerTests // These tests use async Task methods with await instead of Task.Run blocking. public static bool IsRuntimeAsyncSupported => PlatformDetection.IsRuntimeAsyncSupported; - // StateMachine (StateMachineAsync_*) async-task instrumentation is opt-out on NativeAOT to avoid - // ~100KB of per-state-machine generic instantiation overhead. Tests that depend - // on StateMachine events must be gated on these properties so they are skipped on NAOT. + // StateMachine (StateMachineAsync_*) async-task instrumentation is the classic compiler-generated + // state machine (V1). It does not require runtime-async (V2), so it is supported on every runtime + // (CoreCLR and Mono) except NativeAOT, where it is opt-out to avoid ~100KB of per-state-machine + // generic instantiation overhead. Tests that depend on StateMachine events must be gated on these + // properties so they are skipped on NAOT. public static bool IsStateMachineAsyncSupported => - IsRuntimeAsyncSupported && !PlatformDetection.IsNativeAot; + !PlatformDetection.IsNativeAot; public static bool IsStateMachineAsyncAndThreadingSupported => - IsRuntimeAsyncAndThreadingSupported && !PlatformDetection.IsNativeAot; + IsStateMachineAsyncSupported && PlatformDetection.IsMultithreadingSupported; // Gate for tests that exercise a mixed V1 (StateMachine) + V2 (RuntimeAsync) chain: they need both // instrumentation paths, and V1 is disabled on NativeAOT, so require both conditions. public static bool IsStateMachineAsyncAndRuntimeAsyncAndThreadingSupported => IsStateMachineAsyncAndThreadingSupported && IsRuntimeAsyncAndThreadingSupported; + // Alias so tests that additionally require CoreCLR can list the exclusion as an extra ConditionalFact + // condition alongside the shared gates. + public static bool IsNotMonoRuntime => PlatformDetection.IsNotMonoRuntime; + private const string AsyncProfilerEventSourceName = "System.Runtime.CompilerServices.AsyncProfilerEventSource"; private const string WrapperNameTemplate = "Continuation_Wrapper_{0}"; private static readonly string WrapperNamePrefix = WrapperNameTemplate.Substring(0, WrapperNameTemplate.IndexOf("{0}", StringComparison.Ordinal)); @@ -229,109 +235,199 @@ s_getMethodFromNativeIPMethod is null ? typeof(StackFrame).GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, new[] { typeof(IntPtr), typeof(bool) }, null) : null; - // The public 1-arg MethodBase.GetMethodFromHandle resolves the method but then deliberately - // throws ArgumentException when the declaring type is generic. The internal - // RuntimeType.GetMethodBase(RuntimeMethodHandle.GetMethodInfo()) it calls underneath has no - // such guard, so we invoke it directly to name StateMachine frames on generic declaring types. - private static readonly MethodInfo? s_runtimeMethodHandleGetMethodInfo = - typeof(RuntimeMethodHandle).GetMethod("GetMethodInfo", BindingFlags.Instance | BindingFlags.NonPublic); - - private static readonly MethodInfo? s_runtimeTypeGetMethodBase = - // typeof(object) is a RuntimeType instance; its runtime type is System.RuntimeType. - typeof(object).GetType().GetMethods(BindingFlags.Static | BindingFlags.NonPublic) - .FirstOrDefault(m => m.Name == "GetMethodBase" - && m.GetParameters() is { Length: 1 } p - && p[0].ParameterType.Name == "IRuntimeMethodInfo"); - private static string? GetMethodNameFromMethodId(AsyncCallstackType callstackType, ulong methodId) { if (methodId != 0) { - if (callstackType == AsyncCallstackType.Runtime) + if (s_getMethodFromNativeIPMethod is not null) { - if (s_getMethodFromNativeIPMethod is not null) + MethodBase? method = (MethodBase?)s_getMethodFromNativeIPMethod.Invoke(null, new object[] { (IntPtr)methodId }); + if (callstackType == AsyncCallstackType.Runtime) { - MethodBase? method = (MethodBase?)s_getMethodFromNativeIPMethod.Invoke(null, new object[] { (IntPtr)methodId }); return method?.Name; } - - if (s_stackFrameFromIPCtor is not null) + else { - StackFrame frame = (StackFrame)s_stackFrameFromIPCtor.Invoke(new object[] { (IntPtr)methodId, false })!; - DiagnosticMethodInfo? diagInfo = DiagnosticMethodInfo.Create(frame); - return diagInfo?.Name; + return ExtractStateMachineMethodName(method?.DeclaringType?.Name); } } - else if (callstackType == AsyncCallstackType.StateMachine) + + if (s_stackFrameFromIPCtor is not null) { - System.RuntimeMethodHandle handle = RuntimeMethodHandle.FromIntPtr((IntPtr)methodId); - MethodBase? method = null; - try + StackFrame frame = (StackFrame)s_stackFrameFromIPCtor.Invoke(new object[] { (IntPtr)methodId, false })!; + DiagnosticMethodInfo? diagInfo = DiagnosticMethodInfo.Create(frame); + if (callstackType == AsyncCallstackType.Runtime) { - method = MethodBase.GetMethodFromHandle(handle); + return diagInfo?.Name; } - catch (ArgumentException) + else { - // The 1-arg GetMethodFromHandle cannot resolve handles whose declaring - // type is generic (e.g. xUnit's TestClassRunner+<...>d__N); - // it requires the 2-arg overload with an explicit declaring type, which - // we cannot recover from a bare MethodId. Real ETW/EventPipe consumers - // get the declaring type from method-metadata rundown events. - // - // As a test-only fallback, resolve via the internal - // RuntimeType.GetMethodBase the public API uses before its generic guard, - // which does name methods on generic declaring types. This mirrors what a - // rundown-backed consumer would achieve. - method = ResolveCompilerMethodOnGenericType(handle); + return ExtractStateMachineMethodName(diagInfo?.DeclaringTypeName); } + } - if (method?.DeclaringType is Type declaringType) - { - string methodName = declaringType.Name; + // Mono fallback (no managed IP->method API): resolve via the reverse method-id map. + return ResolveStateMachineMethodNameFromId(methodId); + } - int start = methodName.IndexOf('<'); - int end = methodName.IndexOf('>'); + return null; + } - start++; - if (start > 0 && end > start) - { - methodName = methodName.Substring(start, end - start); - } + // Mono has no managed IP->method API, so the reflective resolvers above return null there and a + // frame's method id (a native code IP) can't be mapped back to a name. As a fallback we build a + // reverse map from method id -> async method name by scanning every compiler-generated async state + // machine reachable from the test class and computing the SAME id the runtime emits for a + // StateMachine frame, i.e. the native code of MoveNext (see AsyncStateMachineDiagnostics. + // ResolveMethodId). This covers frame name resolution and the console dump uniformly on Mono. + // + // The IntPtr overload of GetNativeCodeInternal only exists on Mono (CoreCLR's takes an + // IRuntimeMethodInfo), so this reflection yields null on CoreCLR/NativeAOT and the fallback is a + // no-op there, leaving the robust IP->name resolution untouched. + private static readonly MethodInfo? s_getNativeCodeInternalMethod = + typeof(RuntimeMethodHandle).GetMethod( + "GetNativeCodeInternal", + BindingFlags.Static | BindingFlags.NonPublic, + null, + new[] { typeof(IntPtr) }, + null); + + // Set of (resolved name, state machine MoveNext handle) for every async state machine reachable + // from the test class, built once. This includes not just async methods declared on the test class + // but also async lambdas and local functions, whose compiler-generated state machines are nested + // types (under the test class or its display classes) implementing IAsyncStateMachine. Any of these + // can appear as a continuation frame, so all must be covered. + private static readonly Lazy<(string Name, IntPtr MoveNextHandle)[]> s_stateMachineMoveNextMethods = + new(BuildStateMachineMoveNextMethods); + + // Resolved map: state machine frame method id (native code IP of MoveNext) -> async method name. + // Filled lazily on the first resolve miss, and eagerly by tests (SnapshotStateMachineMethodIdFor) that + // need to capture a method's tier-0 id before re-tiering; additive and keyed by address. + private static readonly ConcurrentDictionary s_methodIdToName = new(); + + private static (string Name, IntPtr MoveNextHandle)[] BuildStateMachineMoveNextMethods() + { + var result = new List<(string, IntPtr)>(); + CollectStateMachineMoveNextMethods(typeof(AsyncProfilerTests), result); + return result.ToArray(); + } - return methodName; + // Recursively walks nested types looking for compiler-generated async state machines (types that + // implement IAsyncStateMachine) and records their MoveNext, named the same way IP->name resolution + // does on other runtimes (ExtractStateMachineMethodName over the state machine type name) so the + // fallback is indistinguishable from the primary resolver. + private static void CollectStateMachineMoveNextMethods(Type type, List<(string, IntPtr)> result) + { + foreach (Type nested in type.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic)) + { + if (!nested.ContainsGenericParameters + && typeof(System.Runtime.CompilerServices.IAsyncStateMachine).IsAssignableFrom(nested)) + { + MethodInfo? moveNext = nested.GetMethod( + "MoveNext", + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (moveNext is not null) + { + result.Add((ExtractStateMachineMethodName(nested.Name) ?? nested.Name, moveNext.MethodHandle.Value)); } } - } - return null; + CollectStateMachineMoveNextMethods(nested, result); + } } - // Test-only fallback used when a compiler (StateMachine) MethodId handle cannot be resolved via the - // 1-arg MethodBase.GetMethodFromHandle because its declaring type is generic. Calls the - // internal RuntimeType.GetMethodBase(handle.GetMethodInfo()) the public API uses underneath, - // which has no generic-declaring-type guard, so it names methods on generic declaring types. - // Returns null if the internals are unavailable (e.g. NativeAOT) or resolution throws. - private static MethodBase? ResolveCompilerMethodOnGenericType(RuntimeMethodHandle handle) + // Records the CURRENT native code start of every known state machine MoveNext into the id->name map. + // Used by the resolve path, which only has a raw frame address and so must consider every method. + // A MoveNext has no native code start until it has actually run, and a resume frame's method id is the + // code start of whatever version was current when the runtime first froze that id. The map is additive + // and keyed by address. No-op except on Mono. + private static void SnapshotStateMachineMethodIds() { - if (s_runtimeMethodHandleGetMethodInfo is null || s_runtimeTypeGetMethodBase is null) + if (s_getNativeCodeInternalMethod is null) { - return null; + return; } - try + foreach ((string methodName, IntPtr moveNextHandle) in s_stateMachineMoveNextMethods.Value) { - object? methodInfo = s_runtimeMethodHandleGetMethodInfo.Invoke(handle, null); - if (methodInfo is null) + object? nativeCode = s_getNativeCodeInternalMethod.Invoke(null, new object[] { moveNextHandle }); + if (nativeCode is IntPtr ip && ip != IntPtr.Zero) { - return null; + s_methodIdToName.TryAdd((ulong)(nuint)ip, methodName); } + } + } - return (MethodBase?)s_runtimeTypeGetMethodBase.Invoke(null, new object[] { methodInfo }); + // Records the CURRENT native code start of a single async method's compiler-generated state machine + // MoveNext (found via its [AsyncStateMachine] attribute) into the id->name map, keyed to the async + // method's name so it matches normal resolution. On Mono the interpreter re-tiers a method after + // enough calls, replacing its code start, while the resume frames keep the initial (tier-0) id frozen + // at first run; a test that calls a method enough to trigger re-tiering can snapshot its id up front, + // while the method is still at its tier-0 version, so the frozen id stays resolvable afterwards. + // No-op except on Mono. + private static void SnapshotStateMachineMethodIdFor(MethodInfo asyncMethod) + { + if (s_getNativeCodeInternalMethod is null) + { + return; + } + + Type? stateMachineType = asyncMethod + .GetCustomAttribute()?.StateMachineType; + MethodInfo? moveNext = stateMachineType?.GetMethod( + "MoveNext", + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + if (moveNext is null) + { + return; } - catch (Exception) + + object? nativeCode = s_getNativeCodeInternalMethod.Invoke(null, new object[] { moveNext.MethodHandle.Value }); + if (nativeCode is IntPtr ip && ip != IntPtr.Zero) + { + s_methodIdToName.TryAdd((ulong)(nuint)ip, asyncMethod.Name); + } + } + + // Mono fallback: resolve a StateMachine frame's method id to the async method name via the reverse + // map. On a miss we snapshot the current native code starts and retry, filling the map lazily as + // methods run (a MoveNext has no code start until it has first run). No-op except on Mono. + private static string? ResolveStateMachineMethodNameFromId(ulong methodId) + { + if (s_getNativeCodeInternalMethod is null || methodId == 0) { return null; } + + if (s_methodIdToName.TryGetValue(methodId, out string? name)) + { + return name; + } + + SnapshotStateMachineMethodIds(); + + return s_methodIdToName.TryGetValue(methodId, out name) ? name : null; + } + + // The compiler generates a state machine type named "d__N" (possibly nested + // and namespace-qualified). Extract the original async method name from between the angle + // brackets. Returns the input unchanged when it contains no angle brackets, or null when null. + private static string? ExtractStateMachineMethodName(string? declaringTypeName) + { + if (declaringTypeName is null) + { + return null; + } + + int start = declaringTypeName.IndexOf('<'); + int end = declaringTypeName.IndexOf('>'); + + start++; + if (start > 0 && end > start) + { + return declaringTypeName.Substring(start, end - start); + } + + return declaringTypeName; } private static TestEventListener CreateListener(EventKeywords keywords) diff --git a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV1Tests.cs b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV1Tests.cs index c3f81419e32224..35b9d5d74de1c6 100644 --- a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV1Tests.cs +++ b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/AsyncProfilerV1Tests.cs @@ -1270,7 +1270,7 @@ private static async Task StateMachineAsync_KeywordGatekeeping_Marker() await StateMachineAsync_InnerThrows(); } catch (InvalidOperationException) { } - await RuntimeAsync_SingleYield(); + await StateMachineAsync_SingleYield(); await Task.Delay(50); } @@ -1508,6 +1508,15 @@ public void StateMachineAsync_CallstackOverflow_PreservesFullDepth() const int MaxFrames = byte.MaxValue; const int Iterations = 128; + // Warm up the recursive method so its state machine id is frozen at its tier-0 version, then + // snapshot that id before tracing. (Snapshot is a no-op on non-Mono, where ids resolve + // reflectively; the warmup itself runs on all runtimes.) + var warmupGate = new TaskCompletionSource(); + Task warmup = StateMachineAsync_RecursiveChainGated(2, warmupGate.Task); + warmupGate.SetResult(); + warmup.GetAwaiter().GetResult(); + SnapshotStateMachineMethodIdFor(typeof(AsyncProfilerTests).GetMethod(nameof(StateMachineAsync_RecursiveChainGated), BindingFlags.NonPublic | BindingFlags.Static)!); + var events = CollectEvents(ResumeStateMachineAsyncCallstackKeyword, () => { RunScenarioAndFlush(async () => @@ -1545,11 +1554,23 @@ public void StateMachineAsync_CallstackOverflow_PreservesFullDepth() } } + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task StateMachineAsync_CallstackStressWithVaryingDepths_Recurse(int depth) + { + if (depth <= 1) + { + await Task.Delay(100); + return; + } + await StateMachineAsync_CallstackStressWithVaryingDepths_Recurse(depth - 1); + } + [RuntimeAsyncMethodGeneration(false)] [MethodImpl(MethodImplOptions.NoInlining)] private static async Task StateMachineAsync_CallstackStressWithVaryingDepths_Marker(int depth) { - await StateMachineAsync_RecursiveChain(depth); + await StateMachineAsync_CallstackStressWithVaryingDepths_Recurse(depth); } [ConditionalFact(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncAndThreadingSupported))] @@ -1561,6 +1582,12 @@ public void StateMachineAsync_CallstackStressWithVaryingDepths() for (int i = 0; i < iterations; i++) depths[i] = rng.Next(1, 60); + // Warm up the recursive method so its state machine id is frozen at its tier-0 version, then + // snapshot that id before tracing. (Snapshot is a no-op on non-Mono, where ids resolve + // reflectively; the warmup itself runs on all runtimes.) + StateMachineAsync_CallstackStressWithVaryingDepths_Recurse(2).GetAwaiter().GetResult(); + SnapshotStateMachineMethodIdFor(typeof(AsyncProfilerTests).GetMethod(nameof(StateMachineAsync_CallstackStressWithVaryingDepths_Recurse), BindingFlags.NonPublic | BindingFlags.Static)!); + var events = CollectEvents(ResumeStateMachineAsyncCallstackKeyword | StateMachineAsyncCoreKeywords, () => { RunScenarioAndFlush(async () => @@ -1709,6 +1736,78 @@ public void StateMachineAsync_ConfigureAwaitFalse() AssertExactlyOneCreateAndComplete(stream, markerCallstacks[0].DispatcherId, nameof(StateMachineAsync_ConfigureAwaitFalse_Marker)); } + // Generic async chain. Each method is generic over T and uses its parameter after the await, so the + // compiler emits a generic state machine (d__N`1). Instantiated with a reference type the JIT + // reaches the async body through the shared (__Canon) generic code, whose per-instantiation MethodDesc + // is an instantiating (wrapper) stub. The V1 methodId is the native code start of MoveNext, so + // RuntimeMethodHandle_GetNativeCode must peel wrapper stubs to the shared body's code for the id to map + // back to a managed method; otherwise a frame would carry a stub thunk address that resolves to null. + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task StateMachineAsync_GenericChain_Leaf_Marker(T value) + { + await Task.Delay(100).ConfigureAwait(false); + return value; + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task StateMachineAsync_GenericChain_FramesResolveToSharedBody_Mid_Marker(T value) + { + T result = await StateMachineAsync_GenericChain_Leaf_Marker(value).ConfigureAwait(false); + return result; + } + + [RuntimeAsyncMethodGeneration(false)] + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task StateMachineAsync_GenericChain_FramesResolveToSharedBody_Marker(T value) + { + T result = await StateMachineAsync_GenericChain_FramesResolveToSharedBody_Mid_Marker(value).ConfigureAwait(false); + return result; + } + + // CoreCLR-only: the primary IP->name resolver (StackFrame.GetMethodFromNativeIP) exists only on CoreCLR, + // and the Mono reverse-map fallback (CollectStateMachineMoveNextMethods) deliberately skips generic state + // machines (ContainsGenericParameters), so a generic frame can't be named on Mono without proper rundown + // or JIT-map data. + // + // A reference type (string) reaches the shared __Canon body through an instantiating (wrapper) stub that + // RuntimeMethodHandle_GetNativeCode must peel; a value type (int) is fully specialized into its own code + // (no wrapper stub) and resolves directly. Both must symbolize to their managed names. + [ConditionalTheory(typeof(AsyncProfilerTests), nameof(IsStateMachineAsyncAndThreadingSupported), nameof(IsNotMonoRuntime))] + [InlineData(typeof(string))] + [InlineData(typeof(int))] + public void StateMachineAsync_GenericChain_FramesResolveToSharedBody(Type argType) + { + var events = CollectEvents(ResumeStateMachineAsyncCallstackKeyword | StateMachineAsyncCoreKeywords, () => + { + if (argType == typeof(int)) + { + RunScenarioAndFlush(() => StateMachineAsync_GenericChain_FramesResolveToSharedBody_Marker(42)); + } + else + { + RunScenarioAndFlush(() => StateMachineAsync_GenericChain_FramesResolveToSharedBody_Marker("value")); + } + }); + + // DumpAllEvents(events); + + var stream = ParseAllEvents(events); + + var markerCallstacks = stream.CallstacksWithMarker(AsyncEventID.ResumeStateMachineAsyncCallstack, nameof(StateMachineAsync_GenericChain_FramesResolveToSharedBody_Marker)); + AssertNotEmpty(stream, markerCallstacks); + + var frameNames = markerCallstacks[0].Frames + .Select(f => GetMethodNameFromMethodId(markerCallstacks[0].CallstackType, f.MethodId)) + .Where(n => n is not null) + .ToList(); + + // Every generic-chain frame must resolve to its managed name. + AssertContains(stream, nameof(StateMachineAsync_GenericChain_Leaf_Marker), frameNames); + AssertContains(stream, nameof(StateMachineAsync_GenericChain_FramesResolveToSharedBody_Mid_Marker), frameNames); + } + [RuntimeAsyncMethodGeneration(false)] [MethodImpl(MethodImplOptions.NoInlining)] private static async Task StateMachineAsync_FaultedTask_Inner_Marker() diff --git a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Threading.Tasks.Tests.csproj b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Threading.Tasks.Tests.csproj index d1680d8f7b2a61..57f8c0c09a760f 100644 --- a/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Threading.Tasks.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Threading.Tasks.Tests.csproj @@ -60,14 +60,14 @@ - - + + - + diff --git a/src/mono/System.Private.CoreLib/src/System/RuntimeMethodHandle.cs b/src/mono/System.Private.CoreLib/src/System/RuntimeMethodHandle.cs index ab265e5bd354eb..03b63e68758c94 100644 --- a/src/mono/System.Private.CoreLib/src/System/RuntimeMethodHandle.cs +++ b/src/mono/System.Private.CoreLib/src/System/RuntimeMethodHandle.cs @@ -35,6 +35,14 @@ public IntPtr GetFunctionPointer() return GetFunctionPointer(value); } + [MethodImpl(MethodImplOptions.InternalCall)] + private static extern IntPtr GetNativeCode(IntPtr m); + + internal static IntPtr GetNativeCodeInternal(IntPtr methodHandleValue) + { + return GetNativeCode(methodHandleValue); + } + public override bool Equals(object? obj) { if (obj == null || GetType() != obj.GetType()) diff --git a/src/mono/mono/metadata/icall-def.h b/src/mono/mono/metadata/icall-def.h index 0158888234e080..69dfe4f761eada 100644 --- a/src/mono/mono/metadata/icall-def.h +++ b/src/mono/mono/metadata/icall-def.h @@ -484,6 +484,7 @@ HANDLES_REUSE_WRAPPER(RFH_4, "SetValueInternal", ves_icall_RuntimeFieldInfo_SetV ICALL_TYPE(MHAN, "System.RuntimeMethodHandle", MHAN_1) HANDLES(MHAN_1, "GetFunctionPointer", ves_icall_RuntimeMethodHandle_GetFunctionPointer, gpointer, 1, (MonoMethod_ptr)) +HANDLES(MHAN_4, "GetNativeCode", ves_icall_RuntimeMethodHandle_GetNativeCode, gpointer, 1, (MonoMethod_ptr)) HANDLES(MAHN_3, "ReboxFromNullable", ves_icall_RuntimeMethodHandle_ReboxFromNullable, void, 2, (MonoObject, MonoObjectHandleOnStack)) HANDLES(MAHN_2, "ReboxToNullable", ves_icall_RuntimeMethodHandle_ReboxToNullable, void, 3, (MonoObject, MonoQCallTypeHandle, MonoObjectHandleOnStack)) diff --git a/src/mono/mono/metadata/icall.c b/src/mono/mono/metadata/icall.c index 5c8a35a367ca9c..93a5831d893864 100644 --- a/src/mono/mono/metadata/icall.c +++ b/src/mono/mono/metadata/icall.c @@ -6250,6 +6250,13 @@ ves_icall_RuntimeMethodHandle_GetFunctionPointer (MonoMethod *method, MonoError return mono_method_get_unmanaged_wrapper_ftnptr_internal (method, FALSE, error); } +gpointer +ves_icall_RuntimeMethodHandle_GetNativeCode (MonoMethod *method, MonoError *error) +{ + MonoRuntimeCallbacks *callbacks = mono_get_runtime_callbacks (); + return callbacks->get_method_code_start ? callbacks->get_method_code_start (method) : NULL; +} + void* mono_method_get_unmanaged_wrapper_ftnptr_internal (MonoMethod *method, gboolean only_unmanaged_callers_only, MonoError *error) { diff --git a/src/mono/mono/metadata/object-internals.h b/src/mono/mono/metadata/object-internals.h index a25b69923a8181..207f18cd5bc0b9 100644 --- a/src/mono/mono/metadata/object-internals.h +++ b/src/mono/mono/metadata/object-internals.h @@ -723,6 +723,9 @@ typedef struct { void (*get_exception_stats)(guint32 *exception_count); // Same as compile_method, but returns a MonoFtnDesc in llvmonly mode gpointer (*get_ftnptr)(MonoMethod *method, gboolean need_unbox, MonoError *error); + // Returns the start address of the method's already-present native code (JIT, AOT or interpreter), or NULL if + // the method has not been compiled/prepared. + gpointer (*get_method_code_start)(MonoMethod *method); void (*interp_jit_info_foreach)(InterpJitInfoFunc func, gpointer user_data); gboolean (*interp_sufficient_stack)(gsize size); void (*init_class) (MonoClass *klass); diff --git a/src/mono/mono/mini/mini-runtime.c b/src/mono/mono/mini/mini-runtime.c index 7cf63847ff61b9..46e28a680b3cc7 100644 --- a/src/mono/mono/mini/mini-runtime.c +++ b/src/mono/mono/mini/mini-runtime.c @@ -3114,6 +3114,14 @@ mono_jit_search_all_backends_for_jit_info (MonoMethod *method, MonoJitInfo **out return code; } +static gpointer +get_method_code_start (MonoMethod *method) +{ + MonoJitInfo *ji = NULL; + gpointer code = mono_jit_search_all_backends_for_jit_info (method, &ji); + return ji ? mono_jit_info_get_code_start (ji) : code; +} + gpointer mono_jit_find_compiled_method_with_jit_info (MonoMethod *method, MonoJitInfo **ji) { @@ -4716,6 +4724,7 @@ mini_init (const char *filename) callbacks.get_weak_field_indexes = mono_aot_get_weak_field_indexes; #endif callbacks.find_jit_info_in_aot = mono_aot_find_jit_info; + callbacks.get_method_code_start = get_method_code_start; callbacks.metadata_update_published = mini_invalidate_transformed_interp_methods; callbacks.interp_jit_info_foreach = mini_interp_jit_info_foreach; callbacks.interp_sufficient_stack = mini_interp_sufficient_stack;