From 020bc708e9bd84160b62349241fd9a0b2da7fdc1 Mon Sep 17 00:00:00 2001 From: Tom McDonald Date: Fri, 31 Jul 2026 18:27:38 -0400 Subject: [PATCH 1/7] Emit a resolvable type name for Runtime Async continuations in GC heap dumps The GCHeapDump BulkType event set fixedSizedData.TypeNameID = TypeHandle::GetCl() and the type name from TypeHandle::GetName(). For a metadata-less Runtime Async continuation MethodTable, GetCl() is the nil TypeDef token (0x02000000) and GetName() yields an empty string, so dotnet-gcdump renders every continuation as "Type(0x02000000)" with no usable name. Describe continuations using the base Continuation type (via UpCastTypeIfNeeded) for the module id, name token, and name, while keeping the per-object TypeID as the derived MethodTable so GCBulkNode entries still resolve to this record. This also avoids MethodTable::GetModule()'s IsContinuationWithoutMetadata() assert on checked builds. Validated with dotnet-gcdump against a process holding 128 live continuations: before, 128 objects showed as "Type(0x02000000)"; after, they show as "System.Runtime.CompilerServices.Continuation" and the capture still succeeds. Relates to https://github.com/dotnet/runtime/issues/120800 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/eventtrace_bulktype.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/coreclr/vm/eventtrace_bulktype.cpp b/src/coreclr/vm/eventtrace_bulktype.cpp index 4eac600acfa2c4..44b77d57eee17c 100644 --- a/src/coreclr/vm/eventtrace_bulktype.cpp +++ b/src/coreclr/vm/eventtrace_bulktype.cpp @@ -737,8 +737,18 @@ int BulkTypeEventLogger::LogSingleType(TypeHandle th) return -1; pVal->fixedSizedData.TypeID = (ULONGLONG) th.AsTAddr(); - pVal->fixedSizedData.ModuleID = (ULONGLONG) (TADDR) th.GetModule(); - pVal->fixedSizedData.TypeNameID = (th.GetMethodTable() == NULL) ? 0 : th.GetCl(); + + // Runtime Async introduces dynamically-created Continuation MethodTables that have no + // backing metadata (nil TypeDef token, no resolvable name). Describe them using the base + // Continuation type for the module, name token, and name, so heap dumps show a meaningful + // type name instead of "Type(0x02000000)". The per-object TypeID above intentionally remains + // the derived MethodTable so the GCBulkNode entries still resolve to this record. Using the + // base type also avoids MethodTable::GetModule()'s IsContinuationWithoutMetadata() assert on + // checked builds. See https://github.com/dotnet/runtime/issues/120800. + TypeHandle thNamed = th.UpCastTypeIfNeeded(); + + pVal->fixedSizedData.ModuleID = (ULONGLONG) (TADDR) thNamed.GetModule(); + pVal->fixedSizedData.TypeNameID = (thNamed.GetMethodTable() == NULL) ? 0 : thNamed.GetCl(); pVal->fixedSizedData.Flags = 0; pVal->fixedSizedData.CorElementType = (BYTE) th.GetInternalCorElementType(); @@ -846,7 +856,7 @@ int BulkTypeEventLogger::LogSingleType(TypeHandle th) TRACE_LEVEL_INFORMATION, CLR_GCHEAPANDTYPENAMES_KEYWORD)) { - th.GetName(pVal->sName); + thNamed.GetName(pVal->sName); } pVal->sName.Normalize(); } From 7c34225447fa91c4239665c74bd97ab6c1e21fd0 Mon Sep 17 00:00:00 2001 From: Tom McDonald Date: Fri, 31 Jul 2026 18:36:10 -0400 Subject: [PATCH 2/7] Add GC heap dump test for Runtime Async continuation type names Regression test for https://github.com/dotnet/runtime/issues/120800. Creates live Runtime Async continuations (metadata-less continuation MethodTables), captures a GC heap snapshot over EventPipe (GCHeapSnapshot keyword), and asserts every logged type in the GCBulkType events carries a non-empty name and that a continuation type is observed. Before the accompanying runtime fix, the metadata-less continuations were logged with an empty name (rendered as "Type(0x02000000)" by dotnet-gcdump); the test's _emptyTypeNameCount==0 assertion guards that. Models the existing gcdump test; enables Runtime Async via runtime-async=on plus a DOTNET_RuntimeAsync=1 CLRTestEnvironmentVariable, and skips itself if Runtime Async is not active. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../gcdump_runtimeasync.cs | 166 ++++++++++++++++++ .../gcdump_runtimeasync.csproj | 22 +++ 2 files changed, 188 insertions(+) create mode 100644 src/tests/tracing/eventpipe/gcdump_runtimeasync/gcdump_runtimeasync.cs create mode 100644 src/tests/tracing/eventpipe/gcdump_runtimeasync/gcdump_runtimeasync.csproj diff --git a/src/tests/tracing/eventpipe/gcdump_runtimeasync/gcdump_runtimeasync.cs b/src/tests/tracing/eventpipe/gcdump_runtimeasync/gcdump_runtimeasync.cs new file mode 100644 index 00000000000000..a769157073d242 --- /dev/null +++ b/src/tests/tracing/eventpipe/gcdump_runtimeasync/gcdump_runtimeasync.cs @@ -0,0 +1,166 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Tracing; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Diagnostics.Tracing; +using Microsoft.Diagnostics.Tracing.Parsers; +using Microsoft.Diagnostics.Tracing.Parsers.Clr; +using Microsoft.Diagnostics.NETCore.Client; +using Tracing.Tests.Common; +using Xunit; + +namespace Tracing.Tests.GCDumpRuntimeAsync +{ + // Regression coverage for https://github.com/dotnet/runtime/issues/120800. + // + // With Runtime Async enabled the runtime creates dynamically-generated Continuation + // MethodTables that have no backing metadata (nil TypeDef token, no name). The GCHeapDump + // BulkType events used to emit those types with a nil TypeNameID and an empty name, so a + // heap dump rendered every continuation as "Type(0x02000000)". This test captures a heap + // snapshot while Runtime Async continuations are live and verifies that every logged type + // carries a non-empty name (the runtime now describes continuations using the base + // Continuation type). + public class GCDumpRuntimeAsyncTest + { + private static bool _seenGCStart = false; + private static bool _seenGCStop = false; + private static int _bulkTypeCount = 0; + private static int _bulkNodeCount = 0; + private static int _emptyTypeNameCount = 0; + private static bool _sawContinuationType = false; + + private static ManualResetEvent _gcStopReceived = new ManualResetEvent(false); + + // Keeps every continuation suspended mid-await so they stay live across the heap snapshot. + private static readonly TaskCompletionSource s_gate = + new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + private static List> s_liveContinuations; + + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task Suspended(byte[] captured) + { + // 'captured' is live across the suspension point, so the continuation holds a GC + // reference to it -- this is the metadata-less continuation sub-type the test targets. + await s_gate.Task; + long sum = 0; + for (int i = 0; i < captured.Length; i++) + sum += captured[i]; + return sum; + } + + [Fact] + public static int TestEntryPoint() + { + // Verify Runtime Async is actually active; otherwise the test would be vacuous. + var mi = typeof(GCDumpRuntimeAsyncTest).GetMethod(nameof(Suspended), + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + bool runtimeAsync = (mi.MethodImplementationFlags & System.Reflection.MethodImplAttributes.Async) != 0; + if (!runtimeAsync) + { + Console.WriteLine("Runtime Async is not enabled (Suspended is not a runtime-async method); skipping."); + return 100; + } + + const int ContinuationCount = 128; + s_liveContinuations = new List>(ContinuationCount); + for (int i = 0; i < ContinuationCount; i++) + { + byte[] payload = new byte[128]; + payload[0] = (byte)i; + s_liveContinuations.Add(Suspended(payload)); + } + + List providers = new List + { + new EventPipeProvider("Microsoft-Windows-DotNETRuntime", eventLevel: EventLevel.Verbose, + keywords: (long)ClrTraceEventParser.Keywords.GCHeapSnapshot) + }; + + int ret = IpcTraceTest.RunAndValidateEventCounts(_expectedEventCounts, _eventGeneratingAction, providers, 1024, _Validate); + + // Let the continuations complete so the process shuts down cleanly. + s_gate.SetResult(0); + Task.WaitAll(s_liveContinuations.ToArray()); + return ret; + } + + private static Dictionary _expectedEventCounts = new Dictionary() + { + // This space intentionally left blank + }; + + private static Action _eventGeneratingAction = () => + { + // Wait up to 10 seconds to receive the GCStop event for the heap snapshot. + _gcStopReceived.WaitOne(10000); + }; + + private static Func> _Validate = (source) => + { + source.Clr.GCStart += (GCStartTraceData data) => + { + _seenGCStart = true; + }; + + source.Clr.TypeBulkType += (GCBulkTypeTraceData data) => + { + _bulkTypeCount += data.Count; + for (int i = 0; i < data.Count; i++) + { + string name = data.Values(i).TypeName; + if (string.IsNullOrEmpty(name)) + { + _emptyTypeNameCount++; + } + else if (name.Contains("Continuation")) + { + _sawContinuationType = true; + } + } + }; + + source.Clr.GCBulkNode += delegate (GCBulkNodeTraceData data) + { + _bulkNodeCount += data.Count; + }; + + source.Clr.GCStop += (GCEndTraceData data) => + { + _seenGCStop = true; + _gcStopReceived.Set(); + }; + + return () => + { + // Keep the continuations rooted until validation runs. + GC.KeepAlive(s_liveContinuations); + + if (_seenGCStart + && _seenGCStop + && _bulkTypeCount > 50 + && _bulkNodeCount > 50 + && _sawContinuationType + && _emptyTypeNameCount == 0) + { + return 100; + } + + Console.WriteLine("Test failed."); + Console.WriteLine($"_seenGCStart = {_seenGCStart}"); + Console.WriteLine($"_seenGCStop = {_seenGCStop}"); + Console.WriteLine($"_bulkTypeCount = {_bulkTypeCount}"); + Console.WriteLine($"_bulkNodeCount = {_bulkNodeCount}"); + Console.WriteLine($"_sawContinuationType = {_sawContinuationType}"); + Console.WriteLine($"_emptyTypeNameCount = {_emptyTypeNameCount} (expected 0; a non-zero count means " + + "metadata-less Runtime Async continuations were logged without a type name -- dotnet/runtime#120800)"); + return -1; + }; + }; + } +} diff --git a/src/tests/tracing/eventpipe/gcdump_runtimeasync/gcdump_runtimeasync.csproj b/src/tests/tracing/eventpipe/gcdump_runtimeasync/gcdump_runtimeasync.csproj new file mode 100644 index 00000000000000..b61b5e4addf9f0 --- /dev/null +++ b/src/tests/tracing/eventpipe/gcdump_runtimeasync/gcdump_runtimeasync.csproj @@ -0,0 +1,22 @@ + + + true + .NETCoreApp + true + true + + $(Features);runtime-async=on + + + + + + + + + + + + From 378dd8fe9f7ee0ea6a3ed9617f4dfbc34bab24fc Mon Sep 17 00:00:00 2001 From: Tom McDonald Date: Fri, 31 Jul 2026 18:53:54 -0400 Subject: [PATCH 3/7] Shorten continuation comment in LogSingleType --- src/coreclr/vm/eventtrace_bulktype.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/coreclr/vm/eventtrace_bulktype.cpp b/src/coreclr/vm/eventtrace_bulktype.cpp index 44b77d57eee17c..a31ec3e705d8c7 100644 --- a/src/coreclr/vm/eventtrace_bulktype.cpp +++ b/src/coreclr/vm/eventtrace_bulktype.cpp @@ -738,13 +738,10 @@ int BulkTypeEventLogger::LogSingleType(TypeHandle th) pVal->fixedSizedData.TypeID = (ULONGLONG) th.AsTAddr(); - // Runtime Async introduces dynamically-created Continuation MethodTables that have no - // backing metadata (nil TypeDef token, no resolvable name). Describe them using the base - // Continuation type for the module, name token, and name, so heap dumps show a meaningful - // type name instead of "Type(0x02000000)". The per-object TypeID above intentionally remains - // the derived MethodTable so the GCBulkNode entries still resolve to this record. Using the - // base type also avoids MethodTable::GetModule()'s IsContinuationWithoutMetadata() assert on - // checked builds. See https://github.com/dotnet/runtime/issues/120800. + // Runtime Async continuation MethodTables have no metadata (nil TypeDef token, no name). + // Describe them via the base Continuation type so heap dumps show a real name instead of + // "Type(0x02000000)"; the per-object TypeID above stays the derived MT so GCBulkNode entries + // still resolve. See https://github.com/dotnet/runtime/issues/120800. TypeHandle thNamed = th.UpCastTypeIfNeeded(); pVal->fixedSizedData.ModuleID = (ULONGLONG) (TADDR) thNamed.GetModule(); From 7ce512ebadd2187bbd2ea8ccf7f1aa35da237054 Mon Sep 17 00:00:00 2001 From: Tom McDonald Date: Fri, 31 Jul 2026 23:32:29 -0400 Subject: [PATCH 4/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../eventpipe/gcdump_runtimeasync/gcdump_runtimeasync.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/tests/tracing/eventpipe/gcdump_runtimeasync/gcdump_runtimeasync.cs b/src/tests/tracing/eventpipe/gcdump_runtimeasync/gcdump_runtimeasync.cs index a769157073d242..dfc73bcf5ba2fd 100644 --- a/src/tests/tracing/eventpipe/gcdump_runtimeasync/gcdump_runtimeasync.cs +++ b/src/tests/tracing/eventpipe/gcdump_runtimeasync/gcdump_runtimeasync.cs @@ -54,6 +54,10 @@ private static async Task Suspended(byte[] captured) return sum; } + [ActiveIssue("System.Diagnostics.Process is not supported on wasm", TestPlatforms.Browser)] + [ActiveIssue("Can't find file dotnet-diagnostic-{pid}-*-socket", typeof(TestLibrary.PlatformDetection), nameof(TestLibrary.PlatformDetection.IsMonoRuntime), nameof(TestLibrary.PlatformDetection.IsRiscv64Process))] + [SkipOnCoreClr("This test is sensitive to JIT optimizations.", RuntimeTestModes.AnyJitOptimizationStress)] + [SkipOnCoreClr("Tracing tests routinely time out with JIT stress and GC stress.", RuntimeTestModes.AnyGCStress)] [Fact] public static int TestEntryPoint() { From 598acbbea8381b5960b5144d0ec857525b5e7d20 Mon Sep 17 00:00:00 2001 From: Tom McDonald Date: Fri, 31 Jul 2026 23:38:54 -0400 Subject: [PATCH 5/7] Fix Runtime Async test configuration comment Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e8cd920d-9e3e-4b34-b898-de7bdad68f24 --- .../eventpipe/gcdump_runtimeasync/gcdump_runtimeasync.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/tracing/eventpipe/gcdump_runtimeasync/gcdump_runtimeasync.csproj b/src/tests/tracing/eventpipe/gcdump_runtimeasync/gcdump_runtimeasync.csproj index b61b5e4addf9f0..ea509f3a5db590 100644 --- a/src/tests/tracing/eventpipe/gcdump_runtimeasync/gcdump_runtimeasync.csproj +++ b/src/tests/tracing/eventpipe/gcdump_runtimeasync/gcdump_runtimeasync.csproj @@ -5,8 +5,8 @@ true true + continuation MethodTables this test exercises. Runtime Async is enabled by the + runtime-async feature and DOTNET_RuntimeAsync CLR test environment variable. --> $(Features);runtime-async=on From 3a503683b1c1d75a71714d6b45bd0e9ab291204a Mon Sep 17 00:00:00 2001 From: Tom McDonald Date: Sat, 1 Aug 2026 11:48:27 -0400 Subject: [PATCH 6/7] Use normalized type for bulk element metadata Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: e8cd920d-9e3e-4b34-b898-de7bdad68f24 --- src/coreclr/vm/eventtrace_bulktype.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/vm/eventtrace_bulktype.cpp b/src/coreclr/vm/eventtrace_bulktype.cpp index a31ec3e705d8c7..146d706078f23c 100644 --- a/src/coreclr/vm/eventtrace_bulktype.cpp +++ b/src/coreclr/vm/eventtrace_bulktype.cpp @@ -747,7 +747,7 @@ int BulkTypeEventLogger::LogSingleType(TypeHandle th) pVal->fixedSizedData.ModuleID = (ULONGLONG) (TADDR) thNamed.GetModule(); pVal->fixedSizedData.TypeNameID = (thNamed.GetMethodTable() == NULL) ? 0 : thNamed.GetCl(); pVal->fixedSizedData.Flags = 0; - pVal->fixedSizedData.CorElementType = (BYTE) th.GetInternalCorElementType(); + pVal->fixedSizedData.CorElementType = (BYTE) thNamed.GetInternalCorElementType(); if (th.IsArray()) { From a04419d20eff66046b7a45844319636c536ea474 Mon Sep 17 00:00:00 2001 From: Tom McDonald Date: Sun, 2 Aug 2026 23:17:18 -0400 Subject: [PATCH 7/7] Disable Runtime Async GC dump test on Mono Runtime Async-generated IL is not supported by Mono and causes InvalidProgramException in Mono test legs. Exclude the project for Mono and keep the RISC-V diagnostic socket skip independent. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../eventpipe/gcdump_runtimeasync/gcdump_runtimeasync.cs | 2 +- .../eventpipe/gcdump_runtimeasync/gcdump_runtimeasync.csproj | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/tests/tracing/eventpipe/gcdump_runtimeasync/gcdump_runtimeasync.cs b/src/tests/tracing/eventpipe/gcdump_runtimeasync/gcdump_runtimeasync.cs index dfc73bcf5ba2fd..0d09e9e599ccc9 100644 --- a/src/tests/tracing/eventpipe/gcdump_runtimeasync/gcdump_runtimeasync.cs +++ b/src/tests/tracing/eventpipe/gcdump_runtimeasync/gcdump_runtimeasync.cs @@ -55,7 +55,7 @@ private static async Task Suspended(byte[] captured) } [ActiveIssue("System.Diagnostics.Process is not supported on wasm", TestPlatforms.Browser)] - [ActiveIssue("Can't find file dotnet-diagnostic-{pid}-*-socket", typeof(TestLibrary.PlatformDetection), nameof(TestLibrary.PlatformDetection.IsMonoRuntime), nameof(TestLibrary.PlatformDetection.IsRiscv64Process))] + [ActiveIssue("Can't find file dotnet-diagnostic-{pid}-*-socket", typeof(TestLibrary.PlatformDetection), nameof(TestLibrary.PlatformDetection.IsRiscv64Process))] [SkipOnCoreClr("This test is sensitive to JIT optimizations.", RuntimeTestModes.AnyJitOptimizationStress)] [SkipOnCoreClr("Tracing tests routinely time out with JIT stress and GC stress.", RuntimeTestModes.AnyGCStress)] [Fact] diff --git a/src/tests/tracing/eventpipe/gcdump_runtimeasync/gcdump_runtimeasync.csproj b/src/tests/tracing/eventpipe/gcdump_runtimeasync/gcdump_runtimeasync.csproj index ea509f3a5db590..1ef0469bf789a2 100644 --- a/src/tests/tracing/eventpipe/gcdump_runtimeasync/gcdump_runtimeasync.csproj +++ b/src/tests/tracing/eventpipe/gcdump_runtimeasync/gcdump_runtimeasync.csproj @@ -4,6 +4,8 @@ .NETCoreApp true true + + true