From 76b528e1d26a8c8751147d7672e2c0733502ae0a Mon Sep 17 00:00:00 2001 From: Louis DeJardin Date: Fri, 18 Sep 2015 16:29:29 -0700 Subject: [PATCH] Adding comments to MemoryPool classes --- .../Http/MemoryPool2.cs | 66 ++++++++++++++- .../Http/MemoryPoolBlock2.cs | 81 ++++++++++++++++++- .../Http/MemoryPoolSlab2.cs | 33 ++++++++ 3 files changed, 177 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.AspNet.Server.Kestrel/Http/MemoryPool2.cs b/src/Microsoft.AspNet.Server.Kestrel/Http/MemoryPool2.cs index 9256bdb314..941fb9b796 100644 --- a/src/Microsoft.AspNet.Server.Kestrel/Http/MemoryPool2.cs +++ b/src/Microsoft.AspNet.Server.Kestrel/Http/MemoryPool2.cs @@ -3,22 +3,70 @@ using System.Collections.Concurrent; namespace Microsoft.AspNet.Server.Kestrel.Http { + /// + /// Used to allocate and distribute re-usable blocks of memory. + /// public class MemoryPool2 : IDisposable { + /// + /// The gap between blocks' starting address. 4096 is chosen because most operating systems are 4k pages in size and alignment. + /// private const int blockStride = 4096; + + /// + /// The last 64 bytes of a block are unused to prevent CPU from pre-fetching the next 64 byte into it's memory cache. + /// See https://github.com/aspnet/KestrelHttpServer/issues/117 and https://www.youtube.com/watch?v=L7zSU9HI-6I + /// private const int blockUnused = 64; + + /// + /// Allocating 32 contiguous blocks per slab makes the slab size 128k. This is larger than the 85k size which will place the memory + /// in the large object heap. This means the GC will not try to relocate this array, so the fact it remains pinned does not negatively + /// affect memory management's compactification. + /// private const int blockCount = 32; + + /// + /// 4096 - 64 gives you a blockLength of 4032 usable bytes per block. + /// private const int blockLength = blockStride - blockUnused; + + /// + /// 4096 * 32 gives you a slabLength of 128k contiguous bytes allocated per slab + /// private const int slabLength = blockStride * blockCount; - private ConcurrentStack _blocks = new ConcurrentStack(); - private ConcurrentStack _slabs = new ConcurrentStack(); + /// + /// Thread-safe collection of blocks which are currently in the pool. A slab will pre-allocate all of the block tracking objects + /// and add them to this collection. When memory is requested it is taken from here first, and when it is returned it is re-added. + /// + private readonly ConcurrentStack _blocks = new ConcurrentStack(); + + /// + /// Thread-safe collection of slabs which have been allocated by this pool. As long as a slab is in this collection and slab.IsActive, + /// the blocks will be added to _blocks when returned. + /// + private readonly ConcurrentStack _slabs = new ConcurrentStack(); + + /// + /// This is part of implementing the IDisposable pattern. + /// private bool disposedValue = false; // To detect redundant calls + /// + /// Called to take a block from the pool. + /// + /// The block returned must be at least this size. It may be larger than this minimum size, and if so, + /// the caller may write to the block's entire size rather than being limited to the minumumSize requested. + /// The block that is reserved for the called. It must be passed to Return when it is no longer being used. public MemoryPoolBlock2 Lease(int minimumSize) { if (minimumSize > blockLength) { + // The requested minimumSize is actually larger then the usable memory of a single block. + // Because this is the degenerate case, a one-time-use byte[] array and tracking object are allocated. + // When this block tracking object is returned it is not added to the pool - instead it will be + // allowed to be garbace collected normally. return MemoryPoolBlock2.Create( new ArraySegment(new byte[minimumSize]), dataPtr: IntPtr.Zero, @@ -31,12 +79,18 @@ namespace Microsoft.AspNet.Server.Kestrel.Http MemoryPoolBlock2 block; if (_blocks.TryPop(out block)) { + // block successfully taken from the stack - return it return block; } + // no blocks available - grow the pool and try again AllocateSlab(); } } + /// + /// Internal method called when a block is requested and the pool is empty. It allocates one additional slab, creates all of the + /// block tracking objects, and adds them all to the pool. + /// private void AllocateSlab() { var slab = MemoryPoolSlab2.Create(slabLength); @@ -58,6 +112,14 @@ namespace Microsoft.AspNet.Server.Kestrel.Http } } + /// + /// Called to return a block to the pool. Once Return has been called the memory no longer belongs to the caller, and + /// Very Bad Things will happen if the memory is read of modified subsequently. If a caller fails to call Return and the + /// block tracking object is garbage collected, the block tracking object's finalizer will automatically re-create and return + /// a new tracking object into the pool. This will only happen if there is a bug in the server, however it is necessary to avoid + /// leaving "dead zones" in the slab due to lost block tracking objects. + /// + /// The block to return. It must have been acquired by calling Lease on the same memory pool instance. public void Return(MemoryPoolBlock2 block) { block.Reset(); diff --git a/src/Microsoft.AspNet.Server.Kestrel/Http/MemoryPoolBlock2.cs b/src/Microsoft.AspNet.Server.Kestrel/Http/MemoryPoolBlock2.cs index 7d2f8d27d1..f2a47c35d9 100644 --- a/src/Microsoft.AspNet.Server.Kestrel/Http/MemoryPoolBlock2.cs +++ b/src/Microsoft.AspNet.Server.Kestrel/Http/MemoryPoolBlock2.cs @@ -8,33 +8,93 @@ using System.Text; namespace Microsoft.AspNet.Server.Kestrel.Http { + /// + /// Block tracking object used by the byte buffer memory pool. A slab is a large allocation which is divided into smaller blocks. The + /// individual blocks are then treated as independant array segments. + /// public class MemoryPoolBlock2 { - private static Vector _dotIndex = new Vector(Enumerable.Range(0, Vector.Count).Select(x => (byte)-x).ToArray()); + /// + /// Array of "minus one" bytes of the length of SIMD operations on the current hardware. Used as an argument in the + /// vector dot product that counts matching character occurence. + /// private static Vector _dotCount = new Vector(Byte.MaxValue); + /// + /// Array of negative numbers starting at 0 and continuing for the length of SIMD operations on the current hardware. + /// Used as an argument in the vector dot product that determines matching character index. + /// + private static Vector _dotIndex = new Vector(Enumerable.Range(0, Vector.Count).Select(x => (byte)-x).ToArray()); + + /// + /// If this block represents a one-time-use memory object, this GCHandle will hold that memory object at a fixed address + /// so it can be used in native operations. + /// private GCHandle _pinHandle; + + /// + /// Native address of the first byte of this block's Data memory. It is null for one-time-use memory, or copied from + /// the Slab's ArrayPtr for a slab-block segment. The byte it points to corresponds to Data.Array[0], and in practice you will always + /// use the _dataArrayPtr + Start or _dataArrayPtr + End, which point to the start of "active" bytes, or point to just after the "active" bytes. + /// private IntPtr _dataArrayPtr; + /// + /// The array segment describing the range of memory this block is tracking. The caller which has leased this block may only read and + /// modify the memory in this range. + /// public ArraySegment Data; + /// + /// This object cannot be instantiated outside of the static Create method + /// protected MemoryPoolBlock2() { } + /// + /// Back-reference to the memory pool which this block was allocated from. It may only be returned to this pool. + /// public MemoryPool2 Pool { get; private set; } + /// + /// Back-reference to the slab from which this block was taken, or null if it is one-time-use memory. + /// public MemoryPoolSlab2 Slab { get; private set; } + /// + /// Convenience accessor + /// public byte[] Array => Data.Array; + + /// + /// The Start represents the offset into Array where the range of "active" bytes begins. At the point when the block is leased + /// the Start is guaranteed to be equal to Array.Offset. The value of Start may be assigned anywhere between Data.Offset and + /// Data.Offset + Data.Count, and must be equal to or less than End. + /// public int Start { get; set; } + + /// + /// The End represents the offset into Array where the range of "active" bytes ends. At the point when the block is leased + /// the End is guaranteed to be equal to Array.Offset. The value of Start may be assigned anywhere between Data.Offset and + /// Data.Offset + Data.Count, and must be equal to or less than End. + /// public int End { get; set; } + + + /// + /// Reference to the next block of data when the overall "active" bytes spans multiple blocks. At the point when the block is + /// leased Next is guaranteed to be null. Start, End, and Next are used together in order to create a linked-list of discontiguous + /// working memory. The "active" memory is grown when bytes are copied in, End is increased, and Next is assigned. The "active" + /// memory is shrunk when bytes are consumed, Start is increased, and blocks are returned to the pool. + /// public MemoryPoolBlock2 Next { get; set; } ~MemoryPoolBlock2() { if (_pinHandle.IsAllocated) { + // if this is a one-time-use block, ensure that the GCHandle does not leak _pinHandle.Free(); } @@ -50,16 +110,23 @@ namespace Microsoft.AspNet.Server.Kestrel.Http } } + /// + /// Called to ensure that a block is pinned, and return the pointer to native memory just after + /// the range of "active" bytes. This is where arriving data is read into. + /// + /// public IntPtr Pin() { Debug.Assert(!_pinHandle.IsAllocated); if (_dataArrayPtr != IntPtr.Zero) { + // this is a slab managed block - use the native address of the slab which is always locked return _dataArrayPtr + End; } else { + // this is one-time-use memory - lock the managed memory until Unpin is called _pinHandle = GCHandle.Alloc(Data.Array, GCHandleType.Pinned); return _pinHandle.AddrOfPinnedObject() + End; } @@ -69,6 +136,7 @@ namespace Microsoft.AspNet.Server.Kestrel.Http { if (_dataArrayPtr == IntPtr.Zero) { + // this is one-time-use memory - unlock the managed memory Debug.Assert(_pinHandle.IsAllocated); _pinHandle.Free(); } @@ -100,6 +168,9 @@ namespace Microsoft.AspNet.Server.Kestrel.Http }; } + /// + /// called when the block is returned to the pool. mutable values are re-assigned to their guaranteed initialized state. + /// public void Reset() { Next = null; @@ -107,11 +178,19 @@ namespace Microsoft.AspNet.Server.Kestrel.Http End = Data.Offset; } + /// + /// ToString overridden for debugger convenience. This displays the "active" byte information in this block as ASCII characters. + /// + /// public override string ToString() { return Encoding.ASCII.GetString(Array, Start, End - Start); } + /// + /// acquires a cursor pointing into this block at the Start of "active" byte information + /// + /// public Iterator GetIterator() { return new Iterator(this); diff --git a/src/Microsoft.AspNet.Server.Kestrel/Http/MemoryPoolSlab2.cs b/src/Microsoft.AspNet.Server.Kestrel/Http/MemoryPoolSlab2.cs index 88b20c78b4..bf0221dda7 100644 --- a/src/Microsoft.AspNet.Server.Kestrel/Http/MemoryPoolSlab2.cs +++ b/src/Microsoft.AspNet.Server.Kestrel/Http/MemoryPoolSlab2.cs @@ -5,18 +5,51 @@ using System.Runtime.InteropServices; namespace Microsoft.AspNet.Server.Kestrel.Http { + /// + /// Slab tracking object used by the byte buffer memory pool. A slab is a large allocation which is divided into smaller blocks. The + /// individual blocks are then treated as independant array segments. + /// public class MemoryPoolSlab2 : IDisposable { + /// + /// This handle pins the managed array in memory until the slab is disposed. This prevents it from being + /// relocated and enables any subsections of the array to be used as native memory pointers to P/Invoked API calls. + /// private GCHandle _gcHandle; + + /// + /// The managed memory allocated in the large object heap. + /// public byte[] Array; + + /// + /// The native memory pointer of the pinned Array. All block native addresses are pointers into the memory + /// ranging from ArrayPtr to ArrayPtr + Array.Length + /// public IntPtr ArrayPtr; + + /// + /// True as long as the blocks from this slab are to be considered returnable to the pool. In order to shrink the + /// memory pool size an entire slab must be removed. That is done by (1) setting IsActive to false and removing the + /// slab from the pool's _slabs collection, (2) as each block currently in use is Return()ed to the pool it will + /// be allowed to be garbage collected rather than re-pooled, and (3) when all block tracking objects are garbage + /// collected and the slab is no longer references the slab will be garbage collected and the memory unpinned will + /// be unpinned by the slab's Dispose. + /// public bool IsActive; + + /// + /// Part of the IDisposable implementation + /// private bool disposedValue = false; // To detect redundant calls public static MemoryPoolSlab2 Create(int length) { + // allocate and pin requested memory length var array = new byte[length]; var gcHandle = GCHandle.Alloc(array, GCHandleType.Pinned); + + // allocate and return slab tracking object return new MemoryPoolSlab2 { Array = array,