diff --git a/docs/design/datacontracts/ExecutionManager.md b/docs/design/datacontracts/ExecutionManager.md index 655feda2e2a96..8504456e1d749 100644 --- a/docs/design/datacontracts/ExecutionManager.md +++ b/docs/design/datacontracts/ExecutionManager.md @@ -56,15 +56,20 @@ Data descriptors used: | `Module` | `ReadyToRunInfo` | Pointer to the `ReadyToRunInfo` for the module | | `ReadyToRunInfo` | `CompositeInfo` | Pointer to composite R2R info - or itself for non-composite | | `ReadyToRunInfo` | `NumRuntimeFunctions` | Number of `RuntimeFunctions` | -| `ReadyToRunInfo` | `RuntimeFunctions` | Pointer to an array of `RuntimeFunctions` | +| `ReadyToRunInfo` | `RuntimeFunctions` | Pointer to an array of `RuntimeFunctions` - [see R2R format](../coreclr/botr/readytorun-format.md#readytorunsectiontyperuntimefunctions)| +| `ReadyToRunInfo` | `NumHotColdMap` | Number of entries in the `HotColdMap` | +| `ReadyToRunInfo` | `HotColdMap` | Pointer to an array of 32-bit integers - [see R2R format](../coreclr/botr/readytorun-format.md#readytorunsectiontypehotcoldmap-v80) | | `ReadyToRunInfo` | `DelayLoadMethodCallThunks` | Pointer to an `ImageDataDirectory` for the delay load method call thunks | | `ReadyToRunInfo` | `EntryPointToMethodDescMap` | `HashMap` of entry point addresses to `MethodDesc` pointers | | `ImageDataDirectory` | `VirtualAddress` | Virtual address of the image data directory | | `ImageDataDirectory` | `Size` | Size of the data | | `RuntimeFunction` | `BeginAddress` | Begin address of the function | +| `RuntimeFunction` | `EndAddress` | End address of the function. Only exists on some platforms | +| `RuntimeFunction` | `UnwindData` | Pointer to the unwind info for the function | | `HashMap` | `Buckets` | Pointer to the buckets of a `HashMap` | | `Bucket` | `Keys` | Array of keys of `HashMapSlotsPerBucket` length | | `Bucket` | `Values` | Array of values of `HashMapSlotsPerBucket` length | +| `UnwindInfo` | `FunctionLength` | Length of the associated function in bytes. Only exists on some platforms | Global variables used: | Global Name | Type | Purpose | @@ -80,67 +85,52 @@ Contracts used: | --- | | `PlatformMetadata` | -The bulk of the work is done by the `GetCodeBlockHandle` API that maps a code pointer to information about the containing jitted method. +The bulk of the work is done by the `GetCodeBlockHandle` API that maps a code pointer to information about the containing jitted method. This relies the [range section lookup](#rangesectionmap). ```csharp private CodeBlock? GetCodeBlock(TargetCodePointer jittedCodeAddress) { - RangeSection range = RangeSection.Find(_topRangeSectionMap, jittedCodeAddress); - if (range.Data == null) - { + TargetPointer rangeSection = // find range section corresponding to jittedCodeAddress - see RangeSectionMap below + if (/* no corresponding range section */) return null; - } + JitManager jitManager = GetJitManager(range.Data); - if (jitManager.GetMethodInfo(range, jittedCodeAddress, out CodeBlock? info)) - { + if (/* JIT manager corresponding to rangeSection */.GetMethodInfo(range, jittedCodeAddress, out CodeBlock? info)) return info; - } - else - { - return null; - } + return null; } CodeBlockHandle? IExecutionManager.GetCodeBlockHandle(TargetCodePointer ip) { - TargetPointer key = ip.AsTargetPointer; - if (/*cache*/.ContainsKey(key)) - { - return new CodeBlockHandle(key); - } CodeBlock? info = GetCodeBlock(ip); - if (info == null || !info.Valid) - { + if (info == null) return null; - } - /*cache*/.TryAdd(key, info); - return new CodeBlockHandle(key); + return new CodeBlockHandle(ip.AsTargetPointer); } ``` -Here `RangeSection.Find` implements the range section lookup, summarized below. - -There are two `JitManager`s: the "EE JitManager" for jitted code and "R2R JitManager" for ReadyToRun code. +There are two JIT managers: the "EE JitManager" for jitted code and "R2R JitManager" for ReadyToRun code. The EE JitManager `GetMethodInfo` implements the nibble map lookup, summarized below, followed by returning the `RealCodeHeader` data: ```csharp -bool GetMethodInfo(RangeSection rangeSection, TargetCodePointer jittedCodeAddress, [NotNullWhen(true)] out CodeBlock? info) +bool GetMethodInfo(TargetPointer rangeSection, TargetCodePointer jittedCodeAddress, [NotNullWhen(true)] out CodeBlock? info) { - TargetPointer start = FindMethodCode(rangeSection, jittedCodeAddress); // nibble map lookup + info = default; + TargetPointer start = // look up jittedCodeAddress in nibble map for rangeSection - see NibbleMap below if (start == TargetPointer.Null) - { return false; - } + TargetNUInt relativeOffset = jittedCodeAddress - start; int codeHeaderOffset = Target.PointerSize; TargetPointer codeHeaderIndirect = start - codeHeaderOffset; - if (RangeSection.IsStubCodeBlock(Target, codeHeaderIndirect)) - { + + // Check if address is in a stub code block + if (codeHeaderIndirect < Target.ReadGlobal("StubCodeBlockLast")) return false; - } + TargetPointer codeHeaderAddress = Target.ReadPointer(codeHeaderIndirect); - Data.RealCodeHeader realCodeHeader = Target.ProcessedData.GetOrAdd(codeHeaderAddress); - info = new CodeBlock(jittedCodeAddress, realCodeHeader.MethodDesc, relativeOffset, rangeSection.Data!.JitManager); + TargetPointer methodDesc = Target.ReadPointer(codeHeaderAddress + /* RealCodeHeader::MethodDesc offset */); + info = new CodeBlock(jittedCodeAddress, realCodeHeader.MethodDesc, relativeOffset); return true; } ``` @@ -148,35 +138,31 @@ bool GetMethodInfo(RangeSection rangeSection, TargetCodePointer jittedCodeAddres The R2R JitManager `GetMethodInfo` finds the runtime function corresponding to an address and maps its entry point pack to a method: ```csharp -bool GetMethodInfo(RangeSection rangeSection, TargetCodePointer jittedCodeAddress, [NotNullWhen(true)] out CodeBlock? info) +bool GetMethodInfo(TargetPointer rangeSection, TargetCodePointer jittedCodeAddress, [NotNullWhen(true)] out CodeBlock? info) { - if (rangeSection.Data == null) - throw new ArgumentException(nameof(rangeSection)); - info = default; TargetPointer r2rModule = Target.ReadPointer(/* range section address + RangeSection::R2RModule offset */); TargetPointer r2rInfo = Target.ReadPointer(r2rModule + /* Module::ReadyToRunInfo offset */); // Check if address is in a thunk - if (IsStubCodeBlockThunk(rangeSection.Data, r2rInfo, jittedCodeAddress)) + if (/* jittedCodeAddress is in ReadyToRunInfo::DelayLoadMethodCallThunks */) return false; // Find the relative address that we are looking for - TargetCodePointer code = /* code pointer from jittedCodeAddress using PlatformMetadata.GetCodePointerFlags */ + TargetCodePointer addr = /* code pointer from jittedCodeAddress using PlatformMetadata.GetCodePointerFlags */ TargetPointer imageBase = Target.ReadPointer(/* range section address + RangeSection::RangeBegin offset */); - TargetPointer relativeAddr = code - imageBase; + TargetPointer relativeAddr = addr - imageBase; TargetPointer runtimeFunctions = Target.ReadPointer(r2rInfo + /* ReadyToRunInfo::RuntimeFunctions offset */); int index = // Iterate through runtimeFunctions and find index of function with relativeAddress if (index < 0) return false; - bool featureEHFunclets = Target.ReadGlobal(Constants.Globals.FeatureEHFunclets) != 0; + bool featureEHFunclets = Target.ReadGlobal("FeatureEHFunclets") != 0; if (featureEHFunclets) { - // TODO: [cdac] Look up in hot/cold mapping lookup table and if the method is in the cold block, - // get the index of the associated hot block. + index = // look up hot part index in the hot/cold map } TargetPointer function = runtimeFunctions + (ulong)(index * /* size of RuntimeFunction */); @@ -186,11 +172,22 @@ bool GetMethodInfo(RangeSection rangeSection, TargetCodePointer jittedCodeAddres TargetPointer mapAddress = r2rInfo + /* ReadyToRunInfo::EntryPointToMethodDescMap offset */; TargetPointer methodDesc = /* look up entryPoint in HashMap at mapAddress */; + while (featureEHFunclets && methodDesc == TargetPointer.Null) + { + index--; + methodDesc = /* re-compute entryPoint based on updated index and look up in HashMap at mapAddress */ + } - // TODO: [cdac] Handle method with cold code when computing relative offset TargetNUInt relativeOffset = new TargetNUInt(code - startAddress); + if (/* function has cold part and addr is in the cold part*/) + { + uint coldIndex = // look up cold part in hot/cold map + TargetPointer coldFunction = runtimeFunctions + (ulong)(coldIndex * /* size of RuntimeFunction */); + TargetPointer coldStart = imageBase + Target.Read(function + /* RuntimeFunction::BeginAddress offset */); + relativeOffset = /* function length of hot part */ + addr - coldStart; + } - info = new CodeBlock(startAddress.Value, methodDesc, relativeOffset, rangeSection.Data!.JitManager); + info = new CodeBlock(startAddress.Value, methodDesc, relativeOffset); return true; } ``` @@ -204,19 +201,16 @@ class CodeBlock public TargetCodePointer StartAddress { get; } public TargetPointer MethodDesc { get; } - public TargetPointer JitManagerAddress { get; } public TargetNUInt RelativeOffset { get; } - public CodeBlock(TargetCodePointer startAddress, TargetPointer methodDesc, TargetNUInt relativeOffset, TargetPointer jitManagerAddress) + public CodeBlock(TargetCodePointer startAddress, TargetPointer methodDesc, TargetNUInt relativeOffset) { StartAddress = startAddress; MethodDesc = methodDesc; RelativeOffset = relativeOffset; - JitManagerAddress = jitManagerAddress; } public TargetPointer MethodDescAddress => _codeHeaderData.MethodDesc; - public bool Valid => JitManagerAddress != TargetPointer.Null; } ``` diff --git a/src/coreclr/debug/runtimeinfo/datadescriptor.h b/src/coreclr/debug/runtimeinfo/datadescriptor.h index 4568395df3cac..77e9221f05b34 100644 --- a/src/coreclr/debug/runtimeinfo/datadescriptor.h +++ b/src/coreclr/debug/runtimeinfo/datadescriptor.h @@ -418,6 +418,8 @@ CDAC_TYPE_INDETERMINATE(ReadyToRunInfo) CDAC_TYPE_FIELD(ReadyToRunInfo, /*pointer*/, CompositeInfo, cdac_data::CompositeInfo) CDAC_TYPE_FIELD(ReadyToRunInfo, /*uint32*/, NumRuntimeFunctions, cdac_data::NumRuntimeFunctions) CDAC_TYPE_FIELD(ReadyToRunInfo, /*pointer*/, RuntimeFunctions, cdac_data::RuntimeFunctions) +CDAC_TYPE_FIELD(ReadyToRunInfo, /*uint32*/, NumHotColdMap, cdac_data::NumHotColdMap) +CDAC_TYPE_FIELD(ReadyToRunInfo, /*pointer*/, HotColdMap, cdac_data::HotColdMap) CDAC_TYPE_FIELD(ReadyToRunInfo, /*pointer*/, DelayLoadMethodCallThunks, cdac_data::DelayLoadMethodCallThunks) CDAC_TYPE_FIELD(ReadyToRunInfo, /*HashMap*/, EntryPointToMethodDescMap, cdac_data::EntryPointToMethodDescMap) CDAC_TYPE_END(ReadyToRunInfo) @@ -431,8 +433,19 @@ CDAC_TYPE_END(ImageDataDirectory) CDAC_TYPE_BEGIN(RuntimeFunction) CDAC_TYPE_SIZE(sizeof(RUNTIME_FUNCTION)) CDAC_TYPE_FIELD(RuntimeFunction, /*uint32*/, BeginAddress, offsetof(RUNTIME_FUNCTION, BeginAddress)) +#ifdef TARGET_AMD64 +CDAC_TYPE_FIELD(RuntimeFunction, /*uint32*/, EndAddress, offsetof(RUNTIME_FUNCTION, EndAddress)) +#endif +CDAC_TYPE_FIELD(RuntimeFunction, /*uint32*/, UnwindData, offsetof(RUNTIME_FUNCTION, UnwindData)) CDAC_TYPE_END(RuntimeFunction) +CDAC_TYPE_BEGIN(UnwindInfo) +CDAC_TYPE_INDETERMINATE(UnwindInfo) +#ifdef TARGET_X86 +CDAC_TYPE_FIELD(UnwindInfo, /*uint32*/, FunctionLength, offsetof(UNWIND_INFO, FunctionLength)) +#endif +CDAC_TYPE_END(UnwindInfo) + CDAC_TYPE_BEGIN(HashMap) CDAC_TYPE_INDETERMINATE(HashMap) CDAC_TYPE_FIELD(HashMap, /*pointer*/, Buckets, cdac_data::Buckets) diff --git a/src/coreclr/vm/readytoruninfo.h b/src/coreclr/vm/readytoruninfo.h index 2777d5fbc98e3..74456a6e82aa1 100644 --- a/src/coreclr/vm/readytoruninfo.h +++ b/src/coreclr/vm/readytoruninfo.h @@ -347,6 +347,8 @@ struct cdac_data static constexpr size_t CompositeInfo = offsetof(ReadyToRunInfo, m_pCompositeInfo); static constexpr size_t NumRuntimeFunctions = offsetof(ReadyToRunInfo, m_nRuntimeFunctions); static constexpr size_t RuntimeFunctions = offsetof(ReadyToRunInfo, m_pRuntimeFunctions); + static constexpr size_t NumHotColdMap = offsetof(ReadyToRunInfo, m_nHotColdMap); + static constexpr size_t HotColdMap = offsetof(ReadyToRunInfo, m_pHotColdMap); static constexpr size_t DelayLoadMethodCallThunks = offsetof(ReadyToRunInfo, m_pSectionDelayLoadMethodCallThunks); static constexpr size_t EntryPointToMethodDescMap = offsetof(ReadyToRunInfo, m_entryPointToMethodDescMap); }; diff --git a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs index 6e50d85af40fd..994dec03a67ab 100644 --- a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs +++ b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExecutionManager.cs @@ -17,7 +17,6 @@ internal interface IExecutionManager : IContract CodeBlockHandle? GetCodeBlockHandle(TargetCodePointer ip) => throw new NotImplementedException(); TargetPointer GetMethodDesc(CodeBlockHandle codeInfoHandle) => throw new NotImplementedException(); TargetCodePointer GetStartAddress(CodeBlockHandle codeInfoHandle) => throw new NotImplementedException(); - } internal readonly struct ExecutionManager : IExecutionManager diff --git a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs index 71076275b7fee..98190f5308a8f 100644 --- a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs +++ b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs @@ -72,4 +72,5 @@ public enum DataType RuntimeFunction, HashMap, Bucket, + UnwindInfo, } diff --git a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerBase.ReadyToRunJitManager.cs b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerBase.ReadyToRunJitManager.cs index 68bc4c7493314..8390469e1d76c 100644 --- a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerBase.ReadyToRunJitManager.cs +++ b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerBase.ReadyToRunJitManager.cs @@ -13,12 +13,16 @@ internal partial class ExecutionManagerBase : IExecutionManager private class ReadyToRunJitManager : JitManager { private readonly uint _runtimeFunctionSize; - private readonly PtrHashMapLookup _lookup; + private readonly PtrHashMapLookup _hashMap; + private readonly HotColdLookup _hotCold; + private readonly RuntimeFunctionLookup _runtimeFunctions; public ReadyToRunJitManager(Target target) : base(target) { _runtimeFunctionSize = Target.GetTypeInfo(DataType.RuntimeFunction).Size!.Value; - _lookup = PtrHashMapLookup.Create(target); + _hashMap = PtrHashMapLookup.Create(target); + _hotCold = HotColdLookup.Create(target); + _runtimeFunctions = RuntimeFunctionLookup.Create(target); } public override bool GetMethodInfo(RangeSection rangeSection, TargetCodePointer jittedCodeAddress, [NotNullWhen(true)] out CodeBlock? info) @@ -43,35 +47,49 @@ public override bool GetMethodInfo(RangeSection rangeSection, TargetCodePointer TargetPointer imageBase = rangeSection.Data.RangeBegin; TargetPointer relativeAddr = addr - imageBase; - int index = GetRuntimeFunctionIndexForAddress(r2rInfo, relativeAddr); - if (index < 0) + uint index; + if (!_runtimeFunctions.TryGetRuntimeFunctionIndexForAddress(r2rInfo.RuntimeFunctions, r2rInfo.NumRuntimeFunctions, relativeAddr, out index)) return false; bool featureEHFunclets = Target.ReadGlobal(Constants.Globals.FeatureEHFunclets) != 0; if (featureEHFunclets) { - // TODO: [cdac] Look up in hot/cold mapping lookup table and if the method is in the cold block, - // get the index of the associated hot block. - // HotColdMappingLookupTable::LookupMappingForMethod - // - // while GetMethodDescForEntryPoint for the begin address of function at index is null - // index-- + // Look up index in hot/cold map - if the function is in the cold part, get the index of the hot part. + index = _hotCold.GetHotFunctionIndex(r2rInfo.NumHotColdMap, r2rInfo.HotColdMap, index); + Debug.Assert(index < r2rInfo.NumRuntimeFunctions); } - TargetPointer functionEntry = r2rInfo.RuntimeFunctions + (ulong)(index * _runtimeFunctionSize); - Data.RuntimeFunction function = Target.ProcessedData.GetOrAdd(functionEntry); - - // ReadyToRunInfo::GetMethodDescForEntryPointInNativeImage - TargetCodePointer startAddress = imageBase + function.BeginAddress; - TargetPointer entryPoint = CodePointerUtils.AddressFromCodePointer(startAddress, Target); + TargetPointer methodDesc = GetMethodDescForRuntimeFunction(r2rInfo, imageBase, index); + while (featureEHFunclets && methodDesc == TargetPointer.Null) + { + // Funclets won't have a direct entry in the map of runtime function entry point to method desc. + // The funclet's address (and index) will be greater than that of the corresponding function, so + // we decrement the index to find the actual function / method desc for the funclet. + index--; + methodDesc = GetMethodDescForRuntimeFunction(r2rInfo, imageBase, index); + } - TargetPointer methodDesc = _lookup.GetValue(r2rInfo.EntryPointToMethodDescMap, entryPoint); Debug.Assert(methodDesc != TargetPointer.Null); + Data.RuntimeFunction function = _runtimeFunctions.GetRuntimeFunction(r2rInfo.RuntimeFunctions, index); - // TODO: [cdac] Handle method with cold code when computing relative offset - // ReadyToRunJitManager::JitTokenToMethodRegionInfo + TargetCodePointer startAddress = imageBase + function.BeginAddress; TargetNUInt relativeOffset = new TargetNUInt(addr - startAddress); + // Take hot/cold splitting into account for the relative offset + if (_hotCold.TryGetColdFunctionIndex(r2rInfo.NumHotColdMap, r2rInfo.HotColdMap, index, out uint coldFunctionIndex)) + { + Debug.Assert(coldFunctionIndex < r2rInfo.NumRuntimeFunctions); + Data.RuntimeFunction coldFunction = _runtimeFunctions.GetRuntimeFunction(r2rInfo.RuntimeFunctions, coldFunctionIndex); + TargetPointer coldStart = imageBase + coldFunction.BeginAddress; + if (addr >= coldStart) + { + // If the address is in the cold part, the relative offset is the size of the + // hot part plus the offset from the address to the start of the cold part + uint hotSize = _runtimeFunctions.GetFunctionLength(function); + relativeOffset = new TargetNUInt(hotSize + addr - coldStart); + } + } + info = new CodeBlock(startAddress.Value, methodDesc, relativeOffset, rangeSection.Data!.JitManager); return true; } @@ -87,51 +105,19 @@ private bool IsStubCodeBlockThunk(Data.RangeSection rangeSection, Data.ReadyToRu return thunksData.VirtualAddress <= rva && rva < thunksData.VirtualAddress + thunksData.Size; } - private int GetRuntimeFunctionIndexForAddress(Data.ReadyToRunInfo r2rInfo, TargetPointer relativeAddress) + private TargetPointer GetMethodDescForRuntimeFunction(Data.ReadyToRunInfo r2rInfo, TargetPointer imageBase, uint runtimeFunctionIndex) { - // NativeUnwindInfoLookupTable::LookupUnwindInfoForMethod - uint start = 0; - uint end = r2rInfo.NumRuntimeFunctions - 1; - relativeAddress = CodePointerUtils.CodePointerFromAddress(relativeAddress, Target).AsTargetPointer; - - // Entries are sorted. Binary search until we get to 10 or fewer items. - while (end - start > 10) - { - uint middle = start + (end - start) / 2; - Data.RuntimeFunction func = GetRuntimeFunction(r2rInfo, middle); - if (relativeAddress < func.BeginAddress) - { - end = middle - 1; - } - else - { - start = middle; - } - } + Data.RuntimeFunction function = _runtimeFunctions.GetRuntimeFunction(r2rInfo.RuntimeFunctions, runtimeFunctionIndex); - // Find the runtime function that contains the address of interest - for (uint i = start; i <= end; ++i) - { - // Entries are terminated by a sentinel value of -1, so we can index one past the end safely. - // Read as a runtime function, its begin address is 0xffffffff (always > relative address). - // See RuntimeFunctionsTableNode.GetData in RuntimeFunctionsTableNode.cs - Data.RuntimeFunction nextFunc = GetRuntimeFunction(r2rInfo, i + 1); - if (relativeAddress >= nextFunc.BeginAddress) - continue; - - Data.RuntimeFunction func = GetRuntimeFunction(r2rInfo, i); - if (relativeAddress >= func.BeginAddress) - return (int)i; - } + // ReadyToRunInfo::GetMethodDescForEntryPointInNativeImage + TargetCodePointer startAddress = imageBase + function.BeginAddress; + TargetPointer entryPoint = CodePointerUtils.AddressFromCodePointer(startAddress, Target); - return -1; - } + TargetPointer methodDesc = _hashMap.GetValue(r2rInfo.EntryPointToMethodDescMap, entryPoint); + if (methodDesc == (ulong)HashMapLookup.SpecialKeys.InvalidEntry) + return TargetPointer.Null; - private Data.RuntimeFunction GetRuntimeFunction(Data.ReadyToRunInfo r2rInfo, uint index) - { - TargetPointer first = r2rInfo.RuntimeFunctions; - TargetPointer addr = first + (ulong)(index * _runtimeFunctionSize); - return Target.ProcessedData.GetOrAdd(addr); + return methodDesc; } } } diff --git a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/BinaryThenLinearSearch.cs b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/BinaryThenLinearSearch.cs new file mode 100644 index 0000000000000..9fc0509bb064a --- /dev/null +++ b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/BinaryThenLinearSearch.cs @@ -0,0 +1,46 @@ +// 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.ExecutionManagerHelpers; + +internal static class BinaryThenLinearSearch +{ + private const uint BinarySearchCountThreshold = 10; + + public static bool Search( + uint start, + uint end, + Func compare, + Func match, + out uint index) + { + // Binary search until we get to a fewer than the threshold number of items. + while (end - start > BinarySearchCountThreshold) + { + uint middle = start + (end - start) / 2; + if (compare(middle)) + { + end = middle - 1; + } + else + { + start = middle; + } + } + + // Linear search over the remaining items + for (uint i = start; i <= end; ++i) + { + if (!match(i)) + continue; + + index = i; + return true; + } + + index = ~0u; + return false; + } +} diff --git a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/HotColdLookup.cs b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/HotColdLookup.cs new file mode 100644 index 0000000000000..d2bea5f7eb3c2 --- /dev/null +++ b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/HotColdLookup.cs @@ -0,0 +1,127 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace Microsoft.Diagnostics.DataContractReader.ExecutionManagerHelpers; + +internal sealed class HotColdLookup +{ + public static HotColdLookup Create(Target target) + => new HotColdLookup(target); + + private readonly Target _target; + + private HotColdLookup(Target target) + { + _target = target; + } + + public uint GetHotFunctionIndex(uint numHotColdMap, TargetPointer hotColdMap, uint runtimeFunctionIndex) + { + if (!IsColdCode(numHotColdMap, hotColdMap, runtimeFunctionIndex)) + return runtimeFunctionIndex; + + uint hotIndex; + if (!TryLookupHotColdMappingForMethod(numHotColdMap, hotColdMap, runtimeFunctionIndex, out hotIndex, out _)) + return runtimeFunctionIndex; + + // If runtime function is in the cold part, get the associated hot part + Debug.Assert(hotIndex % 2 != 0, "Hot part index should be an odd number"); + return _target.Read(hotColdMap + (ulong)hotIndex * sizeof(uint)); + } + + public bool TryGetColdFunctionIndex(uint numHotColdMap, TargetPointer hotColdMap, uint runtimeFunctionIndex, out uint functionIndex) + { + functionIndex = ~0u; + + uint coldIndex; + if (!TryLookupHotColdMappingForMethod(numHotColdMap, hotColdMap, runtimeFunctionIndex, out uint _, out coldIndex)) + return false; + + Debug.Assert(coldIndex % 2 == 0, "Cold part index should be an even number"); + functionIndex = _target.Read(hotColdMap + (ulong)coldIndex * sizeof(uint)); + return true; + } + + private bool IsColdCode(uint numHotColdMap, TargetPointer hotColdMap, uint runtimeFunctionIndex) + { + if (numHotColdMap == 0) + return false; + + // Determine if the method index represents a hot or cold part by comparing against the first + // cold part index (hot < cold). + uint firstColdRuntimeFunctionIndex = _target.Read(hotColdMap); + return runtimeFunctionIndex >= firstColdRuntimeFunctionIndex; + } + + // Look up a runtime function index in the hot/cold map. If the function is in the + // hot/cold map, return whether the function corresponds to cold code and the hot + // and cold lookup indexes for the function. + private bool TryLookupHotColdMappingForMethod( + uint numHotColdMap, + TargetPointer hotColdMap, + uint runtimeFunctionIndex, + out uint hotIndex, + out uint coldIndex) + { + hotIndex = ~0u; + coldIndex = ~0u; + + // HotColdMappingLookupTable::LookupMappingForMethod + if (numHotColdMap == 0) + return false; + + // Each method is represented by a pair of unsigned 32-bit integers. First is the runtime + // function index of the cold part, second is the runtime function index of the hot part. + // HotColdMap is these pairs as an array, so the logical size is half the array size. + uint start = 0; + uint end = (numHotColdMap - 1) / 2; + + bool isColdCode = IsColdCode(numHotColdMap, hotColdMap, runtimeFunctionIndex); + uint indexCorrection = isColdCode ? 0u : 1u; + + // Index used for the search is the logical index of hot/cold pairs. We double it to index + // into the HotColdMap array. + if (BinaryThenLinearSearch.Search(start, end, Compare, Match, out uint index)) + { + hotIndex = index * 2 + 1; + coldIndex = index * 2; + return true; + } + + return false; + + bool Compare(uint index) + { + index = index * 2 + indexCorrection; + return runtimeFunctionIndex < _target.Read(hotColdMap + (index * sizeof(uint))); + } + + bool Match(uint index) + { + index *= 2; + + uint value = _target.Read(hotColdMap + (ulong)(index + indexCorrection) * sizeof(uint)); + if (value == runtimeFunctionIndex) + return true; + + // If function index is a cold funclet from a cold block, the above check for equality will fail. + // To get its corresponding hot block, find the cold block containing the funclet, + // then use the lookup table. + // The cold funclet's function index will be greater than its cold block's function index, + // but less than the next cold block's function index in the lookup table. + if (isColdCode && runtimeFunctionIndex > _target.Read(hotColdMap + (ulong)index * sizeof(uint))) + { + bool isFuncletIndex = index + 2 == numHotColdMap + || runtimeFunctionIndex < _target.Read(hotColdMap + (ulong)(index + 2) * sizeof(uint)); + if (isFuncletIndex) + { + return true; + } + } + + return false; + } + } +} diff --git a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/RuntimeFunctionLookup.cs b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/RuntimeFunctionLookup.cs new file mode 100644 index 0000000000000..e8dbbf6d22090 --- /dev/null +++ b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/RuntimeFunctionLookup.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace Microsoft.Diagnostics.DataContractReader.ExecutionManagerHelpers; + +internal sealed class RuntimeFunctionLookup +{ + public static RuntimeFunctionLookup Create(Target target) + => new RuntimeFunctionLookup(target); + + private readonly uint _runtimeFunctionSize; + private readonly Target _target; + + private RuntimeFunctionLookup(Target target) + { + _target = target; + _runtimeFunctionSize = target.GetTypeInfo(DataType.RuntimeFunction).Size!.Value; + } + + public uint GetFunctionLength(Data.RuntimeFunction function) + { + if (function.EndAddress.HasValue) + return function.EndAddress.Value - function.BeginAddress; + + Data.UnwindInfo unwindInfo = _target.ProcessedData.GetOrAdd(function.UnwindData); + if (unwindInfo.FunctionLength.HasValue) + return unwindInfo.FunctionLength.Value; + + Debug.Assert(unwindInfo.Header.HasValue); + + // First 18 bits are function length / (pointer size / 2). + // See UnwindFragmentInfo::Finalize + uint funcLengthInHeader = unwindInfo.Header.Value & ((1 << 18) - 1); + return (uint)(funcLengthInHeader * (_target.PointerSize / 2)); + } + + public bool TryGetRuntimeFunctionIndexForAddress(TargetPointer runtimeFunctions, uint numRuntimeFunctions, TargetPointer relativeAddress, out uint index) + { + // NativeUnwindInfoLookupTable::LookupUnwindInfoForMethod + uint start = 0; + uint end = numRuntimeFunctions - 1; + relativeAddress = CodePointerUtils.CodePointerFromAddress(relativeAddress, _target).AsTargetPointer; + + // Entries are sorted. + return BinaryThenLinearSearch.Search(start, end, Compare, Match, out index); + + bool Compare(uint index) + { + Data.RuntimeFunction func = GetRuntimeFunction(runtimeFunctions, index); + return relativeAddress < func.BeginAddress; + }; + + bool Match(uint index) + { + // Entries are terminated by a sentinel value of -1, so we can index one past the end safely. + // Read as a runtime function, its begin address is 0xffffffff (always > relative address). + // See RuntimeFunctionsTableNode.GetData in RuntimeFunctionsTableNode.cs + Data.RuntimeFunction nextFunc = GetRuntimeFunction(runtimeFunctions, index + 1); + if (relativeAddress >= nextFunc.BeginAddress) + return false; + + Data.RuntimeFunction func = GetRuntimeFunction(runtimeFunctions, index); + if (relativeAddress >= func.BeginAddress) + return true; + + return false; + } + } + + public Data.RuntimeFunction GetRuntimeFunction(TargetPointer runtimeFunctions, uint index) + { + TargetPointer addr = runtimeFunctions + (ulong)(index * _runtimeFunctionSize); + return _target.ProcessedData.GetOrAdd(addr); + } +} diff --git a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs index e64bf88f26711..01db9d76bd978 100644 --- a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs +++ b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; + namespace Microsoft.Diagnostics.DataContractReader.Data; internal sealed class ReadyToRunInfo : IData @@ -18,7 +20,15 @@ public ReadyToRunInfo(Target target, TargetPointer address) CompositeInfo = target.ReadPointer(address + (ulong)type.Fields[nameof(CompositeInfo)].Offset); NumRuntimeFunctions = target.Read(address + (ulong)type.Fields[nameof(NumRuntimeFunctions)].Offset); - RuntimeFunctions = target.ReadPointer(address + (ulong)type.Fields[nameof(RuntimeFunctions)].Offset); + RuntimeFunctions = NumRuntimeFunctions > 0 + ? target.ReadPointer(address + (ulong)type.Fields[nameof(RuntimeFunctions)].Offset) + : TargetPointer.Null; + + NumHotColdMap = target.Read(address + (ulong)type.Fields[nameof(NumHotColdMap)].Offset); + Debug.Assert(NumHotColdMap % 2 == 0, "Hot/cold map should have an even number of entries (pairs of hot/cold runtime function indexes)"); + HotColdMap = NumHotColdMap > 0 + ? target.ReadPointer(address + (ulong)type.Fields[nameof(HotColdMap)].Offset) + : TargetPointer.Null; DelayLoadMethodCallThunks = target.ReadPointer(address + (ulong)type.Fields[nameof(DelayLoadMethodCallThunks)].Offset); @@ -31,6 +41,9 @@ public ReadyToRunInfo(Target target, TargetPointer address) public uint NumRuntimeFunctions { get; } public TargetPointer RuntimeFunctions { get; } + public uint NumHotColdMap { get; } + public TargetPointer HotColdMap { get; } + public TargetPointer DelayLoadMethodCallThunks { get; } public TargetPointer EntryPointToMethodDescMap { get; } } diff --git a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Data/RuntimeFunction.cs b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Data/RuntimeFunction.cs index a95cc6e7647fe..f87da0de3f07f 100644 --- a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Data/RuntimeFunction.cs +++ b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Data/RuntimeFunction.cs @@ -13,7 +13,15 @@ public RuntimeFunction(Target target, TargetPointer address) Target.TypeInfo type = target.GetTypeInfo(DataType.RuntimeFunction); BeginAddress = target.Read(address + (ulong)type.Fields[nameof(BeginAddress)].Offset); + + // Not all platforms define EndAddress + if (type.Fields.ContainsKey(nameof(EndAddress))) + EndAddress = target.Read(address + (ulong)type.Fields[nameof(EndAddress)].Offset); + + UnwindData = target.Read(address + (ulong)type.Fields[nameof(UnwindData)].Offset); } public uint BeginAddress { get; } + public uint? EndAddress { get; } + public uint UnwindData { get; } } diff --git a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Data/UnwindInfo.cs b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Data/UnwindInfo.cs new file mode 100644 index 0000000000000..add5603fdfd51 --- /dev/null +++ b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Data/UnwindInfo.cs @@ -0,0 +1,29 @@ +// 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 UnwindInfo : IData +{ + static UnwindInfo IData.Create(Target target, TargetPointer address) + => new UnwindInfo(target, address); + + public UnwindInfo(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.UnwindInfo); + + if (type.Fields.ContainsKey(nameof(FunctionLength))) + { + // The unwind info contains the function length on some platforms (x86) + FunctionLength = target.Read(address + (ulong)type.Fields[nameof(FunctionLength)].Offset); + } + else + { + // Otherwise, it starts with a bitfield header + Header = target.Read(address); + } + } + + public uint? FunctionLength { get; } + public uint? Header { get; } +} diff --git a/src/native/managed/cdacreader/tests/ExecutionManager/ExecutionManagerTests.cs b/src/native/managed/cdacreader/tests/ExecutionManager/ExecutionManagerTests.cs index d4ba7eafdc483..5687961764cd9 100644 --- a/src/native/managed/cdacreader/tests/ExecutionManager/ExecutionManagerTests.cs +++ b/src/native/managed/cdacreader/tests/ExecutionManager/ExecutionManagerTests.cs @@ -147,7 +147,7 @@ public void GetCodeBlockHandle_R2R_NoRuntimeFunctionMatch(int version, MockTarge uint runtimeFunction = 0x100; - TargetPointer r2rInfo = emBuilder.AddReadyToRunInfo([runtimeFunction]); + TargetPointer r2rInfo = emBuilder.AddReadyToRunInfo([runtimeFunction], []); MockDescriptors.HashMap hashMapBuilder = new(emBuilder.Builder); hashMapBuilder.PopulatePtrMap( r2rInfo + (uint)emBuilder.Types[DataType.ReadyToRunInfo].Fields[nameof(Data.ReadyToRunInfo.EntryPointToMethodDescMap)].Offset, @@ -182,7 +182,7 @@ public void GetMethodDesc_R2R_OneRuntimeFunction(int version, MockTarget.Archite uint expectedRuntimeFunction = 0x100; - TargetPointer r2rInfo = emBuilder.AddReadyToRunInfo([expectedRuntimeFunction]); + TargetPointer r2rInfo = emBuilder.AddReadyToRunInfo([expectedRuntimeFunction], []); MockDescriptors.HashMap hashMapBuilder = new(emBuilder.Builder); hashMapBuilder.PopulatePtrMap( r2rInfo + (uint)emBuilder.Types[DataType.ReadyToRunInfo].Fields[nameof(Data.ReadyToRunInfo.EntryPointToMethodDescMap)].Offset, @@ -228,7 +228,7 @@ public void GetMethodDesc_R2R_MultipleRuntimeFunctions(int version, MockTarget.A uint[] runtimeFunctions = [ 0x100, 0xc00 ]; - TargetPointer r2rInfo = emBuilder.AddReadyToRunInfo(runtimeFunctions); + TargetPointer r2rInfo = emBuilder.AddReadyToRunInfo(runtimeFunctions, []); MockDescriptors.HashMap hashMapBuilder = new(emBuilder.Builder); hashMapBuilder.PopulatePtrMap( r2rInfo + (uint)emBuilder.Types[DataType.ReadyToRunInfo].Fields[nameof(Data.ReadyToRunInfo.EntryPointToMethodDescMap)].Offset, @@ -277,6 +277,57 @@ public void GetMethodDesc_R2R_MultipleRuntimeFunctions(int version, MockTarget.A } } + [Theory] + [MemberData(nameof(StdArchAllVersions))] + public void GetMethodDesc_R2R_HotColdBlock(int version, MockTarget.Architecture arch) + { + const ulong codeRangeStart = 0x0a0a_0000u; // arbitrary + const uint codeRangeSize = 0xc000u; // arbitrary + TargetPointer jitManagerAddress = new(0x000b_ff00); // arbitrary + + TargetPointer[] methodDescAddresses = [0x0101_aaa0, 0x0201_aaa0]; + + MockDescriptors.ExecutionManager emBuilder = new(version, arch, MockDescriptors.ExecutionManager.DefaultAllocationRange); + var jittedCode = emBuilder.AllocateJittedCodeRange(codeRangeStart, codeRangeSize); + + uint[] runtimeFunctions = [0x100, 0x200, 0x300, 0x400, 0x500]; + uint[] hotColdMap = [3, 0, 4, 1]; + + TargetPointer r2rInfo = emBuilder.AddReadyToRunInfo(runtimeFunctions, hotColdMap); + MockDescriptors.HashMap hashMapBuilder = new(emBuilder.Builder); + hashMapBuilder.PopulatePtrMap( + r2rInfo + (uint)emBuilder.Types[DataType.ReadyToRunInfo].Fields[nameof(Data.ReadyToRunInfo.EntryPointToMethodDescMap)].Offset, + [ + (jittedCode.RangeStart + runtimeFunctions[hotColdMap[1]], methodDescAddresses[0]), + (jittedCode.RangeStart + runtimeFunctions[hotColdMap[3]], methodDescAddresses[1]), + ]); + + TargetPointer r2rModule = emBuilder.AddReadyToRunModule(r2rInfo); + TargetPointer rangeSectionAddress = emBuilder.AddReadyToRunRangeSection(jittedCode, jitManagerAddress, r2rModule); + _ = emBuilder.AddRangeSectionFragment(jittedCode, rangeSectionAddress); + + Target target = CreateTarget(emBuilder); + + IExecutionManager em = target.Contracts.ExecutionManager; + Assert.NotNull(em); + + // Hot and cold parts should map to the same method desc + for (int i = 0; i < hotColdMap.Length; i++) + { + // Function start + var handle = em.GetCodeBlockHandle(codeRangeStart + runtimeFunctions[hotColdMap[i]]); + Assert.NotNull(handle); + TargetPointer actualMethodDesc = em.GetMethodDesc(handle.Value); + Assert.Equal(methodDescAddresses[i / 2], actualMethodDesc); + + // Past function start + handle = em.GetCodeBlockHandle(codeRangeStart + runtimeFunctions[hotColdMap[i]] + 8); + Assert.NotNull(handle); + actualMethodDesc = em.GetMethodDesc(handle.Value); + Assert.Equal(methodDescAddresses[i / 2], actualMethodDesc); + } + } + public static IEnumerable StdArchAllVersions() { const int highestVersion = 2; diff --git a/src/native/managed/cdacreader/tests/ExecutionManager/HotColdLookupTests.cs b/src/native/managed/cdacreader/tests/ExecutionManager/HotColdLookupTests.cs new file mode 100644 index 0000000000000..c0d91f58e07f3 --- /dev/null +++ b/src/native/managed/cdacreader/tests/ExecutionManager/HotColdLookupTests.cs @@ -0,0 +1,199 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Moq; +using Xunit; + +using System; +using Microsoft.Diagnostics.DataContractReader.ExecutionManagerHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.Tests.ExecutionManager; + +public class HotColdLookupTests +{ + private static readonly TargetPointer HotColdMapAddr = 0x1000; // arbitrary + + private static Target CreateMockTarget((uint Cold, uint Hot)[] entries) + { + Mock target = new(); + target.Setup(t => t.Read(It.IsAny())) + .Returns((ulong addr) => + { + for (uint i = 0; i < entries.Length; i++) + { + if (addr == HotColdMapAddr + (i * 2) * sizeof(uint)) + return entries[i].Cold; + + if (addr == HotColdMapAddr + (i * 2 + 1) * sizeof(uint)) + return entries[i].Hot; + } + + throw new NotImplementedException(); + }); + + return target.Object; + } + + [Fact] + public void GetHotFunctionIndex() + { + (uint Cold, uint Hot)[] entries = + [ + (0x100, 0x10), + (0x200, 0x20), + (0x300, 0x30), + (0x400, 0x40), + ]; + + uint numHotColdMap = (uint)entries.Length * 2; + Target target = CreateMockTarget(entries); + + var lookup = HotColdLookup.Create(target); + foreach (var entry in entries) + { + // Hot part as input + uint hotIndex = lookup.GetHotFunctionIndex(numHotColdMap, HotColdMapAddr, entry.Hot); + Assert.Equal(entry.Hot, hotIndex); + + // Cold part as input + hotIndex = lookup.GetHotFunctionIndex(numHotColdMap, HotColdMapAddr, entry.Cold); + Assert.Equal(entry.Hot, hotIndex); + } + } + + [Fact] + public void GetHotFunctionIndex_ColdFunclet() + { + (uint Cold, uint Hot)[] entries = + [ + (0x100, 0x10), + (0x200, 0x20), + ]; + + uint numHotColdMap = (uint)entries.Length * 2; + Target target = CreateMockTarget(entries); + + var lookup = HotColdLookup.Create(target); + + // Cold funclet - between two cold blocks' indexes + uint functionIndex = 0x110; + uint hotIndex = lookup.GetHotFunctionIndex(numHotColdMap, HotColdMapAddr, functionIndex); + Assert.Equal(entries[0].Hot, hotIndex); + } + + [Fact] + public void GetHotFunctionIndex_EmptyMap() + { + Target target = CreateMockTarget([]); + + var lookup = HotColdLookup.Create(target); + + // Function must be hot if there is no map + uint functionIndex = 0x110; + uint hotIndex = lookup.GetHotFunctionIndex(0, HotColdMapAddr, functionIndex); + Assert.Equal(functionIndex, hotIndex); + } + + [Fact] + public void GetHotFunctionIndex_NoEntryInMap() + { + (uint Cold, uint Hot)[] entries = + [ + (0x100, 0x10), + (0x200, 0x20), + ]; + + uint numHotColdMap = (uint)entries.Length * 2; + Target target = CreateMockTarget(entries); + + var lookup = HotColdLookup.Create(target); + + // Function must be hot if it is not in the map + uint functionIndex = 0x30; + uint hotIndex = lookup.GetHotFunctionIndex(numHotColdMap, HotColdMapAddr, functionIndex); + Assert.Equal(functionIndex, hotIndex); + } + + [Fact] + public void TryGetColdFunctionIndex() + { + (uint Cold, uint Hot)[] entries = + [ + (0x100, 0x10), + (0x200, 0x20), + (0x300, 0x30), + (0x400, 0x40), + ]; + + uint numHotColdMap = (uint)entries.Length * 2; + Target target = CreateMockTarget(entries); + + var lookup = HotColdLookup.Create(target); + foreach (var entry in entries) + { + // Hot part as input + bool res = lookup.TryGetColdFunctionIndex(numHotColdMap, HotColdMapAddr, entry.Hot, out uint coldFunctionIndex); + Assert.True(res); + Assert.Equal(entry.Cold, coldFunctionIndex); + + // Cold part as input + res = lookup.TryGetColdFunctionIndex(numHotColdMap, HotColdMapAddr, entry.Cold, out coldFunctionIndex); + Assert.True(res); + Assert.Equal(entry.Cold, coldFunctionIndex); + } + } + + [Fact] + public void TryGetColdFunctionIndex_ColdFunclet() + { + (uint Cold, uint Hot)[] entries = + [ + (0x100, 0x10), + (0x200, 0x20), + ]; + + uint numHotColdMap = (uint)entries.Length * 2; + Target target = CreateMockTarget(entries); + + var lookup = HotColdLookup.Create(target); + + // Cold funclet - between two cold blocks' indexes + uint functionIndex = 0x110; + bool res = lookup.TryGetColdFunctionIndex(numHotColdMap, HotColdMapAddr, functionIndex, out uint coldFunctionIndex); + Assert.True(res); + Assert.Equal(entries[0].Cold, coldFunctionIndex); + } + + [Fact] + public void TryGetColdFunctionIndex_EmptyMap() + { + Target target = CreateMockTarget([]); + + var lookup = HotColdLookup.Create(target); + + // Function has no cold part if map is empty + uint functionIndex = 0x110; + bool res = lookup.TryGetColdFunctionIndex(0, HotColdMapAddr, functionIndex, out _); + Assert.False(res); + } + + [Fact] + public void TryGetColdFunctionIndex_NoEntryInMap() + { + (uint Cold, uint Hot)[] entries = + [ + (0x100, 0x10), + (0x200, 0x20), + ]; + + uint numHotColdMap = (uint)entries.Length * 2; + Target target = CreateMockTarget(entries); + + var lookup = HotColdLookup.Create(target); + + // Function has no cold part if it is not in the map + uint functionIndex = 0x30; + bool res = lookup.TryGetColdFunctionIndex(numHotColdMap, HotColdMapAddr, functionIndex, out _); + Assert.False(res); + } +} diff --git a/src/native/managed/cdacreader/tests/ExecutionManager/RuntimeFunctionTests.cs b/src/native/managed/cdacreader/tests/ExecutionManager/RuntimeFunctionTests.cs new file mode 100644 index 0000000000000..fe256024de09a --- /dev/null +++ b/src/native/managed/cdacreader/tests/ExecutionManager/RuntimeFunctionTests.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +using Microsoft.Diagnostics.DataContractReader.ExecutionManagerHelpers; +using System.Collections.Generic; +using System; +using Moq; + +namespace Microsoft.Diagnostics.DataContractReader.Tests.ExecutionManager; + +public class RuntimeFunctionTests +{ + public static IEnumerable StdArchFunctionLengthData() + { + foreach (object[] arr in new MockTarget.StdArch()) + { + MockTarget.Architecture arch = (MockTarget.Architecture)arr[0]; + yield return new object[] { arch, true /*includeEndAddress*/, false /*unwindInfoIsFunctionLength*/}; + yield return new object[] { arch, true /*includeEndAddress*/, true /*unwindInfoIsFunctionLength*/}; + yield return new object[] { arch, false /*includeEndAddress*/, false /*unwindInfoIsFunctionLength*/}; + yield return new object[] { arch, false /*includeEndAddress*/, true /*unwindInfoIsFunctionLength*/}; + } + } + + [Theory] + [MemberData(nameof(StdArchFunctionLengthData))] + public void GetFunctionLength(MockTarget.Architecture arch, bool includeEndAddress, bool unwindInfoIsFunctionLength) + { + MockMemorySpace.Builder builder = new(new TargetTestHelpers(arch)); + MockDescriptors.RuntimeFunctions runtimeFunctions = new(builder, includeEndAddress, unwindInfoIsFunctionLength); + + uint[] entries = [0x100, 0x1f0, 0x1000, 0x2000, 0xa000]; + TargetPointer addr = runtimeFunctions.AddRuntimeFunctions(entries); + + Target target = new TestPlaceholderTarget(builder.TargetTestHelpers.Arch, builder.GetReadContext().ReadFromTarget, runtimeFunctions.Types); + RuntimeFunctionLookup lookup = RuntimeFunctionLookup.Create(target); + + for (uint i = 0; i < entries.Length; i++) + { + uint expectedFunctionLength = i < entries.Length - 1 + ? Math.Min(entries[i + 1] - entries[i], MockDescriptors.RuntimeFunctions.DefaultFunctionLength) + : MockDescriptors.RuntimeFunctions.DefaultFunctionLength; + + Data.RuntimeFunction function = lookup.GetRuntimeFunction(addr, i); + uint functionLength = lookup.GetFunctionLength(function); + Assert.Equal(expectedFunctionLength, functionLength); + } + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void TryGetRuntimeFunctionIndexForAddress(MockTarget.Architecture arch) + { + MockMemorySpace.Builder builder = new(new TargetTestHelpers(arch)); + MockDescriptors.RuntimeFunctions runtimeFunctions = new(builder); + + uint[] entries = [0x100, 0x1f0, 0x1000, 0x2000, 0xa000]; + TargetPointer addr = runtimeFunctions.AddRuntimeFunctions(entries); + + TestPlaceholderTarget target = new TestPlaceholderTarget(builder.TargetTestHelpers.Arch, builder.GetReadContext().ReadFromTarget, runtimeFunctions.Types); + ContractRegistry reg = Mock.Of( + c => c.PlatformMetadata == new Mock().Object); + target.SetContracts(reg); + RuntimeFunctionLookup lookup = RuntimeFunctionLookup.Create(target); + + for (uint i = 0; i < entries.Length; i++) + { + TargetPointer relativeAddress = (TargetPointer)entries[i]; + bool res = lookup.TryGetRuntimeFunctionIndexForAddress(addr, (uint)entries.Length, relativeAddress, out uint index); + Assert.True(res); + Assert.Equal(i, index); + } + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void TryGetRuntimeFunctionIndexForAddress_NoMatch(MockTarget.Architecture arch) + { + MockMemorySpace.Builder builder = new(new TargetTestHelpers(arch)); + MockDescriptors.RuntimeFunctions runtimeFunctions = new(builder); + + uint[] entries = [0x100, 0x1f0]; + TargetPointer addr = runtimeFunctions.AddRuntimeFunctions(entries); + + TestPlaceholderTarget target = new TestPlaceholderTarget(builder.TargetTestHelpers.Arch, builder.GetReadContext().ReadFromTarget, runtimeFunctions.Types); + ContractRegistry reg = Mock.Of( + c => c.PlatformMetadata == new Mock().Object); + target.SetContracts(reg); + RuntimeFunctionLookup lookup = RuntimeFunctionLookup.Create(target); + + TargetPointer relativeAddress = 0x0ff; + bool res = lookup.TryGetRuntimeFunctionIndexForAddress(addr, (uint)entries.Length, relativeAddress, out _); + Assert.False(res); + } +} diff --git a/src/native/managed/cdacreader/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs b/src/native/managed/cdacreader/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs index 98ca626103a04..e061b65972a48 100644 --- a/src/native/managed/cdacreader/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs +++ b/src/native/managed/cdacreader/tests/MockDescriptors/MockDescriptors.ExecutionManager.cs @@ -27,8 +27,8 @@ public struct AllocationRange // nibble maps for various range section fragments are allocated in this range public ulong NibbleMapStart; public ulong NibbleMapEnd; - // "RealCodeHeader" objects for jitted methods and the module, info, and runtime functions for R2R - // are allocated in this range + // "RealCodeHeader" objects for jitted methods and the module, info, runtime functions + // and hot/cold map for R2R are allocated in this range public ulong ExecutionManagerStart; public ulong ExecutionManagerEnd; } @@ -235,15 +235,6 @@ public static RangeSectionMapTestBuilder CreateRangeSection(MockTarget.Architect ] }; - private static readonly MockDescriptors.TypeFields RuntimeFunctionFields = new() - { - DataType = DataType.RuntimeFunction, - Fields = - [ - new(nameof(Data.RuntimeFunction.BeginAddress), DataType.uint32), - ] - }; - private static MockDescriptors.TypeFields ReadyToRunInfoFields(TargetTestHelpers helpers) => new() { DataType = DataType.ReadyToRunInfo, @@ -252,6 +243,8 @@ public static RangeSectionMapTestBuilder CreateRangeSection(MockTarget.Architect new(nameof(Data.ReadyToRunInfo.CompositeInfo), DataType.pointer), new(nameof(Data.ReadyToRunInfo.NumRuntimeFunctions), DataType.uint32), new(nameof(Data.ReadyToRunInfo.RuntimeFunctions), DataType.pointer), + new(nameof(Data.ReadyToRunInfo.NumHotColdMap), DataType.uint32), + new(nameof(Data.ReadyToRunInfo.HotColdMap), DataType.pointer), new(nameof(Data.ReadyToRunInfo.DelayLoadMethodCallThunks), DataType.pointer), new(nameof(Data.ReadyToRunInfo.EntryPointToMethodDescMap), DataType.Unknown, helpers.LayoutFields(MockDescriptors.HashMap.HashMapFields.Fields).Stride), ] @@ -264,6 +257,7 @@ public static RangeSectionMapTestBuilder CreateRangeSection(MockTarget.Architect internal (string Name, ulong Value)[] Globals { get; } private readonly RangeSectionMapTestBuilder _rsmBuilder; + private readonly RuntimeFunctions _rfBuilder; private readonly MockMemorySpace.BumpAllocator _rangeSectionMapAllocator; private readonly MockMemorySpace.BumpAllocator _nibbleMapAllocator; @@ -278,6 +272,7 @@ internal ExecutionManager(int version, MockMemorySpace.Builder builder, Allocati Version = version; Builder = builder; _rsmBuilder = new RangeSectionMapTestBuilder(ExecutionManagerCodeRangeMapAddress, builder); + _rfBuilder = new RuntimeFunctions(builder); _rangeSectionMapAllocator = Builder.CreateAllocator(allocationRange.RangeSectionMapStart, allocationRange.RangeSectionMapEnd); _nibbleMapAllocator = Builder.CreateAllocator(allocationRange.NibbleMapStart, allocationRange.NibbleMapEnd); _allocator = Builder.CreateAllocator(allocationRange.ExecutionManagerStart, allocationRange.ExecutionManagerEnd); @@ -285,14 +280,14 @@ internal ExecutionManager(int version, MockMemorySpace.Builder builder, Allocati Builder.TargetTestHelpers, [ RangeSectionMapFields, - RangeSectionFragmentFields, - RangeSectionFields, - CodeHeapListNodeFields, - RealCodeHeaderFields, - RuntimeFunctionFields, - ReadyToRunInfoFields(Builder.TargetTestHelpers), - MockDescriptors.ModuleFields, + RangeSectionFragmentFields, + RangeSectionFields, + CodeHeapListNodeFields, + RealCodeHeaderFields, + ReadyToRunInfoFields(Builder.TargetTestHelpers), + MockDescriptors.ModuleFields, ]).Concat(MockDescriptors.HashMap.GetTypes(Builder.TargetTestHelpers)) + .Concat(_rfBuilder.Types) .ToDictionary(); // Tests are currently always set to use funclets @@ -436,26 +431,28 @@ public TargetCodePointer AddJittedMethod(JittedCodeRange jittedCodeRange, uint c return codeStart; } - public TargetPointer AddReadyToRunInfo(uint[] runtimeFunctions) + public TargetPointer AddReadyToRunInfo(uint[] runtimeFunctions, uint[] hotColdMap) { TargetTestHelpers helpers = Builder.TargetTestHelpers; // Add the array of runtime functions uint numRuntimeFunctions = (uint)runtimeFunctions.Length; - Target.TypeInfo runtimeFunctionType = Types[DataType.RuntimeFunction]; - uint runtimeFunctionSize = runtimeFunctionType.Size.Value; - MockMemorySpace.HeapFragment runtimeFunctionsFragment = _allocator.Allocate((numRuntimeFunctions + 1) * runtimeFunctionSize, $"RuntimeFunctions[{numRuntimeFunctions}]"); - Builder.AddHeapFragment(runtimeFunctionsFragment); - for (uint i = 0; i < numRuntimeFunctions; i++) + TargetPointer runtimeFunctionsAddr = _rfBuilder.AddRuntimeFunctions(runtimeFunctions); + + // Add the hot/cold map + TargetPointer hotColdMapAddr = TargetPointer.Null; + if (hotColdMap.Length > 0) { - Span func = Builder.BorrowAddressRange(runtimeFunctionsFragment.Address + i * runtimeFunctionSize, (int)runtimeFunctionSize); - helpers.Write(func.Slice(runtimeFunctionType.Fields[nameof(Data.RuntimeFunction.BeginAddress)].Offset, sizeof(uint)), runtimeFunctions[i]); + MockMemorySpace.HeapFragment hotColdMapFragment = _allocator.Allocate((ulong)hotColdMap.Length * sizeof(uint), $"HotColdMap[{hotColdMap.Length}]"); + Builder.AddHeapFragment(hotColdMapFragment); + hotColdMapAddr = hotColdMapFragment.Address; + for (uint i = 0; i < hotColdMap.Length; i++) + { + Span span = Builder.BorrowAddressRange(hotColdMapFragment.Address + i * sizeof(uint), sizeof(uint)); + helpers.Write(span, hotColdMap[i]); + } } - // Runtime function entries are terminated by a sentinel value of -1 - Span sentinel = Builder.BorrowAddressRange(runtimeFunctionsFragment.Address + numRuntimeFunctions * runtimeFunctionSize, (int)runtimeFunctionSize); - helpers.Write(sentinel.Slice(runtimeFunctionType.Fields[nameof(Data.RuntimeFunction.BeginAddress)].Offset, sizeof(uint)), ~0u); - // Add ReadyToRunInfo Target.TypeInfo r2rInfoType = Types[DataType.ReadyToRunInfo]; MockMemorySpace.HeapFragment r2rInfo = _allocator.Allocate(r2rInfoType.Size.Value, "ReadyToRunInfo"); @@ -467,7 +464,11 @@ public TargetPointer AddReadyToRunInfo(uint[] runtimeFunctions) // Point at the runtime functions helpers.Write(data.Slice(r2rInfoType.Fields[nameof(Data.ReadyToRunInfo.NumRuntimeFunctions)].Offset, sizeof(uint)), numRuntimeFunctions); - helpers.WritePointer(data.Slice(r2rInfoType.Fields[nameof(Data.ReadyToRunInfo.RuntimeFunctions)].Offset, helpers.PointerSize), runtimeFunctionsFragment.Address); + helpers.WritePointer(data.Slice(r2rInfoType.Fields[nameof(Data.ReadyToRunInfo.RuntimeFunctions)].Offset, helpers.PointerSize), runtimeFunctionsAddr); + + // Point at the hot/cold map + helpers.Write(data.Slice(r2rInfoType.Fields[nameof(Data.ReadyToRunInfo.NumHotColdMap)].Offset, sizeof(uint)), hotColdMap.Length); + helpers.WritePointer(data.Slice(r2rInfoType.Fields[nameof(Data.ReadyToRunInfo.HotColdMap)].Offset, helpers.PointerSize), hotColdMapAddr); return r2rInfo.Address; } diff --git a/src/native/managed/cdacreader/tests/MockDescriptors/MockDescriptors.RuntimeFunctions.cs b/src/native/managed/cdacreader/tests/MockDescriptors/MockDescriptors.RuntimeFunctions.cs new file mode 100644 index 0000000000000..c7d772f96d630 --- /dev/null +++ b/src/native/managed/cdacreader/tests/MockDescriptors/MockDescriptors.RuntimeFunctions.cs @@ -0,0 +1,115 @@ +// 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.Linq; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +internal partial class MockDescriptors +{ + internal class RuntimeFunctions + { + private static TypeFields RuntimeFunctionFields(bool includeEndAddress) + { + TargetTestHelpers.Field[] fields = [ + new(nameof(Data.RuntimeFunction.BeginAddress), DataType.uint32), + new(nameof(Data.RuntimeFunction.UnwindData), DataType.uint32), + ]; + if (includeEndAddress) + fields = fields.Append(new(nameof(Data.RuntimeFunction.EndAddress), DataType.uint32)).ToArray(); + + return new() + { + DataType = DataType.RuntimeFunction, + Fields = fields + }; + } + + private static TypeFields UnwindInfoFields(bool isFunctionLength) => new() + { + DataType = DataType.UnwindInfo, + Fields = isFunctionLength + ? [new(nameof(Data.UnwindInfo.FunctionLength), DataType.uint32)] + : [new(nameof(Data.UnwindInfo.Header), DataType.uint32)] + }; + + internal MockMemorySpace.Builder Builder { get; } + internal Dictionary Types { get; } + + private const ulong DefaultAllocationRangeStart = 0x0004_0000; + private const ulong DefaultAllocationRangeEnd = 0x0005_0000; + + internal const uint DefaultFunctionLength = 0x100; + + private readonly MockMemorySpace.BumpAllocator _allocator; + + public RuntimeFunctions(MockMemorySpace.Builder builder, bool includeEndAddress = true, bool unwindInfoIsFunctionLength = false) + : this(builder, (DefaultAllocationRangeStart, DefaultAllocationRangeEnd), includeEndAddress, unwindInfoIsFunctionLength) + { } + + public RuntimeFunctions(MockMemorySpace.Builder builder, (ulong Start, ulong End) allocationRange, bool includeEndAddress, bool unwindInfoIsFunctionLength) + { + Builder = builder; + _allocator = Builder.CreateAllocator(allocationRange.Start, allocationRange.End); + Types = GetTypesForTypeFields( + Builder.TargetTestHelpers, + [ + RuntimeFunctionFields(includeEndAddress), + UnwindInfoFields(unwindInfoIsFunctionLength) + ]); + } + + public TargetPointer AddRuntimeFunctions(uint[] runtimeFunctions) + { + TargetTestHelpers helpers = Builder.TargetTestHelpers; + + // Add the array of runtime functions + uint numRuntimeFunctions = (uint)runtimeFunctions.Length; + Target.TypeInfo runtimeFunctionType = Types[DataType.RuntimeFunction]; + uint runtimeFunctionSize = runtimeFunctionType.Size.Value; + Target.TypeInfo unwindInfoType = Types[DataType.UnwindInfo]; + MockMemorySpace.HeapFragment runtimeFunctionsFragment = _allocator.Allocate((numRuntimeFunctions + 1) * runtimeFunctionSize, $"RuntimeFunctions[{numRuntimeFunctions}]"); + Builder.AddHeapFragment(runtimeFunctionsFragment); + for (uint i = 0; i < numRuntimeFunctions; i++) + { + Span func = Builder.BorrowAddressRange(runtimeFunctionsFragment.Address + i * runtimeFunctionSize, (int)runtimeFunctionSize); + helpers.Write(func.Slice(runtimeFunctionType.Fields[nameof(Data.RuntimeFunction.BeginAddress)].Offset, sizeof(uint)), runtimeFunctions[i]); + + // Set the function length to the default function length or up to the next function start + uint functionLength = i < numRuntimeFunctions - 1 + ? Math.Min(runtimeFunctions[i + 1] - runtimeFunctions[i], DefaultFunctionLength) + : DefaultFunctionLength; + if (runtimeFunctionType.Fields.ContainsKey(nameof(Data.RuntimeFunction.EndAddress))) + helpers.Write(func.Slice(runtimeFunctionType.Fields[nameof(Data.RuntimeFunction.EndAddress)].Offset, sizeof(uint)), runtimeFunctions[i] + functionLength); + + // Add the unwindInfo + MockMemorySpace.HeapFragment unwindInfoFragment = _allocator.Allocate(unwindInfoType.Size.Value, $"UnwindInfo for RuntimeFunction {runtimeFunctions[i]}"); + Builder.AddHeapFragment(unwindInfoFragment); + Span unwindInfo = unwindInfoFragment.Data.AsSpan(); + if (Types[DataType.UnwindInfo].Fields.ContainsKey(nameof(Data.UnwindInfo.FunctionLength))) + { + helpers.Write(unwindInfo.Slice(unwindInfoType.Fields[nameof(Data.UnwindInfo.FunctionLength)].Offset, sizeof(uint)), functionLength); + } + else + { + // First 18 bits of the header are function length / (pointer size / 2) + uint headerBits = (uint)(functionLength / (helpers.PointerSize / 2)); + if (headerBits > 1 << 18 - 1) + throw new InvalidOperationException("Function length is too long "); + + helpers.Write(unwindInfo.Slice(unwindInfoType.Fields[nameof(Data.UnwindInfo.Header)].Offset, sizeof(uint)), headerBits); + } + + helpers.Write(func.Slice(runtimeFunctionType.Fields[nameof(Data.RuntimeFunction.UnwindData)].Offset, sizeof(uint)), (uint)unwindInfoFragment.Address); + } + + // Runtime function entries are terminated by a sentinel value of -1 + Span sentinel = Builder.BorrowAddressRange(runtimeFunctionsFragment.Address + numRuntimeFunctions * runtimeFunctionSize, (int)runtimeFunctionSize); + helpers.Write(sentinel.Slice(runtimeFunctionType.Fields[nameof(Data.RuntimeFunction.BeginAddress)].Offset, sizeof(uint)), ~0u); + + return runtimeFunctionsFragment.Address; + } + } +}