Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[cdac] add v2 ExecutionManager contract for NibbleMap change #109654

Merged
merged 12 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 64 additions & 5 deletions docs/design/datacontracts/ExecutionManager.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> : 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> : IExecutionManager
where T : INibbleMap
{
internal readonly Target _target;

Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<IExecutionManager>
Expand All @@ -14,6 +12,9 @@ IExecutionManager IContractFactory<IExecutionManager>.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),
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<NibbleMapLinearLookup>
{
public ExecutionManager_1(Target target, Data.RangeSectionMap topRangeSectionMap) : base(target, topRangeSectionMap)
{
}
}
Original file line number Diff line number Diff line change
@@ -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<NibbleMapConstantLookup>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the move to composability here. I think it is also worth considering (perhaps as a separate change) how this could help with out unit testing. Are we now at a place where we can have a mock nibble map for testing the execution manager? For example, we could have the nibble map tests have the extensive coverage of the nibble map itself and then the execution manager tests could expect the nibble map to be correct based on that coverage and mock out INibbleMap.FindMethodCode to return what is needed instead of having to build out the memory.

{
public ExecutionManager_2(Target target, Data.RangeSectionMap topRangeSectionMap) : base(target, topRangeSectionMap)
{
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading
Loading