diff --git a/src/coreclr/debug/runtimeinfo/contracts.jsonc b/src/coreclr/debug/runtimeinfo/contracts.jsonc index 186230d5c68d6..ab82fbc38c40a 100644 --- a/src/coreclr/debug/runtimeinfo/contracts.jsonc +++ b/src/coreclr/debug/runtimeinfo/contracts.jsonc @@ -9,6 +9,7 @@ // cdac-build-tool can take multiple "-c contract_file" arguments // so to conditionally include contracts, put additional contracts in a separate file { + "Thread": 1, "SOSBreakingChangeVersion": 1 // example contract: "runtime exports an SOS breaking change version global" } diff --git a/src/coreclr/debug/runtimeinfo/datadescriptor.h b/src/coreclr/debug/runtimeinfo/datadescriptor.h index b5ab51774e121..2687ceff55353 100644 --- a/src/coreclr/debug/runtimeinfo/datadescriptor.h +++ b/src/coreclr/debug/runtimeinfo/datadescriptor.h @@ -103,11 +103,17 @@ CDAC_BASELINE("empty") CDAC_TYPES_BEGIN() -CDAC_TYPE_BEGIN(ManagedThread) -CDAC_TYPE_INDETERMINATE(ManagedThread) -CDAC_TYPE_FIELD(ManagedThread, GCHandle, GCHandle, cdac_offsets::ExposedObject) -CDAC_TYPE_FIELD(ManagedThread, pointer, LinkNext, cdac_offsets::Link) -CDAC_TYPE_END(ManagedThread) +CDAC_TYPE_BEGIN(Thread) +CDAC_TYPE_INDETERMINATE(Thread) +CDAC_TYPE_FIELD(Thread, GCHandle, GCHandle, cdac_offsets::ExposedObject) +CDAC_TYPE_FIELD(Thread, pointer, LinkNext, cdac_offsets::Link) +CDAC_TYPE_END(Thread) + +CDAC_TYPE_BEGIN(ThreadStore) +CDAC_TYPE_INDETERMINATE(ThreadStore) +CDAC_TYPE_FIELD(ThreadStore, /*omit type*/, ThreadCount, cdac_offsets::ThreadCount) +CDAC_TYPE_FIELD(ThreadStore, /*omit type*/, ThreadList, cdac_offsets::ThreadList) +CDAC_TYPE_END(ThreadStore) CDAC_TYPE_BEGIN(GCHandle) CDAC_TYPE_SIZE(sizeof(OBJECTHANDLE)) @@ -116,7 +122,7 @@ CDAC_TYPE_END(GCHandle) CDAC_TYPES_END() CDAC_GLOBALS_BEGIN() -CDAC_GLOBAL_POINTER(ManagedThreadStore, &ThreadStore::s_pThreadStore) +CDAC_GLOBAL_POINTER(ThreadStore, &ThreadStore::s_pThreadStore) #if FEATURE_EH_FUNCLETS CDAC_GLOBAL(FeatureEHFunclets, uint8, 1) #else diff --git a/src/coreclr/vm/threads.h b/src/coreclr/vm/threads.h index 67c4b6b83c975..8798d1f6680e9 100644 --- a/src/coreclr/vm/threads.h +++ b/src/coreclr/vm/threads.h @@ -4335,6 +4335,15 @@ class ThreadStore void OnMaxGenerationGCStarted(); bool ShouldTriggerGCForDeadThreads(); void TriggerGCForDeadThreadsIfNecessary(); + + template friend struct ::cdac_offsets; +}; + +template<> +struct cdac_offsets +{ + static constexpr size_t ThreadList = offsetof(ThreadStore, m_ThreadList); + static constexpr size_t ThreadCount = offsetof(ThreadStore, m_ThreadCount); }; struct TSSuspendHelper { diff --git a/src/native/managed/cdacreader/src/Constants.cs b/src/native/managed/cdacreader/src/Constants.cs index 0fbcaa0a246c7..a4874ce179e16 100644 --- a/src/native/managed/cdacreader/src/Constants.cs +++ b/src/native/managed/cdacreader/src/Constants.cs @@ -8,6 +8,7 @@ internal static class Constants internal static class Globals { // See src/coreclr/debug/runtimeinfo/datadescriptor.h + internal const string ThreadStore = nameof(ThreadStore); internal const string SOSBreakingChangeVersion = nameof(SOSBreakingChangeVersion); } } diff --git a/src/native/managed/cdacreader/src/Contracts/IContract.cs b/src/native/managed/cdacreader/src/Contracts/IContract.cs new file mode 100644 index 0000000000000..1a78ce2a42920 --- /dev/null +++ b/src/native/managed/cdacreader/src/Contracts/IContract.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts; + +internal interface IContract +{ + static virtual string Name => throw new NotImplementedException(); + static virtual IContract Create(Target target, int version) => throw new NotImplementedException(); +} diff --git a/src/native/managed/cdacreader/src/Contracts/Registry.cs b/src/native/managed/cdacreader/src/Contracts/Registry.cs new file mode 100644 index 0000000000000..aad64ce527180 --- /dev/null +++ b/src/native/managed/cdacreader/src/Contracts/Registry.cs @@ -0,0 +1,39 @@ +// 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; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts; + +internal sealed class Registry +{ + // Contracts that have already been created for a target. + // Items should not be removed from this, only added. + private readonly Dictionary _contracts = []; + private readonly Target _target; + + public Registry(Target target) + { + _target = target; + } + + public IThread Thread => GetContract(); + + private T GetContract() where T : IContract + { + if (_contracts.TryGetValue(typeof(T), out IContract? contractMaybe)) + return (T)contractMaybe; + + if (!_target.TryGetContractVersion(T.Name, out int version)) + throw new NotImplementedException(); + + // Create and register the contract + IContract contract = T.Create(_target, version); + if (_contracts.TryAdd(typeof(T), contract)) + return (T)contract; + + // Contract was already registered by someone else + return (T)_contracts[typeof(T)]; + } +} diff --git a/src/native/managed/cdacreader/src/Contracts/Thread.cs b/src/native/managed/cdacreader/src/Contracts/Thread.cs new file mode 100644 index 0000000000000..30187567fe561 --- /dev/null +++ b/src/native/managed/cdacreader/src/Contracts/Thread.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts; + +// TODO: [cdac] Add other counts / threads +internal record struct ThreadStoreData( + int ThreadCount, + TargetPointer FirstThread); + +internal interface IThread : IContract +{ + static string IContract.Name { get; } = nameof(Thread); + static IContract IContract.Create(Target target, int version) + { + TargetPointer threadStore = target.ReadGlobalPointer(Constants.Globals.ThreadStore); + return version switch + { + 1 => new Thread_1(target, threadStore), + _ => default(Thread), + }; + } + + public virtual ThreadStoreData GetThreadStoreData() => throw new NotImplementedException(); +} + +internal readonly struct Thread : IThread +{ + // Everything throws NotImplementedException +} + +internal readonly struct Thread_1 : IThread +{ + private readonly Target _target; + private readonly TargetPointer _threadStoreAddr; + + internal Thread_1(Target target, TargetPointer threadStore) + { + _target = target; + _threadStoreAddr = threadStore; + } + + ThreadStoreData IThread.GetThreadStoreData() + { + Data.ThreadStore? threadStore; + if (!_target.ProcessedData.TryGet(_threadStoreAddr.Value, out threadStore)) + { + threadStore = new Data.ThreadStore(_target, _threadStoreAddr); + + // Still okay if processed data is already registered by someone else + _ = _target.ProcessedData.TryRegister(_threadStoreAddr.Value, threadStore); + } + + return new ThreadStoreData(threadStore.ThreadCount, threadStore.FirstThread); + } +} diff --git a/src/native/managed/cdacreader/src/Data/ThreadStore.cs b/src/native/managed/cdacreader/src/Data/ThreadStore.cs new file mode 100644 index 0000000000000..1ba13596359b5 --- /dev/null +++ b/src/native/managed/cdacreader/src/Data/ThreadStore.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +internal sealed class ThreadStore +{ + public ThreadStore(Target target, TargetPointer pointer) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.ThreadStore); + TargetPointer addr = target.ReadPointer(pointer.Value); + + ThreadCount = target.Read(addr.Value + (ulong)type.Fields[nameof(ThreadCount)].Offset); + FirstThread = TargetPointer.Null; + } + + public int ThreadCount { get; init; } + + public TargetPointer FirstThread { get; init; } +} diff --git a/src/native/managed/cdacreader/src/DataType.cs b/src/native/managed/cdacreader/src/DataType.cs new file mode 100644 index 0000000000000..c301c6a008d72 --- /dev/null +++ b/src/native/managed/cdacreader/src/DataType.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader; + +public enum DataType +{ + Unknown = 0, + + int8, + uint8, + int16, + uint16, + int32, + uint32, + int64, + uint64, + nint, + nuint, + pointer, + + GCHandle, + Thread, + ThreadStore, +} diff --git a/src/native/managed/cdacreader/src/Legacy/SOSDacImpl.cs b/src/native/managed/cdacreader/src/Legacy/SOSDacImpl.cs index 261aa034c3f89..5cea71a68b8a9 100644 --- a/src/native/managed/cdacreader/src/Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdacreader/src/Legacy/SOSDacImpl.cs @@ -106,7 +106,25 @@ public int GetBreakingChangeVersion() public unsafe int GetThreadFromThinlockID(uint thinLockId, ulong* pThread) => HResults.E_NOTIMPL; public unsafe int GetThreadLocalModuleData(ulong thread, uint index, void* data) => HResults.E_NOTIMPL; public unsafe int GetThreadpoolData(void* data) => HResults.E_NOTIMPL; - public unsafe int GetThreadStoreData(DacpThreadStoreData* data) => HResults.E_NOTIMPL; + + public unsafe int GetThreadStoreData(DacpThreadStoreData* data) + { + try + { + Contracts.IThread thread = _target.Contracts.Thread; + Contracts.ThreadStoreData threadStoreData = thread.GetThreadStoreData(); + data->threadCount = threadStoreData.ThreadCount; + data->firstThread = threadStoreData.FirstThread.Value; + data->fHostConfig = 0; + } + catch (Exception ex) + { + return ex.HResult; + } + + return HResults.E_NOTIMPL; + } + public unsafe int GetTLSIndex(uint* pIndex) => HResults.E_NOTIMPL; public unsafe int GetUsefulGlobals(void* data) => HResults.E_NOTIMPL; public unsafe int GetWorkRequestData(ulong addrWorkRequest, void* data) => HResults.E_NOTIMPL; diff --git a/src/native/managed/cdacreader/src/Target.cs b/src/native/managed/cdacreader/src/Target.cs index c2e09d9d0fa11..a013fca3d3635 100644 --- a/src/native/managed/cdacreader/src/Target.cs +++ b/src/native/managed/cdacreader/src/Target.cs @@ -2,8 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using System.Buffers.Binary; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Numerics; using System.Runtime.CompilerServices; @@ -19,6 +19,21 @@ public struct TargetPointer public sealed unsafe class Target { + public record struct TypeInfo + { + public uint? Size; + public Dictionary Fields = []; + + public TypeInfo() { } + } + + public record struct FieldInfo + { + public int Offset; + public DataType Type; + public string? TypeName; + } + private const int StackAllocByteThreshold = 1024; private readonly struct Configuration @@ -30,8 +45,13 @@ private readonly struct Configuration private readonly Configuration _config; private readonly Reader _reader; - private readonly IReadOnlyDictionary _contracts = new Dictionary(); + private readonly Dictionary _contracts = []; private readonly IReadOnlyDictionary _globals = new Dictionary(); + private readonly Dictionary _knownTypes = []; + private readonly Dictionary _types = []; + + internal Contracts.Registry Contracts { get; } + internal DataCache ProcessedData { get; } = new DataCache(); public static bool TryCreate(ulong contractDescriptor, delegate* unmanaged readFromTarget, void* readContext, out Target? target) { @@ -48,13 +68,43 @@ public static bool TryCreate(ulong contractDescriptor, delegate* unmanaged(ulong address, out T value) where T : unmanaged, IBinaryInteger, IMinMaxValue + private static DataType GetDataType(string type) { - if (!TryRead(address, out value)) + if (Enum.TryParse(type, false, out DataType dataType) && Enum.IsDefined(dataType)) + return dataType; + + return DataType.Unknown; + } + + public T Read(ulong address) where T : unmanaged, IBinaryInteger, IMinMaxValue + { + if (!TryRead(address, _config.IsLittleEndian, _reader, out T value)) throw new InvalidOperationException($"Failed to read {typeof(T)} at 0x{address:x8}."); return value; } - public bool TryRead(ulong address, out T value) where T : unmanaged, IBinaryInteger, IMinMaxValue - => TryRead(address, _config.IsLittleEndian, _reader, out value); - private static bool TryRead(ulong address, bool isLittleEndian, Reader reader, out T value) where T : unmanaged, IBinaryInteger, IMinMaxValue { value = default; @@ -190,15 +245,12 @@ private static bool IsSigned() where T : struct, INumberBase, IMinMaxValue public TargetPointer ReadPointer(ulong address) { - if (!TryReadPointer(address, out TargetPointer pointer)) + if (!TryReadPointer(address, _config, _reader, out TargetPointer pointer)) throw new InvalidOperationException($"Failed to read pointer at 0x{address:x8}."); return pointer; } - public bool TryReadPointer(ulong address, out TargetPointer pointer) - => TryReadPointer(address, _config, _reader, out pointer); - private static bool TryReadPointer(ulong address, Configuration config, Reader reader, out TargetPointer pointer) { pointer = TargetPointer.Null; @@ -283,6 +335,58 @@ public bool TryReadGlobalPointer(string name, out TargetPointer pointer) return true; } + public TypeInfo GetTypeInfo(DataType type) + { + if (!_knownTypes.TryGetValue(type, out TypeInfo typeInfo)) + throw new InvalidOperationException($"Failed to get type info for '{type}'"); + + return typeInfo; + } + + public TypeInfo GetTypeInfo(string type) + { + if (_types.TryGetValue(type, out TypeInfo typeInfo)) + return typeInfo; + + DataType dataType = GetDataType(type); + if (dataType is not DataType.Unknown) + return GetTypeInfo(dataType); + + throw new InvalidOperationException($"Failed to get type info for '{type}'"); + } + + internal bool TryGetContractVersion(string contractName, out int version) + => _contracts.TryGetValue(contractName, out version); + + /// + /// Store of addresses that have already been read into corresponding data models. + /// This is simply used to avoid re-processing data on every request. + /// + internal sealed class DataCache + { + private readonly Dictionary<(ulong, Type), object?> _readDataByAddress = []; + + public bool TryRegister(ulong address, T data) + { + return _readDataByAddress.TryAdd((address, typeof(T)), data); + } + + public bool TryGet(ulong address, [NotNullWhen(true)] out T? data) + { + data = default; + if (!_readDataByAddress.TryGetValue((address, typeof(T)), out object? dataObj)) + return false; + + if (dataObj is T dataMaybe) + { + data = dataMaybe; + return true; + } + + return false; + } + } + private sealed class Reader { private readonly delegate* unmanaged _readFromTarget; diff --git a/src/native/managed/cdacreader/tests/TargetTests.cs b/src/native/managed/cdacreader/tests/TargetTests.cs index 5a8569c1b557c..359577cef8bd5 100644 --- a/src/native/managed/cdacreader/tests/TargetTests.cs +++ b/src/native/managed/cdacreader/tests/TargetTests.cs @@ -3,6 +3,7 @@ using System; using System.Buffers.Binary; +using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -17,6 +18,86 @@ public unsafe class TargetTests private const uint JsonDescriptorAddr = 0xdddddddd; private const uint PointerDataAddr = 0xeeeeeeee; + private static readonly (DataType Type, Target.TypeInfo Info)[] TestTypes = + [ + // Size and fields + (DataType.Thread, new(){ + Size = 56, + Fields = { + { "Field1", new(){ Offset = 8, Type = DataType.uint16, TypeName = DataType.uint16.ToString() }}, + { "Field2", new(){ Offset = 16, Type = DataType.GCHandle, TypeName = DataType.GCHandle.ToString() }}, + { "Field3", new(){ Offset = 32 }} + }}), + // Fields only + (DataType.ThreadStore, new(){ + Fields = { + { "Field1", new(){ Offset = 0, TypeName = "FieldType" }}, + { "Field2", new(){ Offset = 8 }} + }}), + // Size only + (DataType.GCHandle, new(){ + Size = 8 + }) + ]; + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void GetTypeInfo(bool isLittleEndian, bool is64Bit) + { + string typesJson = string.Join(',', TestTypes.Select(t => GetTypeJson(t.Type.ToString(), t.Info))); + byte[] json = Encoding.UTF8.GetBytes($$""" + { + "version": 0, + "baseline": "empty", + "contracts": {}, + "types": { {{typesJson}} }, + "globals": {} + } + """); + Span descriptor = stackalloc byte[ContractDescriptor.Size(is64Bit)]; + ContractDescriptor.Fill(descriptor, isLittleEndian, is64Bit, json.Length, 0); + fixed (byte* jsonPtr = json) + { + ReadContext context = new ReadContext + { + ContractDescriptor = (byte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(descriptor)), + ContractDescriptorLength = descriptor.Length, + JsonDescriptor = jsonPtr, + JsonDescriptorLength = json.Length, + }; + + bool success = Target.TryCreate(ContractDescriptorAddr, &ReadFromTarget, &context, out Target? target); + Assert.True(success); + + foreach ((DataType type, Target.TypeInfo info) in TestTypes) + { + { + // By known type + Target.TypeInfo actual = target.GetTypeInfo(type); + Assert.Equal(info.Size, actual.Size); + Assert.Equal(info.Fields, actual.Fields); + } + { + // By name + Target.TypeInfo actual = target.GetTypeInfo(type.ToString()); + Assert.Equal(info.Size, actual.Size); + Assert.Equal(info.Fields, actual.Fields); + } + } + } + + static string GetTypeJson(string name, Target.TypeInfo info) + { + string ret = string.Empty; + List fields = info.Size is null ? [] : [$"\"!\":{info.Size}"]; + fields.AddRange(info.Fields.Select(f => $"\"{f.Key}\":{(f.Value.TypeName is null ? f.Value.Offset : $"[{f.Value.Offset},\"{f.Value.TypeName}\"]")}")); + return $"\"{name}\":{{{string.Join(',', fields)}}}"; + } + } + private static readonly (string Name, ulong Value, string? Type)[] TestGlobals = [ ("value", (ulong)sbyte.MaxValue, null),