Skip to content

Commit 4ff20e6

Browse files
Copilotrcj1noahfalk
authored
cDAC: Add GetGCDescSeries contract API and continuation pretty-printing (#127419)
## Summary Implements continuation pretty-printing in the cDAC, mirroring `AsyncContinuationsManager::PrintContinuationName` from `asynccontinuations.h`, and adds a new `GetGCDescSeries` API to the `RuntimeTypeSystem` contract. Fixes the WebApp3 test failure seen in recent runtime-diagnostics legs such as this: https://dev.azure.com/dnceng-public/public/_build/results?buildId=1395576 The `GetGCDescSeries` API accepts an optional `numComponents` parameter (defaulting to `0` for non-array types) and handles both regular (positive `NumSeries`) and value-class repeating (negative `NumSeries`) GCDesc layouts. For value-class GCDesc, the outer loop iterates `numComponents` times — callers must pass the actual element count to enumerate pointer runs across array elements. > [!NOTE] > This PR was created with the assistance of GitHub Copilot (AI-generated content). --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rcj1 <77995559+rcj1@users.noreply.github.com> Co-authored-by: rcj1 <rachel.jarvi@gmail.com> Co-authored-by: Noah Falk <noahfalk@users.noreply.github.com>
1 parent dd17b6d commit 4ff20e6

8 files changed

Lines changed: 729 additions & 1 deletion

File tree

docs/design/datacontracts/RuntimeTypeSystem.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ partial interface IRuntimeTypeSystem : IContract
5858
public virtual bool RequiresAlign8(TypeHandle typeHandle);
5959
// True if the MethodTable represents a continuation type used by the async continuation feature
6060
public virtual bool IsContinuation(TypeHandle typeHandle);
61+
// Returns the GC pointer runs for the method table as (offset, size) pairs. Each
62+
// run starts Offset bytes from the object pointer (`this`), where offset 0
63+
// is the method table pointer, and includes Size bytes of contiguous pointers
64+
// For handles representing value types the object is assumed to be stored in the boxed layout.
65+
public virtual IEnumerable<(uint Offset, uint Size)> GetGCDescSeries(TypeHandle typeHandle, uint numComponents = 0);
6166
public virtual bool IsDynamicStatics(TypeHandle typeHandle);
6267
public virtual ushort GetNumInterfaces(TypeHandle typeHandle);
6368

@@ -554,6 +559,61 @@ Contracts used:
554559

555560
public bool RequiresAlign8(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.RequiresAlign8;
556561

562+
public bool IsContinuation(TypeHandle typeHandle) => typeHandle.IsMethodTable()
563+
&& ContinuationMethodTablePointer != TargetPointer.Null
564+
&& _methodTables[typeHandle.Address].ParentMethodTable == ContinuationMethodTablePointer;
565+
566+
IEnumerable<(uint Offset, uint Size)> GetGCDescSeries(TypeHandle typeHandle, uint numComponents = 0)
567+
{
568+
// Returns empty if not a method table or has no GC pointers.
569+
// Compute objectSize: baseSize + numComponents * componentSize.
570+
// For non-array types, numComponents == 0 so objectSize == baseSize.
571+
572+
// Read NumSeries from (mtAddress - pointerSize), sign-extended to native width.
573+
// NumSeries == 0 → empty.
574+
575+
// NumSeries > 0: Regular series.
576+
// Memory layout (each slot is pointer-sized, growing away from MT):
577+
//
578+
// MT - (2*N+1)*ptrSize : series[N-1].seriessize
579+
// MT - (2*N) *ptrSize : series[N-1].startoffset
580+
// ...
581+
// MT - 3*ptrSize : series[0].seriessize
582+
// MT - 2*ptrSize : series[0].startoffset
583+
// MT - 1*ptrSize : NumSeries (positive)
584+
// MT : MethodTable
585+
//
586+
// The raw seriessize is stored with baseSize subtracted.
587+
// Add objectSize back to get the true run length:
588+
// trueRunLength = rawSeriesSize + objectSize
589+
// For non-arrays objectSize == baseSize, recovering the actual run.
590+
// For arrays objectSize > baseSize, extending the single series across all elements.
591+
592+
// NumSeries < 0: Value-class (repeating) series.
593+
// |NumSeries| val_serie_items describe pointer runs within one array element.
594+
// Memory layout:
595+
//
596+
// MT - (N+2)*ptrSize : val_serie[N-1]
597+
// ...
598+
// MT - 3*ptrSize : val_serie[0]
599+
// MT - 2*ptrSize : startoffset
600+
// MT - 1*ptrSize : NumSeries (-N)
601+
// MT : MethodTable
602+
//
603+
// Each val_serie_item is { HALF_SIZE_T nptrs; HALF_SIZE_T skip; } packed into one pointer-width.
604+
// HALF_SIZE_T is uint16 on 32-bit, uint32 on 64-bit.
605+
// Read nptrs and skip as separate typed reads for endianness safety.
606+
// runBytes = nptrs * pointerSize; advance currentOffset by runBytes + skip after each item.
607+
//
608+
// The val_serie_items describe the GC pointer layout of a single array element.
609+
// An outer loop repeats the pattern across all elements in the array:
610+
// currentOffset = startoffset
611+
// while currentOffset <= objectSize - pointerSize:
612+
// for each val_serie_item:
613+
// yield (currentOffset, nptrs * pointerSize)
614+
// currentOffset += nptrs * pointerSize + skip
615+
}
616+
557617
public bool IsDynamicStatics(TypeHandle TypeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[TypeHandle.Address].Flags.IsDynamicStatics;
558618

559619
public ushort GetNumInterfaces(TypeHandle TypeHandle) => !typeHandle.IsMethodTable() ? 0 : _methodTables[TypeHandle.Address].NumInterfaces;

src/coreclr/vm/datadescriptor/datadescriptor.inc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,10 @@ CDAC_TYPE_FIELD(String, T_POINTER, m_FirstChar, cdac_data<StringObject>::m_First
177177
CDAC_TYPE_FIELD(String, T_UINT32, m_StringLength, cdac_data<StringObject>::m_StringLength)
178178
CDAC_TYPE_END(String)
179179

180+
CDAC_TYPE_BEGIN(ContinuationObject)
181+
CDAC_TYPE_SIZE(sizeof(ContinuationObject))
182+
CDAC_TYPE_END(ContinuationObject)
183+
180184
CDAC_TYPE_BEGIN(Array)
181185
CDAC_TYPE_SIZE(sizeof(ArrayBase))
182186
CDAC_TYPE_FIELD(Array, T_UINT32, m_NumComponents, cdac_data<ArrayBase>::m_NumComponents)

src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,12 @@ public interface IRuntimeTypeSystem : IContract
121121
bool RequiresAlign8(TypeHandle typeHandle) => throw new NotImplementedException();
122122
// True if the MethodTable represents a continuation type used by the async continuation feature
123123
bool IsContinuation(TypeHandle typeHandle) => throw new NotImplementedException();
124+
/// <summary>
125+
/// Enumerates GC pointer runs from the CGCDesc stored before the method table.
126+
/// Returns (offset, size) pairs normalized to actual byte lengths.
127+
/// See RuntimeTypeSystem.md for the full GCDesc format documentation.
128+
/// </summary>
129+
IEnumerable<(uint Offset, uint Size)> GetGCDescSeries(TypeHandle typeHandle, uint numComponents = 0) => throw new NotImplementedException();
124130
bool IsDynamicStatics(TypeHandle typeHandle) => throw new NotImplementedException();
125131
ushort GetNumInterfaces(TypeHandle typeHandle) => throw new NotImplementedException();
126132

src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ public enum DataType
7878
StressMsg,
7979
StressMsgHeader,
8080
Object,
81+
ContinuationObject,
8182
NativeObjectWrapperObject,
8283
ManagedObjectWrapperHolderObject,
8384
ManagedObjectWrapperLayout,

src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,81 @@ private Data.EEClass GetClassData(TypeHandle typeHandle)
564564
public bool IsContinuation(TypeHandle typeHandle) => typeHandle.IsMethodTable()
565565
&& _continuationMethodTablePointer != TargetPointer.Null
566566
&& _methodTables[typeHandle.Address].ParentMethodTable == _continuationMethodTablePointer;
567+
568+
IEnumerable<(uint Offset, uint Size)> IRuntimeTypeSystem.GetGCDescSeries(TypeHandle typeHandle, uint numComponents)
569+
{
570+
if (!typeHandle.IsMethodTable())
571+
yield break;
572+
573+
if (!ContainsGCPointers(typeHandle))
574+
yield break;
575+
576+
uint baseSize = GetBaseSize(typeHandle);
577+
uint componentSize = GetComponentSize(typeHandle);
578+
uint objectSize = baseSize + numComponents * componentSize;
579+
580+
ulong mtAddress = typeHandle.Address;
581+
ulong pointerSize = (ulong)_target.PointerSize;
582+
583+
// Sign-extend NumSeries from native pointer width.
584+
long numSeries = _target.PointerSize == sizeof(uint)
585+
? (long)(int)_target.ReadPointer(mtAddress - pointerSize).Value
586+
: (long)_target.ReadPointer(mtAddress - pointerSize).Value;
587+
if (numSeries == 0)
588+
yield break;
589+
590+
if (numSeries > 0)
591+
{
592+
// Regular series: iterate from highest (closest to MT) to lowest.
593+
for (ulong i = 0; i < (ulong)numSeries; i++)
594+
{
595+
ulong seriesBase = mtAddress - (3 + 2 * i) * pointerSize;
596+
ulong rawSeriesSize = _target.ReadPointer(seriesBase).Value;
597+
ulong seriesOffset = _target.ReadPointer(seriesBase + pointerSize).Value;
598+
yield return ((uint)seriesOffset, (uint)(rawSeriesSize + objectSize));
599+
}
600+
}
601+
else
602+
{
603+
long absNumSeries = -numSeries;
604+
ulong startOffset = _target.ReadPointer(mtAddress - 2 * pointerSize).Value;
605+
606+
var seriesItems = new (uint Nptrs, uint Skip)[absNumSeries];
607+
for (long i = 0; i < absNumSeries; i++)
608+
{
609+
ulong itemAddress = mtAddress - (3 + (ulong)i) * pointerSize;
610+
611+
// Read val_serie_item fields individually for endianness safety.
612+
uint nptrs, skip;
613+
if (_target.PointerSize == sizeof(uint))
614+
{
615+
nptrs = _target.Read<ushort>(itemAddress);
616+
skip = _target.Read<ushort>(itemAddress + sizeof(ushort));
617+
}
618+
else
619+
{
620+
nptrs = _target.Read<uint>(itemAddress);
621+
skip = _target.Read<uint>(itemAddress + sizeof(uint));
622+
}
623+
624+
seriesItems[i] = (nptrs, skip);
625+
}
626+
627+
ulong currentOffset = startOffset;
628+
for (int i = 0; i < numComponents; i++)
629+
{
630+
for (long j = 0; j < absNumSeries; j++)
631+
{
632+
if (currentOffset > objectSize - pointerSize)
633+
yield break;
634+
uint runBytes = seriesItems[j].Nptrs * (uint)pointerSize;
635+
yield return ((uint)currentOffset, runBytes);
636+
currentOffset += runBytes + seriesItems[j].Skip;
637+
}
638+
}
639+
}
640+
}
641+
567642
public bool IsDynamicStatics(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.IsDynamicStatics;
568643
public ushort GetNumInterfaces(TypeHandle typeHandle) => !typeHandle.IsMethodTable() ? (ushort)0 : _methodTables[typeHandle.Address].NumInterfaces;
569644

src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/TypeNameBuilder.cs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Reflection.Metadata;
77
using System.Reflection.Metadata.Ecma335;
88
using System.Text;
9+
using Microsoft.Diagnostics.DataContractReader;
910
using Microsoft.Diagnostics.DataContractReader.Contracts;
1011

1112
namespace Microsoft.Diagnostics.DataContractReader.Legacy;
@@ -283,7 +284,14 @@ private static void AppendTypeCore(ref TypeNameBuilder tnb, Contracts.TypeHandle
283284
Contracts.ModuleHandle moduleHandle = tnb.Target.Contracts.Loader.GetModuleHandleFromModulePtr(typeSystemContract.GetModule(typeHandle));
284285
if (MetadataTokens.EntityHandle((int)typeDefToken).IsNil)
285286
{
286-
tnb.AddName("(dynamicClass)");
287+
if (typeSystemContract.IsContinuation(typeHandle))
288+
{
289+
AppendContinuationName(ref tnb, typeSystemContract, typeHandle);
290+
}
291+
else
292+
{
293+
tnb.AddName("(dynamicClass)");
294+
}
287295
}
288296
else
289297
{
@@ -480,6 +488,52 @@ private void AddAssemblySpec(string? assemblySpec)
480488
}
481489
}
482490

491+
/// <summary>
492+
/// Builds the synthetic name for a dynamically-created continuation method table, mirroring the
493+
/// native <c>AsyncContinuationsManager::PrintContinuationName</c> in <c>asynccontinuations.h</c>.
494+
/// </summary>
495+
/// <remarks>
496+
/// The name has the form:
497+
/// <c>Continuation_&lt;dataSize&gt;[_&lt;gcOffset&gt;_&lt;gcCount&gt;]*</c>
498+
/// where:
499+
/// <list type="bullet">
500+
/// <item><description>
501+
/// <c>dataSize</c> is the number of bytes of data payload (base size minus the fixed
502+
/// object-header and continuation-header overhead).
503+
/// </description></item>
504+
/// <item><description>
505+
/// Each <c>_gcOffset_gcCount</c> pair describes one GC-pointer run: <c>gcOffset</c> is the
506+
/// offset in bytes from the start of the data payload to the run, and <c>gcCount</c> is the
507+
/// number of pointer-sized GC references in that run.
508+
/// </description></item>
509+
/// </list>
510+
/// Only GC descriptor series whose <c>startoffset</c> is at or above the continuation data
511+
/// payload (i.e., after the fixed <c>CORINFO_Continuation</c> header fields) are included.
512+
/// </remarks>
513+
private static void AppendContinuationName(ref TypeNameBuilder tnb, IRuntimeTypeSystem typeSystemContract, TypeHandle typeHandle)
514+
{
515+
uint baseSize = typeSystemContract.GetBaseSize(typeHandle);
516+
uint continuationDataOffset = tnb.Target.GetTypeInfo(DataType.ContinuationObject).Size!.Value;
517+
uint objHeaderSize = tnb.Target.GetTypeInfo(DataType.ObjectHeader).Size!.Value;
518+
uint dataSize = baseSize - (objHeaderSize + continuationDataOffset);
519+
520+
var name = new StringBuilder("Continuation_");
521+
name.Append(dataSize);
522+
523+
foreach ((uint seriesOffset, uint seriesSize) in typeSystemContract.GetGCDescSeries(typeHandle))
524+
{
525+
if (seriesOffset < continuationDataOffset)
526+
continue;
527+
528+
name.Append('_');
529+
name.Append(seriesOffset - continuationDataOffset);
530+
name.Append('_');
531+
name.Append(seriesSize / (uint)tnb.Target.PointerSize);
532+
}
533+
534+
tnb.AddNameNoEscaping(name);
535+
}
536+
483537
private static void AppendNestedTypeDef(ref TypeNameBuilder tnb, MetadataReader reader, TypeDefinitionHandle typeDefToken, TypeNameFormat format)
484538
{
485539
TypeDefinition typeDef = reader.GetTypeDefinition(typeDefToken);

0 commit comments

Comments
 (0)