diff --git a/docs/design/datacontracts/ExecutionManager.md b/docs/design/datacontracts/ExecutionManager.md index 71b8477d90d30..ea032d3cec32c 100644 --- a/docs/design/datacontracts/ExecutionManager.md +++ b/docs/design/datacontracts/ExecutionManager.md @@ -186,19 +186,22 @@ code allocated in that address range), level 4 entires point to level 3 maps and ### NibbleMap -Version 1 of this contract depends on a "nibble map" data structure +The ExecutionManager contract depends on a "nibble map" data structure that allows mapping of a code address in a contiguous subsection of the address space to the pointer to the start of that a code sequence. It takes advantage of the fact that the code starts are aligned and are spaced apart to represent their addresses as a 4-bit nibble value. +Version 1 of the contract depends on the `NibbleMapLinearLookup` implementation of the nibblemap algorithm. + Given a contiguous region of memory in which we lay out a collection of non-overlapping code blocks that are -not too small (so that two adjacent ones aren't too close together) and where the start of each code block is preceeded by a code header aligned on some power of 2, +not too small (so that two adjacent ones aren't too close together) and where the start of each code block is aligned on some power of 2 and preceeded by a code header, we can break up the whole memory space into buckets of a fixed size (32-bytes in the current implementation), where -each bucket either has a code block header or not. -Thinking of each code block header address as a hex number, we can view it as: `[index, offset, zeros]` +each bucket either has a code block or not. +Thinking of each code block address as a hex number, we can view it as: [index, offset] where each index gives us a bucket and the offset gives us the position of the header within the bucket. -We encode each offset into a 4-bit nibble, reserving the special value 0 to mark the places in the map where a method doesn't start. +In the current implementation code must be 4 byte aligned therefore there are 8 possible offsets in a bucket. +These are encoded as values 1-8 in the 4-bit nibble, with 0 reserved to mark the places in the map where a method doesn't start. To find the start of a method given an address we first convert it into a bucket index (giving the map unit) and an offset which we can then turn into the index of the nibble that covers that address. @@ -239,3 +242,59 @@ Now suppose we do a lookup for address 302 (0x12E) * Therefore we know there is no method start at any map index in the current map unit. * We will then align the map index to the start of the current map unit (map index 8) and move back to the previous map unit (map index 7) * At that point, we scan backwards for a non-zero map unit and a non-zero nibble within the first non-zero map unit. Since there are none, we return null. + + +## Version 2 + +Version 2 of the contract depends the new `NibbleMapConstantLookup` algorithm which has O(1) lookup time compared to the `NibbleMapLinearLookup` O(n) lookup time. + +With the exception of the nibblemap change, version 2 is identical to version 1. + +### NibbleMap + +The `NibbleMapConstantLookup` implementation is very similar to `NibbleMapLinearLookup` with the addition +of writing relative pointers into the nibblemap whenever a code block completely covers the code region +represented by a DWORD, with the current values 256 bytes. +This allows for O(1) lookup time with the cost of O(n) write time. + +Pointers are encoded using the top 28 bits of the DWORD. The bottom 4 bits of the pointer +are reduced to 2 bits of data using the fact that code start must be 4 byte aligned. This is encoded into +the nibble in bits 28 .. 31 of the DWORD with values 9-12. This is also used to differentiate DWORDs +filled with nibble values and DWORDs with pointer values. + +| Nibble Value | Meaning | How to decode | +|:------------:|:--------|:--------------:| +| 0 | empty | | +| 1-8 | Nibble | value - 1 | +| 9-12 | Pointer | (value - 9) << 2 | +| 13-15 | unused | | + +To read the nibblemap, we check if the DWORD is a pointer. If so, then we know the value looked up is +part of a managed code block beginning at the map base + decoded pointer. Otherwise we can check for nibbles +as normal. If the DWORD is empty (no pointer or previous nibbles), then we check the previous DWORD for a +pointer or preceeding nibble. If that DWORD is empty, then we must not be in a managed function. If we were, +the write algorithm would have written a relative pointer in the DWORD or we would have seen the start nibble. + +Note, looking up a value that points to bytes outside of a managed function has undefined behavior. +In this implementation we may "extend" the lookup period of a function several hundred bytes +if there is not another function immediately following it. + +We will go through the same example as above with the new algorithm. Suppose there is code starting at address 304 (0x130) with length 1024 (0x400). + +* There will be a nibble at the start of the function as before. + * The map index will be 304 / 32 = 9 and the byte offset will be 304 % 32 = 16 + * Because addresses are 4-byte aligned, the nibble value will be 1 + 16 / 4 = 5 (we reserve 0 to mean no method). + * So the map unit containing index 9 will contain the value 0x5 << 24 (the map index 9 means we want the second nibble in the second map unit, and we number the nibbles starting from the most significant) , or 0x05000000 +* Since the function starts at 304 with a length of 1024, the last byte of the function is at 1327 (0x52F). Map units (DWORDs) contain 256 bytes (0x100) algined to the map base. Therefore map units represnting 0x200-0x2FF, 0x300-0x3FF and 0x400-0x4ff are completely covered by the function and will have a relative pointer. + * To get the relative pointer value we split the code start value at the bottom 4 bits. The top 28 bits are included as normal. We shift the bottom 4 bits 2 to the right and add 9, to get the bottom 4 bits encoding. This gives us a relative pointer value of 311 (0x137). + * 304 = 0b100110000 + * Top 28 bits: 304 = 0b10011xxxx + * Bottom 4 bits: 0 = 0b0000 + * Bottom 4 bits encoding: 9 = (0 >> 2) + 9 + * Relative Pointer Encoding: 311 = 304 + 9 + +Now suppose we do a lookup for address 1300 (0x514) +* The map index will be 1300 / 32 = 40 which is located in the 40 / 8 = 5th map unit (DWORD). +* We read the value of the 5th map unit and find it is empty. +* We read the value of the 4th map unit and find that the nibble in the lowest bits has the value of 9 implying that this map unit is a relative pointer. +* Since we found a relative pointer we can decode the entire map unit as a relative pointer and return that address added to the base. diff --git a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager_1.EEJitManager.cs b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerBase.EEJitManager.cs similarity index 90% rename from src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager_1.EEJitManager.cs rename to src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerBase.EEJitManager.cs index a3a828192e3fb..eda3c4e3fb247 100644 --- a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager_1.EEJitManager.cs +++ b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerBase.EEJitManager.cs @@ -4,15 +4,16 @@ using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using Microsoft.Diagnostics.DataContractReader.ExecutionManagerHelpers; namespace Microsoft.Diagnostics.DataContractReader.Contracts; -internal readonly partial struct ExecutionManager_1 : IExecutionManager +internal partial class ExecutionManagerBase : IExecutionManager { private class EEJitManager : JitManager { - private readonly ExecutionManagerHelpers.NibbleMap _nibbleMap; - public EEJitManager(Target target, ExecutionManagerHelpers.NibbleMap nibbleMap) : base(target) + private readonly INibbleMap _nibbleMap; + public EEJitManager(Target target, INibbleMap nibbleMap) : base(target) { _nibbleMap = nibbleMap; } diff --git a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager_1.cs b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerBase.cs similarity index 96% rename from src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager_1.cs rename to src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerBase.cs index f8a956f5af98c..01d4925dccf26 100644 --- a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager_1.cs +++ b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerBase.cs @@ -4,10 +4,12 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using Microsoft.Diagnostics.DataContractReader.ExecutionManagerHelpers; namespace Microsoft.Diagnostics.DataContractReader.Contracts; -internal readonly partial struct ExecutionManager_1 : IExecutionManager +internal partial class ExecutionManagerBase : IExecutionManager + where T : INibbleMap { internal readonly Target _target; @@ -18,12 +20,12 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts; private readonly EEJitManager _eeJitManager; private readonly ReadyToRunJitManager _r2rJitManager; - public ExecutionManager_1(Target target, Data.RangeSectionMap topRangeSectionMap) + public ExecutionManagerBase(Target target, Data.RangeSectionMap topRangeSectionMap) { _target = target; _topRangeSectionMap = topRangeSectionMap; _rangeSectionMapLookup = ExecutionManagerHelpers.RangeSectionMap.Create(_target); - ExecutionManagerHelpers.NibbleMap nibbleMap = ExecutionManagerHelpers.NibbleMap.Create(_target); + INibbleMap nibbleMap = T.Create(_target); _eeJitManager = new EEJitManager(_target, nibbleMap); _r2rJitManager = new ReadyToRunJitManager(_target); } diff --git a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManagerFactory.cs b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerFactory.cs similarity index 86% rename from src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManagerFactory.cs rename to src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerFactory.cs index 13f2ada40a499..e99074a2b810f 100644 --- a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManagerFactory.cs +++ b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerFactory.cs @@ -1,8 +1,6 @@ // 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 sealed class ExecutionManagerFactory : IContractFactory @@ -14,6 +12,9 @@ IExecutionManager IContractFactory.CreateContract(Target targ return version switch { 1 => new ExecutionManager_1(target, rangeSectionMap), + + // The nibblemap algorithm was changed in version 2 + 2 => new ExecutionManager_2(target, rangeSectionMap), _ => default(ExecutionManager), }; } diff --git a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_1.cs b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_1.cs new file mode 100644 index 0000000000000..c1a8e135b9d43 --- /dev/null +++ b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_1.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.DataContractReader.ExecutionManagerHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts; + +internal sealed class ExecutionManager_1 : ExecutionManagerBase +{ + public ExecutionManager_1(Target target, Data.RangeSectionMap topRangeSectionMap) : base(target, topRangeSectionMap) + { + } +} diff --git a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_2.cs b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_2.cs new file mode 100644 index 0000000000000..fc0397d82dfbc --- /dev/null +++ b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManager_2.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.DataContractReader.ExecutionManagerHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts; + +internal sealed class ExecutionManager_2 : ExecutionManagerBase +{ + public ExecutionManager_2(Target target, Data.RangeSectionMap topRangeSectionMap) : base(target, topRangeSectionMap) + { + } +} diff --git a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/INibbleMap.cs b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/INibbleMap.cs new file mode 100644 index 0000000000000..bc54bc7b006ec --- /dev/null +++ b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/INibbleMap.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Numerics; +using System.Diagnostics; +using System; + +namespace Microsoft.Diagnostics.DataContractReader.ExecutionManagerHelpers; + +internal interface INibbleMap +{ + public static abstract INibbleMap Create(Target target); + + public TargetPointer FindMethodCode(Data.CodeHeapListNode heapListNode, TargetCodePointer jittedCodeAddress); +} diff --git a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/NibbleMapConstantLookup.cs b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/NibbleMapConstantLookup.cs new file mode 100644 index 0000000000000..bb18f4584d8f2 --- /dev/null +++ b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/NibbleMapConstantLookup.cs @@ -0,0 +1,149 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Numerics; +using System.Diagnostics; +using System; + +using static Microsoft.Diagnostics.DataContractReader.ExecutionManagerHelpers.NibbleMapHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.ExecutionManagerHelpers; + +// CoreCLR nibblemap with O(1) lookup time. +// +// Implementation very similar to NibbleMapLinearLookup, but with the addition of writing relative pointers +// into the nibblemap whenever a code block completely covers a DWORD. This allows for O(1) lookup +// with the cost of O(n) write time. +// +// Pointers are encoded using the top 28 bits of the DWORD normally, the bottom 4 bits of the pointer +// are reduced to 2 bits due to 4 byte code offset and encoded in bits 28 .. 31 of the DWORD with values +// 9-12. This is used to differentiate nibble values and pointer DWORDs. +// +// To read the nibblemap, we check if the DWORD is a pointer. If so, then we know the value currentPC is +// part of a managed code block beginning at the mapBase + decoded pointer. If the DWORD is empty +// (no pointer or previous nibbles), then we only need to read the previous DWORD. If that DWORD is empty, +// then we must not be in a managed function. Otherwise the write algorithm would have written a relative +// pointer in the DWORD. +// +// Note, a currentPC pointing to bytes outside a function have undefined lookup behavior. +// In this implementation we may "extend" the lookup period of a function several hundred bytes +// if there is not another function following it. + +internal class NibbleMapConstantLookup : INibbleMap +{ + private readonly Target _target; + + private NibbleMapConstantLookup(Target target) + { + _target = target; + } + + internal static bool IsPointer(MapUnit mapUnit) + { + return (mapUnit.Value & MapUnit.NibbleMask) > 8; + } + + internal static TargetPointer DecodePointer(TargetPointer baseAddress, MapUnit mapUnit) + { + uint nibble = mapUnit.Value & MapUnit.NibbleMask; + uint relativePointer = (mapUnit.Value & ~MapUnit.NibbleMask) + ((nibble - 9) << 2); + return baseAddress + relativePointer; + } + + internal static uint EncodePointer(uint relativeAddress) + { + uint nibble = ((relativeAddress & MapUnit.NibbleMask) >>> 2) + 9; + return (relativeAddress & ~MapUnit.NibbleMask) + nibble; + } + + internal TargetPointer FindMethodCode(TargetPointer mapBase, TargetPointer mapStart, TargetCodePointer currentPC) + { + TargetNUInt relativeAddress = new TargetNUInt(currentPC.Value - mapBase.Value); + DecomposeAddress(relativeAddress, out MapKey mapIdx, out Nibble bucketByteIndex); + + MapUnit t = mapIdx.ReadMapUnit(_target, mapStart); + + // if pointer, return value + if (IsPointer(t)) + { + return DecodePointer(mapBase, t); + } + + // shift the nibble we want to the least significant position + t = t.FocusOnIndexedNibble(mapIdx); + + // if the nibble is non-zero, we have found the start of a method, + // but we need to check that the start is before the current address, not after + if (!t.Nibble.IsEmpty && t.Nibble.Value <= bucketByteIndex.Value) + { + return GetAbsoluteAddress(mapBase, mapIdx, t.Nibble); + } + + // search backwards through the current map unit + // we processed the lsb nibble, move to the next one + t = t.ShiftNextNibble; + + // if there's any nibble set in the current unit, find it + if (!t.IsEmpty) + { + mapIdx = mapIdx.Prev; + while (t.Nibble.IsEmpty) + { + t = t.ShiftNextNibble; + mapIdx = mapIdx.Prev; + } + return GetAbsoluteAddress(mapBase, mapIdx, t.Nibble); + } + + // We finished the current map unit, we want to move to the previous one. + // But if we were in the first map unit, we can stop + if (mapIdx.InFirstMapUnit) + { + return TargetPointer.Null; + } + + // We're now done with the current map unit. + // Align the map index to the current map unit, then move back one nibble into the previous map unit + mapIdx = mapIdx.AlignDownToMapUnit(); + mapIdx = mapIdx.Prev; + + // read the map unit containing mapIdx and skip over it if it is all zeros + t = mapIdx.ReadMapUnit(_target, mapStart); + + // if t is empty, then currentPC can not be in a function + if (t.IsEmpty) + { + return TargetPointer.Null; + } + + // if t is not empty, it must contain a pointer or a nibble + if (IsPointer(t)) + { + return DecodePointer(mapBase, t); + } + + // move to the correct nibble in the map unit + while (!mapIdx.IsZero && t.Nibble.IsEmpty) + { + t = t.ShiftNextNibble; + mapIdx = mapIdx.Prev; + } + + return GetAbsoluteAddress(mapBase, mapIdx, t.Nibble); + } + + public static INibbleMap Create(Target target) + { + return new NibbleMapConstantLookup(target); + } + + public TargetPointer FindMethodCode(Data.CodeHeapListNode heapListNode, TargetCodePointer jittedCodeAddress) + { + if (jittedCodeAddress < heapListNode.StartAddress || jittedCodeAddress > heapListNode.EndAddress) + { + return TargetPointer.Null; + } + + return FindMethodCode(heapListNode.MapBase, heapListNode.HeaderMap, jittedCodeAddress); + } +} diff --git a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/NibbleMapHelpers.cs b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/NibbleMapHelpers.cs new file mode 100644 index 0000000000000..5d5f9870d8879 --- /dev/null +++ b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/NibbleMapHelpers.cs @@ -0,0 +1,155 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Numerics; +using System.Diagnostics; +using System; + +namespace Microsoft.Diagnostics.DataContractReader.ExecutionManagerHelpers; + +internal static class NibbleMapHelpers +{ + // we will partition the address space into buckets of this many bytes. + // There is at most one code block header per bucket. + // Normally we would then need 5 bits (Log2(BytesPerBucket))to find the exact start address, + // but because code headers are aligned, we can store the offset in a 4-bit nibble instead and shift appropriately to compute + // the effective address + internal const ulong BytesPerBucket = 8 * MapUnit.SizeInBytes; + + // We load the map contents as 32-bit integers, which contains 8 4-bit nibbles. + // The algorithm will focus on each nibble in a map unit before moving on to the previous map unit + internal readonly struct MapUnit + { + public const int SizeInBytes = sizeof(uint); + public const ulong SizeInNibbles = 2 * SizeInBytes; + public readonly uint Value; + + public MapUnit(uint value) => Value = value; + + public override string ToString() => $"0x{Value:x}"; + + // Shift the next nibble into the least significant position. + public MapUnit ShiftNextNibble => new MapUnit(Value >>> 4); + + public const uint NibbleMask = 0x0Fu; + internal Nibble Nibble => new(Value & NibbleMask); + + public bool IsEmpty => Value == 0; + + // Assuming mapIdx is the index of a nibble within the current map unit, + // shift the unit so that nibble is in the least significant position and return the result. + public MapUnit FocusOnIndexedNibble(MapKey mapIdx) + { + uint shift = mapIdx.GetNibbleShift(); + return new MapUnit(Value >>> (int)shift); + } + } + + // Each nibble is a 4-bit integer that gives an offset within a bucket. + // We reserse 0 to mean that there is no method starting at any offset within a bucket + internal readonly struct Nibble + { + public readonly uint Value; + + public Nibble(uint value) + { + Debug.Assert(value <= 0xF); + Value = value; + } + + public static Nibble Zero => new Nibble(0); + public bool IsEmpty => Value == 0; + + public ulong TargetByteOffset + { + get + { + Debug.Assert(Value != 0); + return (uint)(Value - 1) * MapUnit.SizeInBytes; + } + } + } + + // The key to the map is the index of an individual nibble + internal readonly struct MapKey + { + private readonly ulong _mapIdx; + public MapKey(ulong mapIdx) => _mapIdx = mapIdx; + public override string ToString() => $"0x{_mapIdx:x}"; + + // The offset of the address in the target space that this map index represents + public ulong TargetByteOffset => _mapIdx * BytesPerBucket; + + // The index of the map unit that contains this map index + public ulong ContainingMapUnitIndex => _mapIdx / MapUnit.SizeInNibbles; + + // The offset of the map unit that contains this map index + public ulong ContainingMapUnitByteOffset => ContainingMapUnitIndex * MapUnit.SizeInBytes; + + // The map index is the index of a nibble within the map, this gives the index of that nibble within a map unit. + public uint NibbleIndexInMapUnit => (uint)(_mapIdx & (MapUnit.SizeInNibbles - 1)); + + // go to the previous nibble + public MapKey Prev + { + get + { + Debug.Assert(_mapIdx > 0); + return new MapKey(_mapIdx - 1); + } + + } + + // to to the previous map unit + public MapKey PrevMapUnit => new MapKey(_mapIdx - MapUnit.SizeInNibbles); + + // Get a MapKey that is aligned to the first nibble in the map unit that contains this map index + public MapKey AlignDownToMapUnit() =>new MapKey(_mapIdx & (~(MapUnit.SizeInNibbles - 1))); + + // If the map index is less than the size of a map unit, we are in the first MapUnit and + // can stop searching + public bool InFirstMapUnit => _mapIdx < MapUnit.SizeInNibbles; + + public bool IsZero => _mapIdx == 0; + + // given the index of a nibble in the map, compute how much we have to shift a MapUnit to put that + // nibble in the least significant position. + internal uint GetNibbleShift() + { + return 28 - (NibbleIndexInMapUnit * 4); // bit shift - 4 bits per nibble + } + + internal MapUnit ReadMapUnit(Target target, TargetPointer mapStart) + { + // Given a logical index into the map, compute the address in memory where that map unit is located + TargetPointer mapUnitAdderss = mapStart + ContainingMapUnitByteOffset; + return new MapUnit(target.Read(mapUnitAdderss)); + } + } + + // for tests + internal static uint ComputeNibbleShift(MapKey mapIdx) => mapIdx.GetNibbleShift(); + + internal static TargetPointer RoundTripAddress(TargetPointer mapBase, TargetPointer currentPC) + { + TargetNUInt relativeAddress = new TargetNUInt(currentPC.Value - mapBase.Value); + DecomposeAddress(relativeAddress, out MapKey mapIdx, out Nibble bucketByteIndex); + return GetAbsoluteAddress(mapBase, mapIdx, bucketByteIndex); + } + + // Given a base address, a map index, and a nibble value, compute the absolute address in memory + // that the index and nibble point to. + internal static TargetPointer GetAbsoluteAddress(TargetPointer baseAddress, MapKey mapIdx, Nibble nibble) + { + return baseAddress + mapIdx.TargetByteOffset + nibble.TargetByteOffset; + } + + // Given a relative address, decompose it into + // the bucket index and an offset within the bucket. + internal static void DecomposeAddress(TargetNUInt relative, out MapKey mapIdx, out Nibble bucketByteIndex) + { + mapIdx = new(relative.Value / BytesPerBucket); + uint bucketByteOffset = (uint)(relative.Value & (BytesPerBucket - 1)); + bucketByteIndex = new Nibble((bucketByteOffset / MapUnit.SizeInBytes) + 1); + } +} diff --git a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/NibbleMapLinearLookup.cs b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/NibbleMapLinearLookup.cs new file mode 100644 index 0000000000000..e32d7b2aaf386 --- /dev/null +++ b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/NibbleMapLinearLookup.cs @@ -0,0 +1,159 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Numerics; +using System.Diagnostics; +using System; + +using static Microsoft.Diagnostics.DataContractReader.ExecutionManagerHelpers.NibbleMapHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.ExecutionManagerHelpers; + +// Given a contiguous region of memory in which we lay out a collection of non-overlapping code blocks that are +// not too small (so that two adjacent ones aren't too close together) and where the start of each code block is aligned on some power of 2 and preceeded by a code header, +// we can break up the whole memory space into buckets of a fixed size (32-bytes in the current implementation), where +// each bucket either has a code block or not. +// Thinking of each code block address as a hex number, we can view it as: [index, offset] +// where each index gives us a bucket and the offset gives us the position of the header within the bucket. +// In the current implementation code must be 4 byte aligned therefore there are 8 possible offsets in a bucket. +// These are encoded as values 1-8 in the 4-bit nibble, with 0 reserved to mark the places in the map where a method doesn't start. +// +// To find the start of a method given an address we first convert it into a bucket index (giving the map unit) +// and an offset which we can then turn into the index of the nibble that covers that address. +// If the nibble is non-zero, we have the start of a method and it is near the given address. +// If the nibble is zero, we have to search backward first through the current map unit, and then through previous map +// units until we find a non-zero nibble. +// +// For example (all code addresses are relative to some unspecified base): +// Suppose there is code starting at address 304 (0x130) +// Then the map index will be 304 / 32 = 9 and the byte offset will be 304 % 32 = 16 +// Because addresses are 4-byte aligned, the nibble value will be 1 + 16 / 4 = 5 (we reserve 0 to mean no method). +// So the map unit containing index 9 will contain the value 0x5 << 24 (the map index 9 means we want the second nibble in the second map unit, and we number the nibbles starting from the most significant) +// Or 0x05000000 +// +// Now suppose we do a lookup for address 306 (0x132) +// The map index will be 306 / 32 = 9 and the byte offset will be 306 % 32 = 18 +// The nibble value will be 1 + 18 / 4 = 5 +// To do the lookup, we will load the map unit with index 9 (so the second 32-bit unit in the map) and get the value 0x05000000 +// We will then shift to focus on the nibble with map index 9 (which again has nibble shift 24), so +// the map unit will be 0x00000005 and we will get the nibble value 5. +// Therefore we know that there is a method start at map index 9, nibble value 5. +// The map index corresponds to an offset of 288 bytes and the nibble value 5 corresponds to an offset of (5 - 1) * 4 = 16 bytes +// So the method starts at offset 288 + 16 = 304, which is the address we were looking for. +// +// Now suppose we do a lookup for address 302 (0x12E) +// The map index will be 302 / 32 = 9 and the byte offset will be 302 % 32 = 14 +// The nibble value will be 1 + 14 / 4 = 4 +// To do the lookup, we will load the map unit containing map index 9 and get the value 0x05000000 +// We will then shift to focus on the nibble with map index 9 (which again has nibble shift 24), so we will get +// the nibble value 5. +// Therefore we know that there is a method start at map index 9, nibble value 5. +// But the address we're looking for is map index 9, nibble value 4. +// We know that methods can't start within 32-bytes of each other, so we know that the method we're looking for is not in the current nibble. +// We will then try to shift to the previous nibble in the map unit (0x00000005 >> 4 = 0x00000000) +// Therefore we know there is no method start at any map index in the current map unit. +// We will then align the map index to the start of the current map unit (map index 8) and move back to the previous map unit (map index 7) +// At that point, we scan backwards for non-zero map units. Since there are none, we return null. + +internal class NibbleMapLinearLookup : INibbleMap +{ + private readonly Target _target; + + private NibbleMapLinearLookup(Target target) + { + _target = target; + } + + internal TargetPointer FindMethodCode(TargetPointer mapBase, TargetPointer mapStart, TargetCodePointer currentPC) + { + TargetNUInt relativeAddress = new TargetNUInt(currentPC.Value - mapBase.Value); + DecomposeAddress(relativeAddress, out MapKey mapIdx, out Nibble bucketByteIndex); + + MapUnit t = mapIdx.ReadMapUnit(_target, mapStart); + + // shift the nibble we want to the least significant position + t = t.FocusOnIndexedNibble(mapIdx); + + // if the nibble is non-zero, we have found the start of a method, + // but we need to check that the start is before the current address, not after + if (!t.Nibble.IsEmpty && t.Nibble.Value <= bucketByteIndex.Value) + { + return GetAbsoluteAddress(mapBase, mapIdx, t.Nibble); + } + + // search backwards through the current map unit + // we processed the lsb nibble, move to the next one + t = t.ShiftNextNibble; + + // if there's any nibble set in the current unit, find it + if (!t.IsEmpty) + { + mapIdx = mapIdx.Prev; + while (t.Nibble.IsEmpty) + { + t = t.ShiftNextNibble; + mapIdx = mapIdx.Prev; + } + return GetAbsoluteAddress(mapBase, mapIdx, t.Nibble); + } + + // We finished the current map unit, we want to move to the previous one. + // But if we were in the first map unit, we can stop + if (mapIdx.InFirstMapUnit) + { + return TargetPointer.Null; + } + + // We're now done with the current map unit. + // Align the map index to the current map unit, then move back one nibble into the previous map unit + mapIdx = mapIdx.AlignDownToMapUnit(); + mapIdx = mapIdx.Prev; + + // read the map unit containing mapIdx and skip over it if it is all zeros + while (true) + { + t = mapIdx.ReadMapUnit(_target, mapStart); + if (!t.IsEmpty) + break; + if (mapIdx.InFirstMapUnit) + { + // we're at the first map unit and all the bits in the map unit are zero, + // there is no code header to find + return TargetPointer.Null; + } + mapIdx = mapIdx.PrevMapUnit; + } + + Debug.Assert(!t.IsEmpty); + + // move to the correct nibble in the map unit + while (!mapIdx.IsZero && t.Nibble.IsEmpty) + { + t = t.ShiftNextNibble; + mapIdx = mapIdx.Prev; + } + + if (mapIdx.IsZero && t.IsEmpty) + { + return TargetPointer.Null; + } + + return GetAbsoluteAddress(mapBase, mapIdx, t.Nibble); + } + + public static INibbleMap Create(Target target) + { + return new NibbleMapLinearLookup(target); + } + + public TargetPointer FindMethodCode(Data.CodeHeapListNode heapListNode, TargetCodePointer jittedCodeAddress) + { + if (jittedCodeAddress < heapListNode.StartAddress || jittedCodeAddress > heapListNode.EndAddress) + { + return TargetPointer.Null; + } + + return FindMethodCode(heapListNode.MapBase, heapListNode.HeaderMap, jittedCodeAddress); + } + +} diff --git a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/ExecutionManagerHelpers/RangeSectionMap.cs b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/RangeSectionMap.cs similarity index 100% rename from src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/ExecutionManagerHelpers/RangeSectionMap.cs rename to src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/Helpers/RangeSectionMap.cs diff --git a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/ExecutionManagerHelpers/NibbleMap.cs b/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/ExecutionManagerHelpers/NibbleMap.cs deleted file mode 100644 index aead8c6909394..0000000000000 --- a/src/native/managed/cdacreader/Microsoft.Diagnostics.DataContractReader.Contracts/ExecutionManagerHelpers/NibbleMap.cs +++ /dev/null @@ -1,294 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Numerics; -using System.Diagnostics; -using System; - -namespace Microsoft.Diagnostics.DataContractReader.ExecutionManagerHelpers; - -// Given a contiguous region of memory in which we lay out a collection of non-overlapping code blocks that are -// not too small (so that two adjacent ones aren't too close together) and where the start of each code block is preceeded by a code header aligned on some power of 2, -// we can break up the whole memory space into buckets of a fixed size (32-bytes in the current implementation), where -// each bucket either has a code block header or not. -// Thinking of each code block header address as a hex number, we can view it as: [index, offset, zeros] -// where each index gives us a bucket and the offset gives us the position of the header within the bucket. -// We encode each offset into a 4-bit nibble, reserving the special value 0 to mark the places in the map where a method doesn't start. -// -// To find the start of a method given an address we first convert it into a bucket index (giving the map unit) -// and an offset which we can then turn into the index of the nibble that covers that address. -// If the nibble is non-zero, we have the start of a method and it is near the given address. -// If the nibble is zero, we have to search backward first through the current map unit, and then through previous map -// units until we find a non-zero nibble. -// -// For example (all code addresses are relative to some unspecified base): -// Suppose there is code starting at address 304 (0x130) -// Then the map index will be 304 / 32 = 9 and the byte offset will be 304 % 32 = 16 -// Because addresses are 4-byte aligned, the nibble value will be 1 + 16 / 4 = 5 (we reserve 0 to mean no method). -// So the map unit containing index 9 will contain the value 0x5 << 24 (the map index 9 means we want the second nibble in the second map unit, and we number the nibbles starting from the most significant) -// Or 0x05000000 -// -// Now suppose we do a lookup for address 306 (0x132) -// The map index will be 306 / 32 = 9 and the byte offset will be 306 % 32 = 18 -// The nibble value will be 1 + 18 / 4 = 5 -// To do the lookup, we will load the map unit with index 9 (so the second 32-bit unit in the map) and get the value 0x05000000 -// We will then shift to focus on the nibble with map index 9 (which again has nibble shift 24), so -// the map unit will be 0x00000005 and we will get the nibble value 5. -// Therefore we know that there is a method start at map index 9, nibble value 5. -// The map index corresponds to an offset of 288 bytes and the nibble value 5 corresponds to an offset of (5 - 1) * 4 = 16 bytes -// So the method starts at offset 288 + 16 = 304, which is the address we were looking for. -// -// Now suppose we do a lookup for address 302 (0x12E) -// The map index will be 302 / 32 = 9 and the byte offset will be 302 % 32 = 14 -// The nibble value will be 1 + 14 / 4 = 4 -// To do the lookup, we will load the map unit containing map index 9 and get the value 0x05000000 -// We will then shift to focus on the nibble with map index 9 (which again has nibble shift 24), so we will get -// the nibble value 5. -// Therefore we know that there is a method start at map index 9, nibble value 5. -// But the address we're looking for is map index 9, nibble value 4. -// We know that methods can't start within 32-bytes of each other, so we know that the method we're looking for is not in the current nibble. -// We will then try to shift to the previous nibble in the map unit (0x00000005 >> 4 = 0x00000000) -// Therefore we know there is no method start at any map index in the current map unit. -// We will then align the map index to the start of the current map unit (map index 8) and move back to the previous map unit (map index 7) -// At that point, we scan backwards for non-zero map units. Since there are none, we return null. - -internal class NibbleMap -{ - // We load the map contents as 32-bit integers, which contains 8 4-bit nibbles. - // The algorithm will focus on each nibble in a map unit before moving on to the previous map unit - internal readonly struct MapUnit - { - public const int SizeInBytes = sizeof(uint); - public const ulong SizeInNibbles = 2 * SizeInBytes; - public readonly uint Value; - - public MapUnit(uint value) => Value = value; - - public override string ToString() => $"0x{Value:x}"; - - // Shift the next nibble into the least significant position. - public MapUnit ShiftNextNibble => new MapUnit(Value >>> 4); - - public const uint NibbleMask = 0x0Fu; - internal Nibble Nibble => new ((int)(Value & NibbleMask)); - - public bool IsZero => Value == 0; - - // Assuming mapIdx is the index of a nibble within the current map unit, - // shift the unit so that nibble is in the least significant position and return the result. - public MapUnit FocusOnIndexedNibble(MapKey mapIdx) - { - int shift = mapIdx.GetNibbleShift(); - return new MapUnit (Value >>> shift); - } - } - - // Each nibble is a 4-bit integer that gives an offset within a bucket. - // We reserse 0 to mean that there is no method starting at any offset within a bucket - internal readonly struct Nibble - { - public readonly int Value; - - public Nibble(int value) - { - Debug.Assert (value >= 0 && value <= 0xF); - Value = value; - } - - public static Nibble Zero => new Nibble(0); - public bool IsEmpty => Value == 0; - - public ulong TargetByteOffset - { - get - { - Debug.Assert(Value != 0); - return (uint)(Value - 1) * MapUnit.SizeInBytes; - } - } - } - - // The key to the map is the index of an individual nibble - internal readonly struct MapKey - { - private readonly ulong MapIdx; - public MapKey(ulong mapIdx) => MapIdx = mapIdx; - public override string ToString() => $"0x{MapIdx:x}"; - - // The offset of the address in the target space that this map index represents - public ulong TargetByteOffset => MapIdx * BytesPerBucket; - - // The index of the map unit that contains this map index - public ulong ContainingMapUnitIndex => MapIdx / MapUnit.SizeInNibbles; - - // The offset of the map unit that contains this map index - public ulong ContainingMapUnitByteOffset => ContainingMapUnitIndex * MapUnit.SizeInBytes; - - // The map index is the index of a nibble within the map, this gives the index of that nibble within a map unit. - public int NibbleIndexInMapUnit => (int)(MapIdx & (MapUnit.SizeInNibbles - 1)); - - // go to the previous nibble - public MapKey Prev => new MapKey(MapIdx - 1); - - // to to the previous map unit - public MapKey PrevMapUnit => new MapKey(MapIdx - MapUnit.SizeInNibbles); - - // Get a MapKey that is aligned to the first nibble in the map unit that contains this map index - public MapKey AlignDownToMapUnit() =>new MapKey(MapIdx & (~(MapUnit.SizeInNibbles - 1))); - - // If the map index is less than the size of a map unit, we are in the first MapUnit and - // can stop searching - public bool InFirstMapUnit => MapIdx < MapUnit.SizeInNibbles; - - public bool IsZero => MapIdx == 0; - - // given the index of a nibble in the map, compute how much we have to shift a MapUnit to put that - // nibble in the least significant position. - internal int GetNibbleShift() - { - return 28 - (NibbleIndexInMapUnit * 4); // bit shift - 4 bits per nibble - } - } - - public static NibbleMap Create(Target target) - { - return new NibbleMap(target); - } - - private readonly Target _target; - private NibbleMap(Target target) - { - _target = target; - } - - - - // we will partition the address space into buckets of this many bytes. - // There is at most one code block header per bucket. - // Normally we would then need 5 bits (Log2(BytesPerBucket))to find the exact start address, - // but because code headers are aligned, we can store the offset in a 4-bit nibble instead and shift appropriately to compute - // the effective address - private const ulong BytesPerBucket = 8 * MapUnit.SizeInBytes; - - - // for tests - internal static int ComputeNibbleShift(MapKey mapIdx) => mapIdx.GetNibbleShift(); - - // Given a base address, a map index, and a nibble value, compute the absolute address in memory - // that the index and nibble point to. - private static TargetPointer GetAbsoluteAddress(TargetPointer baseAddress, MapKey mapIdx, Nibble nibble) - { - return baseAddress + mapIdx.TargetByteOffset + nibble.TargetByteOffset; - } - - // Given a relative address, decompose it into - // the bucket index and an offset within the bucket. - private static void DecomposeAddress(TargetNUInt relative, out MapKey mapIdx, out Nibble bucketByteIndex) - { - mapIdx = new (relative.Value / BytesPerBucket); - int bucketByteOffset = (int)(relative.Value & (BytesPerBucket - 1)); - bucketByteIndex = new Nibble ((bucketByteOffset / MapUnit.SizeInBytes) + 1); - } - - internal static TargetPointer RoundTripAddress(TargetPointer mapBase, TargetPointer currentPC) - { - TargetNUInt relativeAddress = new TargetNUInt(currentPC.Value - mapBase.Value); - DecomposeAddress(relativeAddress, out MapKey mapIdx, out Nibble bucketByteIndex); - return GetAbsoluteAddress(mapBase, mapIdx, bucketByteIndex); - } - - private MapUnit ReadMapUnit(TargetPointer mapStart, MapKey mapIdx) - { - // Given a logical index into the map, compute the address in memory where that map unit is located - TargetPointer mapUnitAdderss = mapStart + mapIdx.ContainingMapUnitByteOffset; - return new MapUnit (_target.Read(mapUnitAdderss)); - } - - internal TargetPointer FindMethodCode(TargetPointer mapBase, TargetPointer mapStart, TargetCodePointer currentPC) - { - TargetNUInt relativeAddress = new TargetNUInt(currentPC.Value - mapBase.Value); - DecomposeAddress(relativeAddress, out MapKey mapIdx, out Nibble bucketByteIndex); - - MapUnit t = ReadMapUnit(mapStart, mapIdx); - - // shift the nibble we want to the least significant position - t = t.FocusOnIndexedNibble(mapIdx); - - // if the nibble is non-zero, we have found the start of a method, - // but we need to check that the start is before the current address, not after - if (!t.Nibble.IsEmpty && t.Nibble.Value <= bucketByteIndex.Value) - { - return GetAbsoluteAddress(mapBase, mapIdx, t.Nibble); - } - - // search backwards through the current map unit - // we processed the lsb nibble, move to the next one - t = t.ShiftNextNibble; - - // if there's any nibble set in the current unit, find it - if (!t.IsZero) - { - mapIdx = mapIdx.Prev; - while (t.Nibble.IsEmpty) - { - t = t.ShiftNextNibble; - mapIdx = mapIdx.Prev; - } - return GetAbsoluteAddress(mapBase, mapIdx, t.Nibble); - } - - // We finished the current map unit, we want to move to the previous one. - // But if we were in the first map unit, we can stop - if (mapIdx.InFirstMapUnit) - { - return TargetPointer.Null; - } - - // We're now done with the current map unit. - // Align the map index to the current map unit, then move back one nibble into the previous map unit - mapIdx = mapIdx.AlignDownToMapUnit(); - mapIdx = mapIdx.Prev; - - // read the map unit containing mapIdx and skip over it if it is all zeros - while (true) - { - t = ReadMapUnit(mapStart, mapIdx); - if (!t.IsZero) - break; - if (mapIdx.InFirstMapUnit) - { - // we're at the first map unit and all the bits in the map unit are zero, - // there is no code header to find - return TargetPointer.Null; - } - mapIdx = mapIdx.PrevMapUnit; - } - - Debug.Assert(!t.IsZero); - - // move to the correct nibble in the map unit - while (!mapIdx.IsZero && t.Nibble.IsEmpty) - { - t = t.ShiftNextNibble; - mapIdx = mapIdx.Prev; - } - - if (mapIdx.IsZero && t.IsZero) - { - return TargetPointer.Null; - } - - return GetAbsoluteAddress(mapBase, mapIdx, t.Nibble); - } - - public TargetPointer FindMethodCode(Data.CodeHeapListNode heapListNode, TargetCodePointer jittedCodeAddress) - { - if (jittedCodeAddress < heapListNode.StartAddress || jittedCodeAddress > heapListNode.EndAddress) - { - return TargetPointer.Null; - } - - return FindMethodCode(heapListNode.MapBase, heapListNode.HeaderMap, jittedCodeAddress); - } - -} diff --git a/src/native/managed/cdacreader/tests/ExecutionManagerTestBuilder.cs b/src/native/managed/cdacreader/tests/ExecutionManager/ExecutionManagerTestBuilder.cs similarity index 78% rename from src/native/managed/cdacreader/tests/ExecutionManagerTestBuilder.cs rename to src/native/managed/cdacreader/tests/ExecutionManager/ExecutionManagerTestBuilder.cs index 614d63d3f9740..6a3e32dd49cb6 100644 --- a/src/native/managed/cdacreader/tests/ExecutionManagerTestBuilder.cs +++ b/src/native/managed/cdacreader/tests/ExecutionManager/ExecutionManagerTestBuilder.cs @@ -7,7 +7,7 @@ using InteriorMapValue = Microsoft.Diagnostics.DataContractReader.ExecutionManagerHelpers.RangeSectionMap.InteriorMapValue; -namespace Microsoft.Diagnostics.DataContractReader.UnitTests; +namespace Microsoft.Diagnostics.DataContractReader.UnitTests.ExecutionManager; internal class ExecutionManagerTestBuilder { @@ -36,103 +36,6 @@ public struct AllocationRange CodeHeaderStart = 0x0033_4000, CodeHeaderEnd = 0x0033_5000, }; - internal class NibbleMapTestBuilder - { - // This is the base address of the memory range that the map covers. - // The map works on code pointers as offsets from this address - // For testing we don't actually place anything into this space - private readonly TargetPointer MapBase; - - internal readonly MockTarget.Architecture Arch; - // this is the target memory representation of the nibble map itself - public readonly MockMemorySpace.HeapFragment NibbleMapFragment; - - public NibbleMapTestBuilder(TargetPointer mapBase, ulong mapRangeSize, TargetPointer mapStart,MockTarget.Architecture arch) - { - MapBase = mapBase; - Arch = arch; - int nibbleMapSize = (int)Addr2Pos(mapRangeSize); - NibbleMapFragment = new MockMemorySpace.HeapFragment { - Address = mapStart, - Data = new byte[nibbleMapSize], - Name = "Nibble Map", - }; - } - - public NibbleMapTestBuilder(TargetPointer mapBase, ulong mapRangeSize, MockMemorySpace.BumpAllocator allocator, MockTarget.Architecture arch) - { - MapBase = mapBase; - Arch = arch; - int nibbleMapSize = (int)Addr2Pos(mapRangeSize); - NibbleMapFragment = allocator.Allocate((ulong)nibbleMapSize, "Nibble Map"); - } - - const int Log2CodeAlign = 2; // N.B. this might be different on 64-bit in the future - const int Log2NibblesPerDword = 3; - const int Log2BytesPerBucket = Log2CodeAlign + Log2NibblesPerDword; - const int Log2NibbleSize = 2; - const int NibbleSize = 1 << Log2NibbleSize; - const uint NibblesPerDword = (8 * sizeof(uint)) >> Log2NibbleSize; - const uint NibblesPerDwordMask = NibblesPerDword - 1; - const uint BytesPerBucket = NibblesPerDword * (1 << Log2CodeAlign); - - const uint MaskBytesPerBucket = BytesPerBucket - 1; - - const uint NibbleMask = 0xf; - const int HighestNibbleBit = 32 - NibbleSize; - - const uint HighestNibbleMask = NibbleMask << HighestNibbleBit; - - private ulong Addr2Pos(ulong addr) - { - return addr >> Log2BytesPerBucket; - } - - private uint Addr2Offs(ulong addr) - { - return (uint) (((addr & MaskBytesPerBucket) >> Log2CodeAlign) + 1); - } - - private int Pos2ShiftCount (ulong addr) - { - return HighestNibbleBit - (int)((addr & NibblesPerDwordMask) << Log2NibbleSize); - } - public void AllocateCodeChunk(TargetCodePointer codeStart, int codeSize) - { - // paraphrased from EEJitManager::NibbleMapSetUnlocked - if (codeStart.Value < MapBase.Value) - { - throw new ArgumentException("Code start address is below the map base"); - } - ulong delta = codeStart.Value - MapBase.Value; - ulong pos = Addr2Pos(delta); - bool bSet = true; - uint value = bSet?Addr2Offs(delta):0; - - uint index = (uint) (pos >> Log2NibblesPerDword); - uint mask = ~(HighestNibbleMask >> (int)((pos & NibblesPerDwordMask) << Log2NibbleSize)); - - value = value << Pos2ShiftCount(pos); - - Span entry = NibbleMapFragment.Data.AsSpan((int)(index * sizeof(uint)), sizeof(uint)); - uint oldValue = TestPlaceholderTarget.ReadFromSpan(entry, Arch.IsLittleEndian); - - if (value != 0 && (oldValue & ~mask) != 0) - { - throw new InvalidOperationException("Overwriting existing offset"); - } - - uint newValue = (oldValue & mask) | value; - TestPlaceholderTarget.WriteToSpan(newValue, Arch.IsLittleEndian, entry); - } - } - - - internal static NibbleMapTestBuilder CreateNibbleMap(TargetPointer mapBase, ulong mapRangeSize, TargetPointer mapStart, MockTarget.Architecture arch) - { - return new NibbleMapTestBuilder(mapBase, mapRangeSize, mapStart, arch); - } - internal class RangeSectionMapTestBuilder { const ulong DefaultTopLevelAddress = 0x0000_1000u; // arbitrary @@ -149,7 +52,7 @@ internal class RangeSectionMapTestBuilder { } - public RangeSectionMapTestBuilder (TargetPointer topLevelAddress, MockMemorySpace.Builder builder) + public RangeSectionMapTestBuilder(TargetPointer topLevelAddress, MockMemorySpace.Builder builder) { _topLevelAddress = topLevelAddress; _builder = builder; @@ -271,6 +174,8 @@ public static RangeSectionMapTestBuilder CreateRangeSection(MockTarget.Architect return new RangeSectionMapTestBuilder(arch); } + internal int Version { get;} + internal MockMemorySpace.Builder Builder { get; } private readonly RangeSectionMapTestBuilder _rsmBuilder; @@ -280,12 +185,12 @@ public static RangeSectionMapTestBuilder CreateRangeSection(MockTarget.Architect internal readonly Dictionary TypeInfoCache = new(); - internal ExecutionManagerTestBuilder(MockTarget.Architecture arch, AllocationRange allocationRange) : this(new MockMemorySpace.Builder(new TargetTestHelpers(arch)), allocationRange) + internal ExecutionManagerTestBuilder(int version, MockTarget.Architecture arch, AllocationRange allocationRange) : this(version, new MockMemorySpace.Builder(new TargetTestHelpers(arch)), allocationRange) {} - - internal ExecutionManagerTestBuilder(MockMemorySpace.Builder builder, AllocationRange allocationRange, Dictionary? typeInfoCache = null) + internal ExecutionManagerTestBuilder(int version, MockMemorySpace.Builder builder, AllocationRange allocationRange, Dictionary? typeInfoCache = null) { + Version = version; Builder = builder; _rsmBuilder = new RangeSectionMapTestBuilder(ExecutionManagerCodeRangeMapAddress, builder); _rangeSectionMapAllocator = Builder.CreateAllocator(allocationRange.RangeSectionMapStart, allocationRange.RangeSectionMapEnd); @@ -353,10 +258,17 @@ internal static void AddToTypeInfoCache(TargetTestHelpers targetTestHelpers, Dic }; } - internal NibbleMapTestBuilder CreateNibbleMap(ulong codeRangeStart, uint codeRangeSize) + internal NibbleMapTestBuilderBase CreateNibbleMap(ulong codeRangeStart, uint codeRangeSize) { + NibbleMapTestBuilderBase nibBuilder = Version switch + { + 1 => new NibbleMapTestBuilder_1(codeRangeStart, codeRangeSize, _nibbleMapAllocator, Builder.TargetTestHelpers.Arch), + + // The nibblemap algorithm was changed in version 2 + 2 => new NibbleMapTestBuilder_2(codeRangeStart, codeRangeSize, _nibbleMapAllocator, Builder.TargetTestHelpers.Arch), + _ => throw new InvalidOperationException("Unknown version"), + }; - NibbleMapTestBuilder nibBuilder = new NibbleMapTestBuilder(codeRangeStart, codeRangeSize, _nibbleMapAllocator, Builder.TargetTestHelpers.Arch); Builder.AddHeapFragment(nibBuilder.NibbleMapFragment); return nibBuilder; } @@ -433,16 +345,16 @@ public TargetPointer AddCodeHeapListNode(TargetPointer next, TargetPointer start // offset from the start of the code private uint CodeHeaderOffset => CodeHeaderSize; - private (MockMemorySpace.HeapFragment fragment, TargetCodePointer codeStart) AllocateJittedMethod(JittedCodeRange jittedCodeRange, int codeSize, string name = "Method Header & Code") + private (MockMemorySpace.HeapFragment fragment, TargetCodePointer codeStart) AllocateJittedMethod(JittedCodeRange jittedCodeRange, uint codeSize, string name = "Method Header & Code") { - ulong size = (ulong)(codeSize + CodeHeaderOffset); + ulong size = codeSize + CodeHeaderOffset; MockMemorySpace.HeapFragment methodFragment = jittedCodeRange.Allocator.Allocate(size, name); Builder.AddHeapFragment(methodFragment); TargetCodePointer codeStart = methodFragment.Address + CodeHeaderOffset; return (methodFragment, codeStart); } - public TargetCodePointer AddJittedMethod(JittedCodeRange jittedCodeRange, int codeSize, TargetPointer methodDescAddress) + public TargetCodePointer AddJittedMethod(JittedCodeRange jittedCodeRange, uint codeSize, TargetPointer methodDescAddress) { (MockMemorySpace.HeapFragment methodFragment, TargetCodePointer codeStart) = AllocateJittedMethod(jittedCodeRange, codeSize); diff --git a/src/native/managed/cdacreader/tests/ExecutionManager/ExecutionManagerTests.cs b/src/native/managed/cdacreader/tests/ExecutionManager/ExecutionManagerTests.cs new file mode 100644 index 0000000000000..df151ee05dcac --- /dev/null +++ b/src/native/managed/cdacreader/tests/ExecutionManager/ExecutionManagerTests.cs @@ -0,0 +1,193 @@ +// 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 System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Contracts; + +namespace Microsoft.Diagnostics.DataContractReader.UnitTests.ExecutionManager; + +public class ExecutionManagerTests +{ + + internal class ExecutionManagerTestTarget : TestPlaceholderTarget + { + private readonly ulong _topRangeSectionMap; + + public static ExecutionManagerTestTarget FromBuilder(ExecutionManagerTestBuilder emBuilder) + { + var arch = emBuilder.Builder.TargetTestHelpers.Arch; + ReadFromTargetDelegate reader = emBuilder.Builder.GetReadContext().ReadFromTarget; + var topRangeSectionMap = ExecutionManagerTestBuilder.ExecutionManagerCodeRangeMapAddress; + var typeInfo = emBuilder.TypeInfoCache; + return new ExecutionManagerTestTarget(emBuilder.Version, arch, reader, topRangeSectionMap, typeInfo); + } + + public ExecutionManagerTestTarget(int version, MockTarget.Architecture arch, ReadFromTargetDelegate dataReader, TargetPointer topRangeSectionMap, Dictionary typeInfoCache) : base(arch) + { + _topRangeSectionMap = topRangeSectionMap; + SetDataReader(dataReader); + SetTypeInfoCache(typeInfoCache); + SetDataCache(new DefaultDataCache(this)); + IContractFactory emfactory = new ExecutionManagerFactory(); + SetContracts(new TestRegistry() { + ExecutionManagerContract = new (() => emfactory.CreateContract(this, version)), + }); + } + public override TargetPointer ReadGlobalPointer(string global) + { + switch (global) + { + case Constants.Globals.ExecutionManagerCodeRangeMapAddress: + return new TargetPointer(_topRangeSectionMap); + default: + return base.ReadGlobalPointer(global); + } + } + + public override T ReadGlobal(string name) + { + switch (name) + { + case Constants.Globals.StubCodeBlockLast: + if (typeof(T) == typeof(byte)) + return (T)(object)(byte)0x0Fu; + break; + default: + break; + } + return base.ReadGlobal(name); + + } + + } + + [Theory] + [MemberData(nameof(StdArchAllVersions))] + public void LookupNull(int version, MockTarget.Architecture arch) + { + ExecutionManagerTestBuilder emBuilder = new (version, arch, ExecutionManagerTestBuilder.DefaultAllocationRange); + emBuilder.MarkCreated(); + var target = ExecutionManagerTestTarget.FromBuilder (emBuilder); + + var em = target.Contracts.ExecutionManager; + Assert.NotNull(em); + var eeInfo = em.GetCodeBlockHandle(TargetCodePointer.Null); + Assert.Null(eeInfo); + } + + [Theory] + [MemberData(nameof(StdArchAllVersions))] + public void LookupNonNullMissing(int version, MockTarget.Architecture arch) + { + ExecutionManagerTestBuilder emBuilder = new (version, arch, ExecutionManagerTestBuilder.DefaultAllocationRange); + emBuilder.MarkCreated(); + var target = ExecutionManagerTestTarget.FromBuilder (emBuilder); + + var em = target.Contracts.ExecutionManager; + Assert.NotNull(em); + var eeInfo = em.GetCodeBlockHandle(new TargetCodePointer(0x0a0a_0000)); + Assert.Null(eeInfo); + } + + [Theory] + [MemberData(nameof(StdArchAllVersions))] + public void LookupNonNullOneRangeOneMethod(int version, MockTarget.Architecture arch) + { + const ulong codeRangeStart = 0x0a0a_0000u; // arbitrary + const uint codeRangeSize = 0xc000u; // arbitrary + const uint methodSize = 0x450; // arbitrary + + TargetPointer jitManagerAddress = new (0x000b_ff00); // arbitrary + + TargetPointer expectedMethodDescAddress = new TargetPointer(0x0101_aaa0); + + ExecutionManagerTestBuilder emBuilder = new(version, arch, ExecutionManagerTestBuilder.DefaultAllocationRange); + var jittedCode = emBuilder.AllocateJittedCodeRange(codeRangeStart, codeRangeSize); + + TargetCodePointer methodStart = emBuilder.AddJittedMethod(jittedCode, methodSize, expectedMethodDescAddress); + + NibbleMapTestBuilderBase nibBuilder = emBuilder.CreateNibbleMap(codeRangeStart, codeRangeSize); + nibBuilder.AllocateCodeChunk(methodStart, methodSize); + + TargetPointer codeHeapListNodeAddress = emBuilder.AddCodeHeapListNode(TargetPointer.Null, codeRangeStart, codeRangeStart + codeRangeSize, codeRangeStart, nibBuilder.NibbleMapFragment.Address); + TargetPointer rangeSectionAddress = emBuilder.AddRangeSection(jittedCode, jitManagerAddress: jitManagerAddress, codeHeapListNodeAddress: codeHeapListNodeAddress); + TargetPointer rangeSectionFragmentAddress = emBuilder.AddRangeSectionFragment(jittedCode, rangeSectionAddress); + + emBuilder.MarkCreated(); + + var target = ExecutionManagerTestTarget.FromBuilder(emBuilder); + + var em = target.Contracts.ExecutionManager; + Assert.NotNull(em); + + // test at method start + var eeInfo = em.GetCodeBlockHandle(methodStart); + Assert.NotNull(eeInfo); + TargetPointer actualMethodDesc = em.GetMethodDesc(eeInfo.Value); + Assert.Equal(expectedMethodDescAddress, actualMethodDesc); + + // test middle of method + eeInfo = em.GetCodeBlockHandle(methodStart + methodSize / 2); + Assert.NotNull(eeInfo); + actualMethodDesc = em.GetMethodDesc(eeInfo.Value); + Assert.Equal(expectedMethodDescAddress, actualMethodDesc); + + // test end of method + eeInfo = em.GetCodeBlockHandle(methodStart + methodSize - 1); + Assert.NotNull(eeInfo); + actualMethodDesc = em.GetMethodDesc(eeInfo.Value); + Assert.Equal(expectedMethodDescAddress, actualMethodDesc); + } + + [Theory] + [MemberData(nameof(StdArchAllVersions))] + public void LookupNullOneRangeZeroMethod(int version, MockTarget.Architecture arch) + { + const ulong codeRangeStart = 0x0a0a_0000u; // arbitrary + const uint codeRangeSize = 0xc000u; // arbitrary + + TargetPointer jitManagerAddress = new (0x000b_ff00); // arbitrary + + ExecutionManagerTestBuilder emBuilder = new(version, arch, ExecutionManagerTestBuilder.DefaultAllocationRange); + var jittedCode = emBuilder.AllocateJittedCodeRange(codeRangeStart, codeRangeSize); + + NibbleMapTestBuilderBase nibBuilder = emBuilder.CreateNibbleMap(codeRangeStart, codeRangeSize); + + TargetPointer codeHeapListNodeAddress = emBuilder.AddCodeHeapListNode(TargetPointer.Null, codeRangeStart, codeRangeStart + codeRangeSize, codeRangeStart, nibBuilder.NibbleMapFragment.Address); + TargetPointer rangeSectionAddress = emBuilder.AddRangeSection(jittedCode, jitManagerAddress: jitManagerAddress, codeHeapListNodeAddress: codeHeapListNodeAddress); + TargetPointer rangeSectionFragmentAddress = emBuilder.AddRangeSectionFragment(jittedCode, rangeSectionAddress); + + emBuilder.MarkCreated(); + + var target = ExecutionManagerTestTarget.FromBuilder(emBuilder); + + var em = target.Contracts.ExecutionManager; + Assert.NotNull(em); + + // test at code range start + var eeInfo = em.GetCodeBlockHandle(codeRangeSize + codeRangeSize); + Assert.Null(eeInfo); + + // test middle of code range + eeInfo = em.GetCodeBlockHandle(codeRangeSize + codeRangeSize / 2); + Assert.Null(eeInfo); + + // test end of code range + eeInfo = em.GetCodeBlockHandle(codeRangeSize + codeRangeSize - 1); + Assert.Null(eeInfo); + } + + public static IEnumerable StdArchAllVersions() + { + const int highestVersion = 2; + foreach(object[] arr in new MockTarget.StdArch()) + { + MockTarget.Architecture arch = (MockTarget.Architecture)arr[0]; + for(int version = 1; version <= highestVersion; version++){ + yield return new object[] { version, arch }; + } + } + } +} diff --git a/src/native/managed/cdacreader/tests/ExecutionManager/NibbleMapTestBuilder.cs b/src/native/managed/cdacreader/tests/ExecutionManager/NibbleMapTestBuilder.cs new file mode 100644 index 0000000000000..3e51e6f890cec --- /dev/null +++ b/src/native/managed/cdacreader/tests/ExecutionManager/NibbleMapTestBuilder.cs @@ -0,0 +1,172 @@ +// 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 Microsoft.Diagnostics.DataContractReader.ExecutionManagerHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.UnitTests.ExecutionManager; + +internal abstract class NibbleMapTestBuilderBase +{ + // This is the base address of the memory range that the map covers. + // The map works on code pointers as offsets from this address + // For testing we don't actually place anything into this space + protected TargetPointer MapBase { get; init; } + + public MockTarget.Architecture Arch { get; init; } + + // this is the target memory representation of the nibble map itself + public MockMemorySpace.HeapFragment NibbleMapFragment { get; init; } + + protected const int Log2CodeAlign = 2; // This might be different on 64-bit in the future + protected const int Log2NibblesPerDword = 3; + protected const int Log2BytesPerBucket = Log2CodeAlign + Log2NibblesPerDword; + protected const int Log2NibbleSize = 2; + protected const int NibbleSize = 1 << Log2NibbleSize; + protected const uint NibblesPerDword = (8 * sizeof(uint)) >>> Log2NibbleSize; + protected const uint NibblesPerDwordMask = NibblesPerDword - 1; + protected const uint BytesPerBucket = NibblesPerDword * (1 << Log2CodeAlign); + + protected const uint MaskBytesPerBucket = BytesPerBucket - 1; + + protected const uint NibbleMask = 0xf; + protected const int HighestNibbleBit = 32 - NibbleSize; + + protected const uint HighestNibbleMask = NibbleMask << HighestNibbleBit; + + protected ulong Addr2Pos(ulong addr) + { + return addr >>> Log2BytesPerBucket; + } + + protected uint Addr2Offs(ulong addr) + { + return (uint) (((addr & MaskBytesPerBucket) >>> Log2CodeAlign) + 1); + } + + protected int Pos2ShiftCount (ulong addr) + { + return HighestNibbleBit - (int)((addr & NibblesPerDwordMask) << Log2NibbleSize); + } + + public NibbleMapTestBuilderBase(TargetPointer mapBase, ulong mapRangeSize, TargetPointer mapStart, MockTarget.Architecture arch) + { + MapBase = mapBase; + Arch = arch; + int nibbleMapSize = (int)Addr2Pos(mapRangeSize); + NibbleMapFragment = new MockMemorySpace.HeapFragment { + Address = mapStart, + Data = new byte[nibbleMapSize], + Name = "Nibble Map", + }; + } + + public NibbleMapTestBuilderBase(TargetPointer mapBase, ulong mapRangeSize, MockMemorySpace.BumpAllocator allocator, MockTarget.Architecture arch) + { + MapBase = mapBase; + Arch = arch; + int nibbleMapSize = (int)Addr2Pos(mapRangeSize); + NibbleMapFragment = allocator.Allocate((ulong)nibbleMapSize, "Nibble Map"); + } + + public abstract void AllocateCodeChunk(TargetCodePointer codeStart, uint codeSize); +} + +internal class NibbleMapTestBuilder_1 : NibbleMapTestBuilderBase +{ + public NibbleMapTestBuilder_1(TargetPointer mapBase, ulong mapRangeSize, TargetPointer mapStart, MockTarget.Architecture arch) + : base(mapBase, mapRangeSize, mapStart, arch) + { + } + + public NibbleMapTestBuilder_1(TargetPointer mapBase, ulong mapRangeSize, MockMemorySpace.BumpAllocator allocator, MockTarget.Architecture arch) + : base(mapBase, mapRangeSize, allocator, arch) + { + } + + public override void AllocateCodeChunk(TargetCodePointer codeStart, uint codeSize) + { + // paraphrased from EEJitManager::NibbleMapSetUnlocked + if (codeStart.Value < MapBase.Value) + { + throw new ArgumentException("Code start address is below the map base"); + } + ulong delta = codeStart.Value - MapBase.Value; + ulong pos = Addr2Pos(delta); + bool bSet = true; + uint value = bSet?Addr2Offs(delta):0; + + uint index = (uint) (pos >>> Log2NibblesPerDword); + uint mask = ~(HighestNibbleMask >>> (int)((pos & NibblesPerDwordMask) << Log2NibbleSize)); + + value = value << Pos2ShiftCount(pos); + + Span entry = NibbleMapFragment.Data.AsSpan((int)(index * sizeof(uint)), sizeof(uint)); + uint oldValue = TestPlaceholderTarget.ReadFromSpan(entry, Arch.IsLittleEndian); + + if (value != 0 && (oldValue & ~mask) != 0) + { + throw new InvalidOperationException("Overwriting existing offset"); + } + + uint newValue = (oldValue & mask) | value; + TestPlaceholderTarget.WriteToSpan(newValue, Arch.IsLittleEndian, entry); + } +} + +internal class NibbleMapTestBuilder_2 : NibbleMapTestBuilderBase +{ + public NibbleMapTestBuilder_2(TargetPointer mapBase, ulong mapRangeSize, TargetPointer mapStart, MockTarget.Architecture arch) + : base(mapBase, mapRangeSize, mapStart, arch) + { + } + + public NibbleMapTestBuilder_2(TargetPointer mapBase, ulong mapRangeSize, MockMemorySpace.BumpAllocator allocator, MockTarget.Architecture arch) + : base(mapBase, mapRangeSize, allocator, arch) + { + } + + public override void AllocateCodeChunk(TargetCodePointer codeStart, uint codeSize) + { + // paraphrased from EEJitManager::NibbleMapSetUnlocked + if (codeStart.Value < MapBase.Value) + { + throw new ArgumentException("Code start address is below the map base"); + } + ulong delta = codeStart.Value - MapBase.Value; + + ulong pos = Addr2Pos(delta); + uint value = Addr2Offs(delta); + + uint index = (uint) (pos >>> Log2NibblesPerDword); + uint mask = ~(HighestNibbleMask >>> (int)((pos & NibblesPerDwordMask) << Log2NibbleSize)); + + value = value << Pos2ShiftCount(pos); + + Span entry = NibbleMapFragment.Data.AsSpan((int)(index * sizeof(uint)), sizeof(uint)); + uint oldValue = TestPlaceholderTarget.ReadFromSpan(entry, Arch.IsLittleEndian); + + if (value != 0 && (oldValue & ~mask) != 0) + { + throw new InvalidOperationException("Overwriting existing offset"); + } + + uint newValue = (oldValue & mask) | value; + TestPlaceholderTarget.WriteToSpan(newValue, Arch.IsLittleEndian, entry); + + ulong firstByteAfterMethod = delta + (uint)codeSize; + uint encodedPointer = NibbleMapConstantLookup.EncodePointer((uint)delta); + index++; + while((index + 1) * 256 <= firstByteAfterMethod) + { + entry = NibbleMapFragment.Data.AsSpan((int)(index * sizeof(uint)), sizeof(uint)); + oldValue = TestPlaceholderTarget.ReadFromSpan(entry, Arch.IsLittleEndian); + if(oldValue != 0) + { + throw new InvalidOperationException("Overwriting existing offset"); + } + TestPlaceholderTarget.WriteToSpan(encodedPointer, Arch.IsLittleEndian, entry); + index++; + } + } +} diff --git a/src/native/managed/cdacreader/tests/NibbleMapTests.cs b/src/native/managed/cdacreader/tests/ExecutionManager/NibbleMapTests.cs similarity index 58% rename from src/native/managed/cdacreader/tests/NibbleMapTests.cs rename to src/native/managed/cdacreader/tests/ExecutionManager/NibbleMapTests.cs index e887ee4e2cdb1..c92fc3f1afa4c 100644 --- a/src/native/managed/cdacreader/tests/NibbleMapTests.cs +++ b/src/native/managed/cdacreader/tests/ExecutionManager/NibbleMapTests.cs @@ -1,15 +1,13 @@ // 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 Xunit; using Microsoft.Diagnostics.DataContractReader.ExecutionManagerHelpers; -using System.Diagnostics; -namespace Microsoft.Diagnostics.DataContractReader.UnitTests; +namespace Microsoft.Diagnostics.DataContractReader.UnitTests.ExecutionManager; -public class NibbleMapTests +public class NibbleMapTestsBase { internal class NibbleMapTestTarget : TestPlaceholderTarget { @@ -22,14 +20,16 @@ public NibbleMapTestTarget(MockTarget.Architecture arch, MockMemorySpace.ReadCon } - internal static NibbleMapTestTarget CreateTarget(ExecutionManagerTestBuilder.NibbleMapTestBuilder nibbleMapTestBuilder) + internal static NibbleMapTestTarget CreateTarget(NibbleMapTestBuilderBase nibbleMapTestBuilder) { return new NibbleMapTestTarget(nibbleMapTestBuilder.Arch, new MockMemorySpace.ReadContext() { HeapFragments = new[] { nibbleMapTestBuilder.NibbleMapFragment } }); } +} - +public class NibbleMapLinearLookupTests : NibbleMapTestsBase +{ [Fact] public void RoundTripAddressTest() { @@ -37,7 +37,7 @@ public void RoundTripAddressTest() uint delta = 0x10u; for (TargetPointer p = mapBase; p < mapBase + 0x1000; p += delta) { - TargetPointer actual = NibbleMap.RoundTripAddress(mapBase, p); + TargetPointer actual = NibbleMapHelpers.RoundTripAddress(mapBase, p); Assert.Equal(p, actual); } } @@ -57,8 +57,8 @@ public void ExhaustiveNibbbleShifts(ulong irrelevant) int expectedShift = 28; for (int i = 0; i < 255; i++) { - NibbleMap.MapKey input = new (irrelevant + (ulong)i); - int actualShift = NibbleMap.ComputeNibbleShift(input); + NibbleMapHelpers.MapKey input = new (irrelevant + (ulong)i); + uint actualShift = NibbleMapHelpers.ComputeNibbleShift(input); Assert.True(expectedShift == actualShift, $"Expected {expectedShift}, got {actualShift} for input {input}"); expectedShift -= 4; if (expectedShift == -4) @@ -81,17 +81,17 @@ public void NibbleMapOneItemLookupOk(MockTarget.Architecture arch) /// this is how big the address space is that the map covers const uint MapRangeSize = 0x1000; TargetPointer MapEnd = mapBase + MapRangeSize; - var builder = ExecutionManagerTestBuilder.CreateNibbleMap(mapBase, MapRangeSize, mapStart, arch); + var builder = new NibbleMapTestBuilder_1(mapBase, MapRangeSize, mapStart, arch); // don't put the code too close to the start - the NibbleMap bails if the code is too close to the start of the range TargetCodePointer inputPC = new(mapBase + 0x0200u); - int codeSize = 0x80; // doesn't matter + uint codeSize = 0x80; // doesn't matter builder.AllocateCodeChunk (inputPC, codeSize); NibbleMapTestTarget target = CreateTarget(builder); // TESTCASE: - NibbleMap map = NibbleMap.Create(target); + NibbleMapLinearLookup map = (NibbleMapLinearLookup)NibbleMapLinearLookup.Create(target); Assert.NotNull(map); TargetPointer methodCode = map.FindMethodCode(mapBase, mapStart, inputPC); @@ -124,6 +124,60 @@ public void NibbleMapOneItemLookupOk(MockTarget.Architecture arch) } } +} + +public class NibbleMapConstantLookupTests : NibbleMapTestsBase +{ + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void NibbleMapOneItemLookupOk(MockTarget.Architecture arch) + { + // SETUP: + + // this is the beginning of the address range where code pointers might point + TargetPointer mapBase = new(0x5f5f_0000u); + // this is the beginning of the nibble map itself + TargetPointer mapStart = new(0x0456_1000u); + /// this is how big the address space is that the map covers + const uint MapRangeSize = 0x1000; + TargetPointer MapEnd = mapBase + MapRangeSize; + var builder = new NibbleMapTestBuilder_2(mapBase, MapRangeSize, mapStart, arch); + // don't put the code too close to the start - the NibbleMap bails if the code is too close to the start of the range + TargetCodePointer inputPC = new(mapBase + 0x0200u); + uint codeSize = 0x400; + builder.AllocateCodeChunk (inputPC, codeSize); + NibbleMapTestTarget target = CreateTarget(builder); + // TESTCASE: + + NibbleMapConstantLookup map = (NibbleMapConstantLookup)NibbleMapConstantLookup.Create(target); + Assert.NotNull(map); + + TargetPointer methodCode = map.FindMethodCode(mapBase, mapStart, inputPC); + Assert.Equal(inputPC.Value, methodCode.Value); + + // All addresses in the code chunk should map to the same method + for (int i = 0; i < codeSize; i++) + { + methodCode = map.FindMethodCode(mapBase, mapStart, inputPC.Value + (uint)i); + // we should always find the beginning of the method + Assert.Equal(inputPC.Value, methodCode.Value); + } + + // All addresses before the code chunk should return null + for (ulong i = mapBase; i < inputPC; i++) + { + methodCode = map.FindMethodCode(mapBase, mapStart, i); + Assert.Equal(0u, methodCode.Value); + } + + // All addresses more than 512 bytes after the code chunk should return null + for (ulong i = inputPC.Value + (uint)codeSize + 512; i < MapEnd; i++) + { + methodCode = map.FindMethodCode(mapBase, mapStart, i); + Assert.Equal(0u, methodCode.Value); + } + } } + diff --git a/src/native/managed/cdacreader/tests/RangeSectionMapTests.cs b/src/native/managed/cdacreader/tests/ExecutionManager/RangeSectionMapTests.cs similarity index 97% rename from src/native/managed/cdacreader/tests/RangeSectionMapTests.cs rename to src/native/managed/cdacreader/tests/ExecutionManager/RangeSectionMapTests.cs index e13e670189ba6..36d3a5f9647d0 100644 --- a/src/native/managed/cdacreader/tests/RangeSectionMapTests.cs +++ b/src/native/managed/cdacreader/tests/ExecutionManager/RangeSectionMapTests.cs @@ -7,7 +7,7 @@ using Microsoft.Diagnostics.DataContractReader.ExecutionManagerHelpers; -namespace Microsoft.Diagnostics.DataContractReader.UnitTests; +namespace Microsoft.Diagnostics.DataContractReader.UnitTests.ExecutionManager; public class RangeSectionMapTests { diff --git a/src/native/managed/cdacreader/tests/ExecutionManagerTests.cs b/src/native/managed/cdacreader/tests/ExecutionManagerTests.cs deleted file mode 100644 index 274addc986b86..0000000000000 --- a/src/native/managed/cdacreader/tests/ExecutionManagerTests.cs +++ /dev/null @@ -1,131 +0,0 @@ -// 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.Contracts; -using System.Collections.Generic; -using System; -namespace Microsoft.Diagnostics.DataContractReader.UnitTests; - -public class ExecutionManagerTests -{ - - internal class ExecutionManagerTestTarget : TestPlaceholderTarget - { - private readonly ulong _topRangeSectionMap; - - public static ExecutionManagerTestTarget FromBuilder (ExecutionManagerTestBuilder emBuilder) - { - var arch = emBuilder.Builder.TargetTestHelpers.Arch; - ReadFromTargetDelegate reader = emBuilder.Builder.GetReadContext().ReadFromTarget; - var topRangeSectionMap = ExecutionManagerTestBuilder.ExecutionManagerCodeRangeMapAddress; - var typeInfo = emBuilder.TypeInfoCache; - return new ExecutionManagerTestTarget(arch, reader, topRangeSectionMap, typeInfo); - } - - public ExecutionManagerTestTarget(MockTarget.Architecture arch, ReadFromTargetDelegate dataReader, TargetPointer topRangeSectionMap, Dictionary typeInfoCache) : base(arch) - { - _topRangeSectionMap = topRangeSectionMap; - SetDataReader(dataReader); - SetTypeInfoCache(typeInfoCache); - SetDataCache(new DefaultDataCache(this)); - IContractFactory emfactory = new ExecutionManagerFactory(); - SetContracts(new TestRegistry() { - ExecutionManagerContract = new (() => emfactory.CreateContract(this, 1)), - }); - } - public override TargetPointer ReadGlobalPointer(string global) - { - switch (global) - { - case Constants.Globals.ExecutionManagerCodeRangeMapAddress: - return new TargetPointer(_topRangeSectionMap); - default: - return base.ReadGlobalPointer(global); - } - } - - public override T ReadGlobal(string name) - { - switch (name) - { - case Constants.Globals.StubCodeBlockLast: - if (typeof(T) == typeof(byte)) - return (T)(object)(byte)0x0Fu; - break; - default: - break; - } - return base.ReadGlobal(name); - - } - - } - - [Theory] - [ClassData(typeof(MockTarget.StdArch))] - public void LookupNull(MockTarget.Architecture arch) - { - ExecutionManagerTestBuilder emBuilder = new (arch, ExecutionManagerTestBuilder.DefaultAllocationRange); - emBuilder.MarkCreated(); - var target = ExecutionManagerTestTarget.FromBuilder (emBuilder); - - var em = target.Contracts.ExecutionManager; - Assert.NotNull(em); - var eeInfo = em.GetCodeBlockHandle(TargetCodePointer.Null); - Assert.Null(eeInfo); - } - - [Theory] - [ClassData(typeof(MockTarget.StdArch))] - public void LookupNonNullMissing(MockTarget.Architecture arch) - { - ExecutionManagerTestBuilder emBuilder = new (arch, ExecutionManagerTestBuilder.DefaultAllocationRange); - emBuilder.MarkCreated(); - var target = ExecutionManagerTestTarget.FromBuilder (emBuilder); - - var em = target.Contracts.ExecutionManager; - Assert.NotNull(em); - var eeInfo = em.GetCodeBlockHandle(new TargetCodePointer(0x0a0a_0000)); - Assert.Null(eeInfo); - } - - [Theory] - [ClassData(typeof(MockTarget.StdArch))] - public void LookupNonNullOneRangeOneMethod(MockTarget.Architecture arch) - { - const ulong codeRangeStart = 0x0a0a_0000u; // arbitrary - const uint codeRangeSize = 0xc000u; // arbitrary - int methodSize = 0x100; // arbitrary - - TargetPointer jitManagerAddress = new (0x000b_ff00); // arbitrary - - TargetPointer expectedMethodDescAddress = new TargetPointer(0x0101_aaa0); - - ExecutionManagerTestBuilder emBuilder = new (arch, ExecutionManagerTestBuilder.DefaultAllocationRange); - var jittedCode = emBuilder.AllocateJittedCodeRange(codeRangeStart, codeRangeSize); - - TargetCodePointer methodStart = emBuilder.AddJittedMethod(jittedCode, methodSize, expectedMethodDescAddress); - - ExecutionManagerTestBuilder.NibbleMapTestBuilder nibBuilder = emBuilder.CreateNibbleMap(codeRangeStart, codeRangeSize); - nibBuilder.AllocateCodeChunk(methodStart, methodSize); - - TargetPointer codeHeapListNodeAddress = emBuilder.AddCodeHeapListNode(TargetPointer.Null, codeRangeStart, codeRangeStart + codeRangeSize, codeRangeStart, nibBuilder.NibbleMapFragment.Address); - TargetPointer rangeSectionAddress = emBuilder.AddRangeSection(jittedCode, jitManagerAddress: jitManagerAddress, codeHeapListNodeAddress: codeHeapListNodeAddress); - TargetPointer rangeSectionFragmentAddress = emBuilder.AddRangeSectionFragment(jittedCode, rangeSectionAddress); - - emBuilder.MarkCreated(); - - var target = ExecutionManagerTestTarget.FromBuilder(emBuilder); - - // test - - var em = target.Contracts.ExecutionManager; - Assert.NotNull(em); - var eeInfo = em.GetCodeBlockHandle(methodStart); - Assert.NotNull(eeInfo); - TargetPointer actualMethodDesc = em.GetMethodDesc(eeInfo.Value); - Assert.Equal(expectedMethodDescAddress, actualMethodDesc); - } -}