File: EnumMemory\ObjectCollector.cs
Web Access
Project: Microsoft.Diagnostics.DataContractReader.Legacy.csproj (Microsoft.Diagnostics.DataContractReader.Legacy)
// 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.Buffers.Binary;
using System.Collections.Generic;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Diagnostics.DataContractReader.Contracts;
using Microsoft.Diagnostics.DataContractReader.Legacy;

namespace Microsoft.Diagnostics.DataContractReader.Legacy.EnumMemory;

internal sealed class ObjectCollector(CollectorTarget target, MethodCollector methods)
{
    private const ulong MaxObjectSize = 64 * 1024 * 1024;
    private const int MaxInnerExceptionCount = 256;
    private const int MaxTypeTraversalDepth = 1_024;

    private readonly CollectorTarget _collectorTarget = target;
    private readonly MethodCollector _methods = methods;
    private readonly Stack<TargetPointer> _pendingObjects = [];
    private readonly HashSet<TargetPointer> _visitedObjects = [];
    private int _remainingInnerExceptions;

    public void EnumerateObject(TargetPointer objectAddress)
    {
        _remainingInnerExceptions = MaxInnerExceptionCount;
        _pendingObjects.Push(objectAddress);
        try
        {
            while (_pendingObjects.TryPop(out TargetPointer pendingObject))
                EnumerateObjectCore(pendingObject);
        }
        finally
        {
            _pendingObjects.Clear();
        }
    }

    private void EnumerateObjectCore(TargetPointer objectAddress)
    {
        if (objectAddress == TargetPointer.Null || !_visitedObjects.Add(objectAddress))
        {
            return;
        }

        try
        {
            IObject objects = _collectorTarget.Target.Contracts.Object;
            IRuntimeTypeSystem types = _collectorTarget.Target.Contracts.RuntimeTypeSystem;
            ulong objectSize = objects.GetSize(objectAddress);
            if (objectSize == 0 || objectSize > MaxObjectSize)
                return;

            _collectorTarget.Emitter.Add(objectAddress.Value, objectSize);
            objects.GetSyncBlockAddress(objectAddress);

            TargetPointer methodTable = objects.GetMethodTableAddress(objectAddress);
            ITypeHandle type = types.GetTypeHandle(methodTable);
            bool isException = EnumerateTypeHierarchy(type);

            if (methodTable == types.GetWellKnownMethodTable(WellKnownMethodTable.String))
            {
                objects.GetStringValue(objectAddress);
            }
            else if (types.IsArray(type, out _))
            {
                EnumerateArray(objects, objectAddress, type);
            }

            if (_collectorTarget.Target.Contracts.FeatureFlags.IsEnabled(RuntimeFeature.COMInterop))
                objects.GetBuiltInComData(objectAddress, out _, out _, out _);

            if (isException)
            {
                CacheMethodTableName(methodTable);
                EnumerateExceptionData(objectAddress);
            }
        }
        catch (System.Exception ex) when (ex.HResult != HResults.COR_E_OPERATIONCANCELED)
        {
        }
    }

    private bool EnumerateTypeHierarchy(ITypeHandle type)
    {
        IRuntimeTypeSystem types = _collectorTarget.Target.Contracts.RuntimeTypeSystem;
        TargetPointer exceptionMethodTable =
            types.GetWellKnownMethodTable(WellKnownMethodTable.Exception);
        bool isException = false;

        for (int depth = 0; type.Address != TargetPointer.Null && depth < MaxTypeTraversalDepth; depth++)
        {
            EnumerateMethodTableData(type);
            isException |= type.Address == exceptionMethodTable;

            TargetPointer parentMethodTable = types.GetParentMethodTable(type);
            if (parentMethodTable == TargetPointer.Null)
                break;

            type = types.GetTypeHandle(parentMethodTable);
        }

        return isException;
    }

    private void EnumerateMethodTableData(ITypeHandle type)
    {
        IRuntimeTypeSystem types = _collectorTarget.Target.Contracts.RuntimeTypeSystem;
        types.GetBaseSize(type);
        types.GetComponentSize(type);
        if (types.IsFreeObjectMethodTable(type))
            return;

        types.GetModule(type);
        types.GetCanonicalMethodTable(type);
        types.GetParentMethodTable(type);
        types.GetNumInterfaces(type);
        types.GetNumMethods(type);
        types.GetTypeDefToken(type);
        types.GetTypeDefTypeAttributes(type);
        types.ContainsGCPointers(type);
        types.IsDynamicStatics(type);
    }

    private void EnumerateArray(IObject objects, TargetPointer objectAddress, ITypeHandle type)
    {
        IRuntimeTypeSystem types = _collectorTarget.Target.Contracts.RuntimeTypeSystem;
        objects.GetArrayData(objectAddress, out _, out _, out _, out _, out _);
        ITypeHandle elementType = types.GetTypeParam(type);
        types.GetSignatureCorElementType(elementType);
        for (int depth = 0; depth < MaxTypeTraversalDepth && types.IsArray(elementType, out _); depth++)
            elementType = types.GetTypeParam(elementType);
    }

    private void EnumerateExceptionData(TargetPointer exceptionObject)
    {
        IException exceptions = _collectorTarget.Target.Contracts.Exception;
        ExceptionData data = exceptions.GetExceptionData(exceptionObject);
        if (data.InnerException != TargetPointer.Null && _remainingInnerExceptions > 0)
        {
            _remainingInnerExceptions--;
            _pendingObjects.Push(data.InnerException);
        }
        EnumerateStackTraceString(data.StackTraceString);
        if (data.RemoteStackTraceString != TargetPointer.Null &&
            (_collectorTarget.DumpType != DumpType.Triage || !ExceptionTypeOverridesStackTraceGetter(exceptionObject)))
        {
            EnumerateStackTraceString(data.RemoteStackTraceString);
        }
        _pendingObjects.Push(data.WatsonBuckets);
        _pendingObjects.Push(data.StackTrace);
        if (_collectorTarget.DumpType != DumpType.Triage)
            _pendingObjects.Push(data.Message);

        foreach (ExceptionStackFrameInfo frame in exceptions.GetExceptionStackFrames(exceptionObject))
            _methods.CaptureMethod(frame.MethodDesc, new TargetCodePointer(frame.Ip.Value));
    }

    private bool ExceptionTypeOverridesStackTraceGetter(TargetPointer exceptionObject)
    {
        IRuntimeTypeSystem types = _collectorTarget.Target.Contracts.RuntimeTypeSystem;
        TargetPointer methodTable = _collectorTarget.Target.Contracts.Object.GetMethodTableAddress(exceptionObject);
        TargetPointer exceptionMethodTable = types.GetWellKnownMethodTable(WellKnownMethodTable.Exception);
        if (methodTable == exceptionMethodTable)
            return false;

        ITypeHandle exceptionType = types.GetTypeHandle(exceptionMethodTable);
        ITypeHandle objectType = types.GetTypeHandle(types.GetWellKnownMethodTable(WellKnownMethodTable.Object));
        ITypeHandle derivedType = types.GetTypeHandle(methodTable);
        Contracts.ModuleHandle module = _collectorTarget.Target.Contracts.Loader.GetModuleHandleFromModulePtr(types.GetModule(exceptionType));
        MetadataReader metadata = _collectorTarget.Target.Contracts.EcmaMetadata.GetMetadata(module)
            ?? throw new InvalidOperationException("Exception metadata is required to identify the StackTrace getter.");

        ushort numSlots = types.GetNumVtableSlots(exceptionType);
        for (ushort slot = types.GetNumVtableSlots(objectType); slot < numSlots; slot++)
        {
            TargetPointer methodDesc = types.GetMethodDescForSlot(exceptionType, slot);
            if (methodDesc == TargetPointer.Null)
                continue;

            uint token = types.GetMethodToken(types.GetMethodDescHandle(methodDesc));
            MethodDefinition method = metadata.GetMethodDefinition(MetadataTokens.MethodDefinitionHandle((int)(token & 0x00FFFFFF)));
            if (metadata.StringComparer.Equals(method.Name, "get_StackTrace"))
                return types.GetMethodDescForSlot(derivedType, slot) != methodDesc;
        }

        throw new InvalidOperationException("Could not find the StackTrace getter on System.Exception.");
    }

    private void EnumerateStackTraceString(TargetPointer address)
    {
        if (address == TargetPointer.Null)
            return;

        EnumerateObjectCore(address);
        if (_collectorTarget.DumpType != DumpType.Triage)
            return;

        IObject objects = _collectorTarget.Target.Contracts.Object;
        objects.GetStringData(address, out uint length, out uint offsetToFirstChar);
        if (length == 0 || length > MaxObjectSize / sizeof(char))
            return;

        char[] buffer = new char[checked((int)length)];
        Span<byte> bytes = MemoryMarshal.AsBytes(buffer.AsSpan());
        _collectorTarget.Target.ReadBuffer(address + offsetToFirstChar, bytes);
        if (_collectorTarget.Target.IsLittleEndian != BitConverter.IsLittleEndian)
        {
            for (int i = 0; i < buffer.Length; i++)
                buffer[i] = (char)BinaryPrimitives.ReverseEndianness((ushort)buffer[i]);
        }
        Sanitizer.StripFileInfoFromStackTrace(buffer);
        if (_collectorTarget.Target.IsLittleEndian != BitConverter.IsLittleEndian)
        {
            for (int i = 0; i < buffer.Length; i++)
                buffer[i] = (char)BinaryPrimitives.ReverseEndianness((ushort)buffer[i]);
        }
        // Native DAC also uses the optional update callback, leaving the string length unchanged.
        _collectorTarget.Emitter.Update(address.Value + offsetToFirstChar, bytes);
    }

    private void CacheMethodTableName(TargetPointer methodTable)
    {
        if (methodTable == TargetPointer.Null || _collectorTarget.MiniMetadata.Contains(methodTable))
            return;

        try
        {
            IRuntimeTypeSystem types = _collectorTarget.Target.Contracts.RuntimeTypeSystem;
            ITypeHandle type = types.GetTypeHandle(methodTable);
            if (types.IsFreeObjectMethodTable(type))
                return;

            StringBuilder name = new();
            TypeNameBuilder.AppendType(
                _collectorTarget,
                name,
                type,
                TypeNameFormat.FormatNamespace | TypeNameFormat.FormatFullInst);
            if (name.Length != 0)
                _collectorTarget.MiniMetadata.Add(methodTable, name.ToString());
        }
        catch (System.Exception ex) when (ex.HResult != HResults.COR_E_OPERATIONCANCELED)
        {
        }
    }
}