diff --git a/NuGetPackageVerifier.json b/NuGetPackageVerifier.json index b153ab15..8e89b63f 100644 --- a/NuGetPackageVerifier.json +++ b/NuGetPackageVerifier.json @@ -1,7 +1,14 @@ { - "Default": { - "rules": [ - "DefaultCompositeRule" - ] + "nonshipping": { + "rules": [], + "packages": { + "Microsoft.Extensions.Buffers.MemoryPool.Sources": {}, + "Microsoft.Extensions.Buffers.Testing.Sources": {} } -} \ No newline at end of file + }, + "Default": { + "rules": [ + "DefaultCompositeRule" + ] + } +} diff --git a/build/dependencies.props b/build/dependencies.props index 7cde60bf..d3ae9f2d 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -19,9 +19,6 @@ 2.2.0-preview1-34755 2.2.0-preview1-34755 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 - 2.2.0-preview1-34755 2.2.0-preview1-34755 2.2.0-preview1-34755 2.2.0-preview1-34755 diff --git a/shared/Microsoft.Extensions.Buffers.MemoryPool.Sources/DiagnosticMemoryPool.cs b/shared/Microsoft.Extensions.Buffers.MemoryPool.Sources/DiagnosticMemoryPool.cs new file mode 100644 index 00000000..3d2c0d72 --- /dev/null +++ b/shared/Microsoft.Extensions.Buffers.MemoryPool.Sources/DiagnosticMemoryPool.cs @@ -0,0 +1,165 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace System.Buffers +{ + /// + /// Used to allocate and distribute re-usable blocks of memory. + /// + internal class DiagnosticMemoryPool : MemoryPool + { + private readonly MemoryPool _pool; + + private readonly bool _allowLateReturn; + + private readonly bool _rentTracking; + + private readonly object _syncObj; + + private readonly HashSet _blocks; + + private readonly List _blockAccessExceptions; + + private readonly TaskCompletionSource _allBlocksRetuned; + + private int _totalBlocks; + + /// + /// This default value passed in to Rent to use the default value for the pool. + /// + private const int AnySize = -1; + + public DiagnosticMemoryPool(MemoryPool pool, bool allowLateReturn = false, bool rentTracking = false) + { + _pool = pool; + _allowLateReturn = allowLateReturn; + _rentTracking = rentTracking; + _blocks = new HashSet(); + _syncObj = new object(); + _allBlocksRetuned = new TaskCompletionSource(); + _blockAccessExceptions = new List(); + } + + public bool IsDisposed { get; private set; } + + public override IMemoryOwner Rent(int size = AnySize) + { + lock (_syncObj) + { + if (IsDisposed) + { + MemoryPoolThrowHelper.ThrowObjectDisposedException(MemoryPoolThrowHelper.ExceptionArgument.MemoryPool); + } + + var diagnosticPoolBlock = new DiagnosticPoolBlock(this, _pool.Rent(size)); + if (_rentTracking) + { + diagnosticPoolBlock.Track(); + } + _totalBlocks++; + _blocks.Add(diagnosticPoolBlock); + return diagnosticPoolBlock; + } + } + + public override int MaxBufferSize => _pool.MaxBufferSize; + + internal void Return(DiagnosticPoolBlock block) + { + bool returnedAllBlocks; + lock (_syncObj) + { + _blocks.Remove(block); + returnedAllBlocks = _blocks.Count == 0; + } + + if (IsDisposed) + { + if (!_allowLateReturn) + { + MemoryPoolThrowHelper.ThrowInvalidOperationException_BlockReturnedToDisposedPool(block); + } + + if (returnedAllBlocks) + { + SetAllBlocksReturned(); + } + } + + } + + internal void ReportException(Exception exception) + { + lock (_syncObj) + { + _blockAccessExceptions.Add(exception); + } + } + + protected override void Dispose(bool disposing) + { + if (IsDisposed) + { + MemoryPoolThrowHelper.ThrowInvalidOperationException_DoubleDispose(); + } + + bool allBlocksReturned = false; + try + { + lock (_syncObj) + { + IsDisposed = true; + allBlocksReturned = _blocks.Count == 0; + if (!allBlocksReturned && !_allowLateReturn) + { + MemoryPoolThrowHelper.ThrowInvalidOperationException_DisposingPoolWithActiveBlocks(_totalBlocks - _blocks.Count, _totalBlocks, _blocks.ToArray()); + } + + if (_blockAccessExceptions.Any()) + { + throw CreateAccessExceptions(); + } + } + } + finally + { + if (allBlocksReturned) + { + SetAllBlocksReturned(); + } + } + } + + private void SetAllBlocksReturned() + { + if (_blockAccessExceptions.Any()) + { + _allBlocksRetuned.SetException(CreateAccessExceptions()); + } + else + { + _allBlocksRetuned.SetResult(null); + } + } + + private AggregateException CreateAccessExceptions() + { + return new AggregateException("Exceptions occurred while accessing blocks", _blockAccessExceptions.ToArray()); + } + + public async Task WhenAllBlocksReturnedAsync(TimeSpan timeout) + { + var task = await Task.WhenAny(_allBlocksRetuned.Task, Task.Delay(timeout)); + if (task != _allBlocksRetuned.Task) + { + MemoryPoolThrowHelper.ThrowInvalidOperationException_BlocksWereNotReturnedInTime(_totalBlocks - _blocks.Count, _totalBlocks, _blocks.ToArray()); + } + + await task; + } + } +} diff --git a/shared/Microsoft.Extensions.Buffers.MemoryPool.Sources/DiagnosticPoolBlock.cs b/shared/Microsoft.Extensions.Buffers.MemoryPool.Sources/DiagnosticPoolBlock.cs new file mode 100644 index 00000000..3d4c5f87 --- /dev/null +++ b/shared/Microsoft.Extensions.Buffers.MemoryPool.Sources/DiagnosticPoolBlock.cs @@ -0,0 +1,224 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Threading; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace System.Buffers +{ + /// + /// 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 independent array segments. + /// + internal sealed class DiagnosticPoolBlock : MemoryManager + { + /// + /// Back-reference to the memory pool which this block was allocated from. It may only be returned to this pool. + /// + private readonly DiagnosticMemoryPool _pool; + + private readonly IMemoryOwner _memoryOwner; + private MemoryHandle? _memoryHandle; + private Memory _memory; + + private readonly object _syncObj = new object(); + private bool _isDisposed; + private int _pinCount; + + + /// + /// This object cannot be instantiated outside of the static Create method + /// + internal DiagnosticPoolBlock(DiagnosticMemoryPool pool, IMemoryOwner memoryOwner) + { + _pool = pool; + _memoryOwner = memoryOwner; + _memory = memoryOwner.Memory; + } + + public override Memory Memory + { + get + { + try + { + lock (_syncObj) + { + if (_isDisposed) + { + MemoryPoolThrowHelper.ThrowObjectDisposedException(MemoryPoolThrowHelper.ExceptionArgument.MemoryPoolBlock); + } + + if (_pool.IsDisposed) + { + MemoryPoolThrowHelper.ThrowInvalidOperationException_BlockIsBackedByDisposedSlab(this); + } + + return CreateMemory(_memory.Length); + } + } + catch (Exception exception) + { + _pool.ReportException(exception); + throw; + } + } + } + + protected override void Dispose(bool disposing) + { + try + { + lock (_syncObj) + { + if (Volatile.Read(ref _pinCount) > 0) + { + MemoryPoolThrowHelper.ThrowInvalidOperationException_ReturningPinnedBlock(this); + } + + if (_isDisposed) + { + MemoryPoolThrowHelper.ThrowInvalidOperationException_BlockDoubleDispose(this); + } + + _memoryOwner.Dispose(); + + _pool.Return(this); + + _isDisposed = true; + } + } + catch (Exception exception) + { + _pool.ReportException(exception); + throw; + } + } + + public override Span GetSpan() + { + try + { + lock (_syncObj) + { + if (_isDisposed) + { + MemoryPoolThrowHelper.ThrowObjectDisposedException(MemoryPoolThrowHelper.ExceptionArgument.MemoryPoolBlock); + } + + if (_pool.IsDisposed) + { + MemoryPoolThrowHelper.ThrowInvalidOperationException_BlockIsBackedByDisposedSlab(this); + } + + return _memory.Span; + } + } + catch (Exception exception) + { + _pool.ReportException(exception); + throw; + } + } + + public override MemoryHandle Pin(int byteOffset = 0) + { + try + { + lock (_syncObj) + { + if (_isDisposed) + { + MemoryPoolThrowHelper.ThrowObjectDisposedException(MemoryPoolThrowHelper.ExceptionArgument.MemoryPoolBlock); + } + + if (_pool.IsDisposed) + { + MemoryPoolThrowHelper.ThrowInvalidOperationException_BlockIsBackedByDisposedSlab(this); + } + + if (byteOffset < 0 || byteOffset > _memory.Length) + { + MemoryPoolThrowHelper.ThrowArgumentOutOfRangeException(_memory.Length, byteOffset); + } + + _pinCount++; + + _memoryHandle = _memoryHandle ?? _memory.Pin(); + + unsafe + { + return new MemoryHandle(((IntPtr)_memoryHandle.Value.Pointer + byteOffset).ToPointer(), default, this); + } + } + } + catch (Exception exception) + { + _pool.ReportException(exception); + throw; + } + } + + protected override bool TryGetArray(out ArraySegment segment) + { + try + { + lock (_syncObj) + { + if (_isDisposed) + { + MemoryPoolThrowHelper.ThrowObjectDisposedException(MemoryPoolThrowHelper.ExceptionArgument.MemoryPoolBlock); + } + + if (_pool.IsDisposed) + { + MemoryPoolThrowHelper.ThrowInvalidOperationException_BlockIsBackedByDisposedSlab(this); + } + + return MemoryMarshal.TryGetArray(_memory, out segment); + } + } + catch (Exception exception) + { + _pool.ReportException(exception); + throw; + } + } + + public override void Unpin() + { + try + { + lock (_syncObj) + { + if (_pinCount == 0) + { + MemoryPoolThrowHelper.ThrowInvalidOperationException_PinCountZero(this); + } + + _pinCount--; + + if (_pinCount == 0) + { + Debug.Assert(_memoryHandle.HasValue); + _memoryHandle.Value.Dispose(); + _memoryHandle = null; + } + } + } + catch (Exception exception) + { + _pool.ReportException(exception); + throw; + } + } + + public StackTrace Leaser { get; set; } + + public void Track() + { + Leaser = new StackTrace(false); + } + } +} diff --git a/shared/Microsoft.Extensions.Buffers.MemoryPool.Sources/MemoryPoolBlock.cs b/shared/Microsoft.Extensions.Buffers.MemoryPool.Sources/MemoryPoolBlock.cs new file mode 100644 index 00000000..be442d06 --- /dev/null +++ b/shared/Microsoft.Extensions.Buffers.MemoryPool.Sources/MemoryPoolBlock.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; + +namespace System.Buffers +{ + /// + /// 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 independent array segments. + /// + internal sealed class MemoryPoolBlock : IMemoryOwner + { + private readonly int _offset; + private readonly int _length; + + /// + /// This object cannot be instantiated outside of the static Create method + /// + internal MemoryPoolBlock(SlabMemoryPool pool, MemoryPoolSlab slab, int offset, int length) + { + _offset = offset; + _length = length; + + Pool = pool; + Slab = slab; + + Memory = MemoryMarshal.CreateFromPinnedArray(slab.Array, _offset, _length); + } + + /// + /// Back-reference to the memory pool which this block was allocated from. It may only be returned to this pool. + /// + public SlabMemoryPool Pool { get; } + + /// + /// Back-reference to the slab from which this block was taken, or null if it is one-time-use memory. + /// + public MemoryPoolSlab Slab { get; } + + public Memory Memory { get; } + + ~MemoryPoolBlock() + { + if (Slab != null && Slab.IsActive) + { + // Need to make a new object because this one is being finalized + Pool.Return(new MemoryPoolBlock(Pool, Slab, _offset, _length)); + } + } + + public void Dispose() + { + Pool.Return(this); + } + + public void Lease() + { + } + } +} \ No newline at end of file diff --git a/shared/Microsoft.Extensions.Buffers.MemoryPool.Sources/MemoryPoolSlab.cs b/shared/Microsoft.Extensions.Buffers.MemoryPool.Sources/MemoryPoolSlab.cs new file mode 100644 index 00000000..d4e5d5ab --- /dev/null +++ b/shared/Microsoft.Extensions.Buffers.MemoryPool.Sources/MemoryPoolSlab.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; + +namespace System.Buffers +{ + /// + /// 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. + /// + internal class MemoryPoolSlab : 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; + private bool _isDisposed; + + public MemoryPoolSlab(byte[] data) + { + Array = data; + _gcHandle = GCHandle.Alloc(data, GCHandleType.Pinned); + NativePointer = _gcHandle.AddrOfPinnedObject(); + } + + /// + /// 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 => !_isDisposed; + + public IntPtr NativePointer { get; private set; } + + public byte[] Array { get; private set; } + + public static MemoryPoolSlab Create(int length) + { + // allocate and pin requested memory length + var array = new byte[length]; + + // allocate and return slab tracking object + return new MemoryPoolSlab(array); + } + + protected void Dispose(bool disposing) + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + + Array = null; + NativePointer = IntPtr.Zero;; + + if (_gcHandle.IsAllocated) + { + _gcHandle.Free(); + } + } + + ~MemoryPoolSlab() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} diff --git a/shared/Microsoft.Extensions.Buffers.MemoryPool.Sources/MemoryPoolThrowHelper.cs b/shared/Microsoft.Extensions.Buffers.MemoryPool.Sources/MemoryPoolThrowHelper.cs new file mode 100644 index 00000000..104d23e7 --- /dev/null +++ b/shared/Microsoft.Extensions.Buffers.MemoryPool.Sources/MemoryPoolThrowHelper.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Text; + +namespace System.Buffers +{ + internal class MemoryPoolThrowHelper + { + public static void ThrowArgumentOutOfRangeException(int sourceLength, int offset) + { + throw GetArgumentOutOfRangeException(sourceLength, offset); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static ArgumentOutOfRangeException GetArgumentOutOfRangeException(int sourceLength, int offset) + { + if ((uint)offset > (uint)sourceLength) + { + // Offset is negative or less than array length + return new ArgumentOutOfRangeException(GetArgumentName(ExceptionArgument.offset)); + } + + // The third parameter (not passed) length must be out of range + return new ArgumentOutOfRangeException(GetArgumentName(ExceptionArgument.length)); + } + + public static void ThrowInvalidOperationException_PinCountZero(DiagnosticPoolBlock block) + { + throw new InvalidOperationException(GenerateMessage("Can't unpin, pin count is zero", block)); + } + + public static void ThrowInvalidOperationException_ReturningPinnedBlock(DiagnosticPoolBlock block) + { + throw new InvalidOperationException(GenerateMessage("Disposing pinned block", block)); + } + + public static void ThrowInvalidOperationException_DoubleDispose() + { + throw new InvalidOperationException("Object is being disposed twice"); + } + + public static void ThrowInvalidOperationException_BlockDoubleDispose(DiagnosticPoolBlock block) + { + throw new InvalidOperationException("Block is being disposed twice"); + } + + public static void ThrowInvalidOperationException_BlockReturnedToDisposedPool(DiagnosticPoolBlock block) + { + throw new InvalidOperationException(GenerateMessage("Block is being returned to disposed pool", block)); + } + + public static void ThrowInvalidOperationException_BlockIsBackedByDisposedSlab(DiagnosticPoolBlock block) + { + throw new InvalidOperationException(GenerateMessage("Block is backed by disposed slab", block)); + } + + public static void ThrowInvalidOperationException_DisposingPoolWithActiveBlocks(int returned, int total, DiagnosticPoolBlock[] blocks) + { + throw new InvalidOperationException(GenerateMessage($"Memory pool with active blocks is being disposed, {returned} of {total} returned", blocks)); + } + + public static void ThrowInvalidOperationException_BlocksWereNotReturnedInTime(int returned, int total, DiagnosticPoolBlock[] blocks) + { + throw new InvalidOperationException(GenerateMessage($"Blocks were not returned in time, {returned} of {total} returned ", blocks)); + } + + private static string GenerateMessage(string message, params DiagnosticPoolBlock[] blocks) + { + StringBuilder builder = new StringBuilder(message); + foreach (var diagnosticPoolBlock in blocks) + { + if (diagnosticPoolBlock.Leaser != null) + { + builder.AppendLine(); + + builder.AppendLine("Block leased from:"); + builder.AppendLine(diagnosticPoolBlock.Leaser.ToString()); + } + } + + return builder.ToString(); + } + + public static void ThrowArgumentOutOfRangeException_BufferRequestTooLarge(int maxSize) + { + throw GetArgumentOutOfRangeException_BufferRequestTooLarge(maxSize); + } + + public static void ThrowObjectDisposedException(ExceptionArgument argument) + { + throw GetObjectDisposedException(argument); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static ArgumentOutOfRangeException GetArgumentOutOfRangeException_BufferRequestTooLarge(int maxSize) + { + return new ArgumentOutOfRangeException(GetArgumentName(ExceptionArgument.size), $"Cannot allocate more than {maxSize} bytes in a single buffer"); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static ObjectDisposedException GetObjectDisposedException(ExceptionArgument argument) + { + return new ObjectDisposedException(GetArgumentName(argument)); + } + + private static string GetArgumentName(ExceptionArgument argument) + { + Debug.Assert(Enum.IsDefined(typeof(ExceptionArgument), argument), "The enum value is not defined, please check the ExceptionArgument Enum."); + + return argument.ToString(); + } + + internal enum ExceptionArgument + { + size, + offset, + length, + MemoryPoolBlock, + MemoryPool + } + } +} diff --git a/shared/Microsoft.Extensions.Buffers.MemoryPool.Sources/SlabMemoryPool.cs b/shared/Microsoft.Extensions.Buffers.MemoryPool.Sources/SlabMemoryPool.cs new file mode 100644 index 00000000..205beeef --- /dev/null +++ b/shared/Microsoft.Extensions.Buffers.MemoryPool.Sources/SlabMemoryPool.cs @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Threading; + +namespace System.Buffers +{ + /// + /// Used to allocate and distribute re-usable blocks of memory. + /// + internal class SlabMemoryPool : MemoryPool + { + /// + /// The size of a block. 4096 is chosen because most operating systems use 4k pages. + /// + private const int _blockSize = 4096; + + /// + /// 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; + + /// + /// Max allocation block size for pooled blocks, + /// larger values can be leased but they will be disposed after use rather than returned to the pool. + /// + public override int MaxBufferSize { get; } = _blockSize; + + /// + /// 4096 * 32 gives you a slabLength of 128k contiguous bytes allocated per slab + /// + private static readonly int _slabLength = _blockSize * _blockCount; + + /// + /// 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 ConcurrentQueue _blocks = new ConcurrentQueue(); + + /// + /// 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 _isDisposed; // To detect redundant calls + + private int _totalAllocatedBlocks; + + private readonly object _disposeSync = new object(); + + /// + /// This default value passed in to Rent to use the default value for the pool. + /// + private const int AnySize = -1; + + public override IMemoryOwner Rent(int size = AnySize) + { + if (size > _blockSize) + { + MemoryPoolThrowHelper.ThrowArgumentOutOfRangeException_BufferRequestTooLarge(_blockSize); + } + + var block = Lease(); + return block; + } + + /// + /// Called to take a block from the pool. + /// + /// The block that is reserved for the called. It must be passed to Return when it is no longer being used. + private MemoryPoolBlock Lease() + { + if (_isDisposed) + { + MemoryPoolThrowHelper.ThrowObjectDisposedException(MemoryPoolThrowHelper.ExceptionArgument.MemoryPool); + } + + if (_blocks.TryDequeue(out MemoryPoolBlock block)) + { + // block successfully taken from the stack - return it + + block.Lease(); + return block; + } + // no blocks available - grow the pool + block = AllocateSlab(); + block.Lease(); + return block; + } + + /// + /// 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 MemoryPoolBlock AllocateSlab() + { + var slab = MemoryPoolSlab.Create(_slabLength); + _slabs.Push(slab); + + var basePtr = slab.NativePointer; + // Page align the blocks + var offset = (int)((((ulong)basePtr + (uint)_blockSize - 1) & ~((uint)_blockSize - 1)) - (ulong)basePtr); + // Ensure page aligned + Debug.Assert(((ulong)basePtr + (uint)offset) % _blockSize == 0); + + var blockCount = (_slabLength - offset) / _blockSize; + Interlocked.Add(ref _totalAllocatedBlocks, blockCount); + + MemoryPoolBlock block = null; + + for (int i = 0; i < blockCount; i++) + { + block = new MemoryPoolBlock(this, slab, offset, _blockSize); + + if (i != blockCount - 1) // last block + { +#if BLOCK_LEASE_TRACKING + block.IsLeased = true; +#endif + Return(block); + } + + offset += _blockSize; + } + + return block; + } + + /// + /// 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. + internal void Return(MemoryPoolBlock block) + { +#if BLOCK_LEASE_TRACKING + Debug.Assert(block.Pool == this, "Returned block was not leased from this pool"); + Debug.Assert(block.IsLeased, $"Block being returned to pool twice: {block.Leaser}{Environment.NewLine}"); + block.IsLeased = false; +#endif + + if (!_isDisposed) + { + _blocks.Enqueue(block); + } + else + { + GC.SuppressFinalize(block); + } + } + + protected override void Dispose(bool disposing) + { + if (_isDisposed) + { + return; + } + + lock (_disposeSync) + { + _isDisposed = true; + + if (disposing) + { + while (_slabs.TryPop(out MemoryPoolSlab slab)) + { + // dispose managed state (managed objects). + slab.Dispose(); + } + } + + // Discard blocks in pool + while (_blocks.TryDequeue(out MemoryPoolBlock block)) + { + GC.SuppressFinalize(block); + } + } + } + } +} diff --git a/shared/Microsoft.Extensions.Buffers.Testing.Sources/BufferSegment.cs b/shared/Microsoft.Extensions.Buffers.Testing.Sources/BufferSegment.cs new file mode 100644 index 00000000..d89f4add --- /dev/null +++ b/shared/Microsoft.Extensions.Buffers.Testing.Sources/BufferSegment.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Buffers +{ + internal class BufferSegment : ReadOnlySequenceSegment + { + public BufferSegment(Memory memory) + { + Memory = memory; + } + + public BufferSegment Append(Memory memory) + { + var segment = new BufferSegment(memory) + { + RunningIndex = RunningIndex + Memory.Length + }; + Next = segment; + return segment; + } + } +} diff --git a/shared/Microsoft.Extensions.Buffers.Testing.Sources/CustomMemoryForTest.cs b/shared/Microsoft.Extensions.Buffers.Testing.Sources/CustomMemoryForTest.cs new file mode 100644 index 00000000..20dab206 --- /dev/null +++ b/shared/Microsoft.Extensions.Buffers.Testing.Sources/CustomMemoryForTest.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Buffers +{ + internal class CustomMemoryForTest : IMemoryOwner + { + private bool _disposed; + private T[] _array; + private readonly int _offset; + private readonly int _length; + + public CustomMemoryForTest(T[] array): this(array, 0, array.Length) + { + } + + public CustomMemoryForTest(T[] array, int offset, int length) + { + _array = array; + _offset = offset; + _length = length; + } + + public Memory Memory + { + get + { + if (_disposed) + throw new ObjectDisposedException(nameof(CustomMemoryForTest)); + return new Memory(_array, _offset, _length); + } + } + + public void Dispose() + { + if (_disposed) + return; + + _array = null; + _disposed = true; + } + } +} + diff --git a/shared/Microsoft.Extensions.Buffers.Testing.Sources/ReadOnlySequenceFactory.cs b/shared/Microsoft.Extensions.Buffers.Testing.Sources/ReadOnlySequenceFactory.cs new file mode 100644 index 00000000..0fc0c658 --- /dev/null +++ b/shared/Microsoft.Extensions.Buffers.Testing.Sources/ReadOnlySequenceFactory.cs @@ -0,0 +1,148 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Text; + +namespace System.Buffers +{ + internal abstract class ReadOnlySequenceFactory + { + public static ReadOnlySequenceFactory ArrayFactory { get; } = new ArrayTestSequenceFactory(); + public static ReadOnlySequenceFactory MemoryFactory { get; } = new MemoryTestSequenceFactory(); + public static ReadOnlySequenceFactory OwnedMemoryFactory { get; } = new OwnedMemoryTestSequenceFactory(); + public static ReadOnlySequenceFactory SingleSegmentFactory { get; } = new SingleSegmentTestSequenceFactory(); + public static ReadOnlySequenceFactory SegmentPerByteFactory { get; } = new BytePerSegmentTestSequenceFactory(); + + public abstract ReadOnlySequence CreateOfSize(int size); + public abstract ReadOnlySequence CreateWithContent(byte[] data); + + public ReadOnlySequence CreateWithContent(string data) + { + return CreateWithContent(Encoding.ASCII.GetBytes(data)); + } + + internal class ArrayTestSequenceFactory : ReadOnlySequenceFactory + { + public override ReadOnlySequence CreateOfSize(int size) + { + return new ReadOnlySequence(new byte[size + 20], 10, size); + } + + public override ReadOnlySequence CreateWithContent(byte[] data) + { + var startSegment = new byte[data.Length + 20]; + Array.Copy(data, 0, startSegment, 10, data.Length); + return new ReadOnlySequence(startSegment, 10, data.Length); + } + } + + internal class MemoryTestSequenceFactory : ReadOnlySequenceFactory + { + public override ReadOnlySequence CreateOfSize(int size) + { + return CreateWithContent(new byte[size]); + } + + public override ReadOnlySequence CreateWithContent(byte[] data) + { + var startSegment = new byte[data.Length + 20]; + Array.Copy(data, 0, startSegment, 10, data.Length); + return new ReadOnlySequence(new Memory(startSegment, 10, data.Length)); + } + } + + internal class OwnedMemoryTestSequenceFactory : ReadOnlySequenceFactory + { + public override ReadOnlySequence CreateOfSize(int size) + { + return CreateWithContent(new byte[size]); + } + + public override ReadOnlySequence CreateWithContent(byte[] data) + { + var startSegment = new byte[data.Length + 20]; + Array.Copy(data, 0, startSegment, 10, data.Length); + return new ReadOnlySequence(new CustomMemoryForTest(startSegment, 10, data.Length).Memory); + } + } + + internal class SingleSegmentTestSequenceFactory : ReadOnlySequenceFactory + { + public override ReadOnlySequence CreateOfSize(int size) + { + return CreateWithContent(new byte[size]); + } + + public override ReadOnlySequence CreateWithContent(byte[] data) + { + return CreateSegments(data); + } + } + + internal class BytePerSegmentTestSequenceFactory : ReadOnlySequenceFactory + { + public override ReadOnlySequence CreateOfSize(int size) + { + return CreateWithContent(new byte[size]); + } + + public override ReadOnlySequence CreateWithContent(byte[] data) + { + var segments = new List(); + + segments.Add(Array.Empty()); + foreach (var b in data) + { + segments.Add(new[] { b }); + segments.Add(Array.Empty()); + } + + return CreateSegments(segments.ToArray()); + } + } + + public static ReadOnlySequence CreateSegments(params byte[][] inputs) + { + if (inputs == null || inputs.Length == 0) + { + throw new InvalidOperationException(); + } + + int i = 0; + + BufferSegment last = null; + BufferSegment first = null; + + do + { + byte[] s = inputs[i]; + int length = s.Length; + int dataOffset = length; + var chars = new byte[length * 2]; + + for (int j = 0; j < length; j++) + { + chars[dataOffset + j] = s[j]; + } + + // Create a segment that has offset relative to the OwnedMemory and OwnedMemory itself has offset relative to array + var memory = new Memory(chars).Slice(length, length); + + if (first == null) + { + first = new BufferSegment(memory); + last = first; + } + else + { + last = last.Append(memory); + } + i++; + } while (i < inputs.Length); + + return new ReadOnlySequence(first, 0, last, last.Memory.Length); + } + } +} diff --git a/src/Kestrel.Core/Internal/BufferReader.cs b/src/Kestrel.Core/Internal/BufferReader.cs new file mode 100644 index 00000000..c9688984 --- /dev/null +++ b/src/Kestrel.Core/Internal/BufferReader.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; + +namespace System.Buffers +{ + internal ref struct BufferReader + { + private ReadOnlySpan _currentSpan; + private int _index; + + private ReadOnlySequence _sequence; + private SequencePosition _currentSequencePosition; + private SequencePosition _nextSequencePosition; + + private int _consumedBytes; + private bool _end; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public BufferReader(in ReadOnlySequence buffer) + { + _index = 0; + _consumedBytes = 0; + _sequence = buffer; + _currentSequencePosition = _sequence.Start; + _nextSequencePosition = _currentSequencePosition; + + if (_sequence.TryGet(ref _nextSequencePosition, out var memory, true)) + { + _end = false; + _currentSpan = memory.Span; + if (_currentSpan.Length == 0) + { + // No space in first span, move to one with space + MoveNext(); + } + } + else + { + // No space in any spans and at end of sequence + _end = true; + _currentSpan = default; + } + } + + public bool End => _end; + + public int CurrentSegmentIndex => _index; + + public SequencePosition Position => _sequence.GetPosition(_index, _currentSequencePosition); + + public ReadOnlySpan CurrentSegment => _currentSpan; + + public ReadOnlySpan UnreadSegment => _currentSpan.Slice(_index); + + public int ConsumedBytes => _consumedBytes; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int Peek() + { + if (_end) + { + return -1; + } + return _currentSpan[_index]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int Read() + { + if (_end) + { + return -1; + } + + var value = _currentSpan[_index]; + _index++; + _consumedBytes++; + + if (_index >= _currentSpan.Length) + { + MoveNext(); + } + + return value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void MoveNext() + { + var previous = _nextSequencePosition; + while (_sequence.TryGet(ref _nextSequencePosition, out var memory, true)) + { + _currentSequencePosition = previous; + _currentSpan = memory.Span; + _index = 0; + if (_currentSpan.Length > 0) + { + return; + } + } + _end = true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Advance(int byteCount) + { + if (!_end && byteCount > 0 && (_index + byteCount) < _currentSpan.Length) + { + _consumedBytes += byteCount; + _index += byteCount; + } + else + { + AdvanceNext(byteCount); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void AdvanceNext(int byteCount) + { + if (byteCount < 0) + { + BuffersThrowHelper.ThrowArgumentOutOfRangeException(BuffersThrowHelper.ExceptionArgument.length); + } + + _consumedBytes += byteCount; + + while (!_end && byteCount > 0) + { + if ((_index + byteCount) < _currentSpan.Length) + { + _index += byteCount; + byteCount = 0; + break; + } + + var remaining = (_currentSpan.Length - _index); + + _index += remaining; + byteCount -= remaining; + + MoveNext(); + } + + if (byteCount > 0) + { + BuffersThrowHelper.ThrowArgumentOutOfRangeException(BuffersThrowHelper.ExceptionArgument.length); + } + } + } +} diff --git a/src/Kestrel.Core/Internal/Http/CountingBufferWriter.cs b/src/Kestrel.Core/Internal/BufferWriter.cs similarity index 90% rename from src/Kestrel.Core/Internal/Http/CountingBufferWriter.cs rename to src/Kestrel.Core/Internal/BufferWriter.cs index e3299c60..1f33f3e4 100644 --- a/src/Kestrel.Core/Internal/Http/CountingBufferWriter.cs +++ b/src/Kestrel.Core/Internal/BufferWriter.cs @@ -1,4 +1,3 @@ - // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. @@ -6,16 +5,14 @@ using System.Runtime.CompilerServices; namespace System.Buffers { - // TODO: Once this is public, update the actual CountingBufferWriter in the Common repo, - // and go back to using that. - internal ref struct CountingBufferWriter where T: IBufferWriter + internal ref struct BufferWriter where T : IBufferWriter { private T _output; private Span _span; private int _buffered; private long _bytesCommitted; - public CountingBufferWriter(T output) + public BufferWriter(T output) { _buffered = 0; _bytesCommitted = 0; diff --git a/src/Kestrel.Core/Internal/Http/ChunkWriter.cs b/src/Kestrel.Core/Internal/Http/ChunkWriter.cs index 2184937b..3d8cc456 100644 --- a/src/Kestrel.Core/Internal/Http/ChunkWriter.cs +++ b/src/Kestrel.Core/Internal/Http/ChunkWriter.cs @@ -48,14 +48,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http return new ArraySegment(bytes, offset, 10 - offset); } - internal static int WriteBeginChunkBytes(ref CountingBufferWriter start, int dataCount) + internal static int WriteBeginChunkBytes(ref BufferWriter start, int dataCount) { var chunkSegment = BeginChunkBytes(dataCount); start.Write(new ReadOnlySpan(chunkSegment.Array, chunkSegment.Offset, chunkSegment.Count)); return chunkSegment.Count; } - internal static void WriteEndChunkBytes(ref CountingBufferWriter start) + internal static void WriteEndChunkBytes(ref BufferWriter start) { start.Write(new ReadOnlySpan(_endChunkBytes.Array, _endChunkBytes.Offset, _endChunkBytes.Count)); } diff --git a/src/Kestrel.Core/Internal/Http/Http1OutputProducer.cs b/src/Kestrel.Core/Internal/Http/Http1OutputProducer.cs index ce04650a..1337b6b3 100644 --- a/src/Kestrel.Core/Internal/Http/Http1OutputProducer.cs +++ b/src/Kestrel.Core/Internal/Http/Http1OutputProducer.cs @@ -117,7 +117,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } var buffer = _pipeWriter; - var writer = new CountingBufferWriter(buffer); + var writer = new BufferWriter(buffer); writer.Write(_bytesHttpVersion11); var statusBytes = ReasonPhrases.ToStatusBytes(statusCode, reasonPhrase); @@ -196,7 +196,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http return Task.CompletedTask; } - var writer = new CountingBufferWriter(_pipeWriter); + var writer = new BufferWriter(_pipeWriter); if (buffer.Length > 0) { writer.Write(buffer); diff --git a/src/Kestrel.Core/Internal/Http/HttpHeaders.Generated.cs b/src/Kestrel.Core/Internal/Http/HttpHeaders.Generated.cs index 182e90aa..daee31b4 100644 --- a/src/Kestrel.Core/Internal/Http/HttpHeaders.Generated.cs +++ b/src/Kestrel.Core/Internal/Http/HttpHeaders.Generated.cs @@ -7765,7 +7765,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http return true; } - internal void CopyToFast(ref CountingBufferWriter output) + internal void CopyToFast(ref BufferWriter output) { var tempBits = _bits | (_contentLength.HasValue ? -9223372036854775808L : 0); diff --git a/src/Kestrel.Core/Internal/Http/HttpProtocol.cs b/src/Kestrel.Core/Internal/Http/HttpProtocol.cs index 5518c0be..ccd409ce 100644 --- a/src/Kestrel.Core/Internal/Http/HttpProtocol.cs +++ b/src/Kestrel.Core/Internal/Http/HttpProtocol.cs @@ -936,7 +936,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http var bytesWritten = 0L; if (buffer.Length > 0) { - var writer = new CountingBufferWriter(writableBuffer); + var writer = new BufferWriter(writableBuffer); ChunkWriter.WriteBeginChunkBytes(ref writer, buffer.Length); writer.Write(buffer.Span); diff --git a/src/Kestrel.Core/Internal/Http/HttpResponseHeaders.cs b/src/Kestrel.Core/Internal/Http/HttpResponseHeaders.cs index 41a27c4a..5f9c4b36 100644 --- a/src/Kestrel.Core/Internal/Http/HttpResponseHeaders.cs +++ b/src/Kestrel.Core/Internal/Http/HttpResponseHeaders.cs @@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http return GetEnumerator(); } - internal void CopyTo(ref CountingBufferWriter buffer) + internal void CopyTo(ref BufferWriter buffer) { CopyToFast(ref buffer); if (MaybeUnknown != null) diff --git a/src/Kestrel.Core/Internal/Http/PipelineExtensions.cs b/src/Kestrel.Core/Internal/Http/PipelineExtensions.cs index fce822b6..e56c43b2 100644 --- a/src/Kestrel.Core/Internal/Http/PipelineExtensions.cs +++ b/src/Kestrel.Core/Internal/Http/PipelineExtensions.cs @@ -40,7 +40,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http return result; } - internal static unsafe void WriteAsciiNoValidation(ref this CountingBufferWriter buffer, string data) + internal static unsafe void WriteAsciiNoValidation(ref this BufferWriter buffer, string data) { if (string.IsNullOrEmpty(data)) { @@ -69,7 +69,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static unsafe void WriteNumeric(ref this CountingBufferWriter buffer, ulong number) + internal static unsafe void WriteNumeric(ref this BufferWriter buffer, ulong number) { const byte AsciiDigitStart = (byte)'0'; @@ -119,7 +119,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } [MethodImpl(MethodImplOptions.NoInlining)] - private static void WriteNumericMultiWrite(ref this CountingBufferWriter buffer, ulong number) + private static void WriteNumericMultiWrite(ref this BufferWriter buffer, ulong number) { const byte AsciiDigitStart = (byte)'0'; @@ -140,7 +140,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } [MethodImpl(MethodImplOptions.NoInlining)] - private unsafe static void WriteAsciiMultiWrite(ref this CountingBufferWriter buffer, string data) + private unsafe static void WriteAsciiMultiWrite(ref this BufferWriter buffer, string data) { var remaining = data.Length; diff --git a/src/Kestrel.Core/Kestrel.Core.csproj b/src/Kestrel.Core/Kestrel.Core.csproj index cdacbd0e..8551a560 100644 --- a/src/Kestrel.Core/Kestrel.Core.csproj +++ b/src/Kestrel.Core/Kestrel.Core.csproj @@ -11,6 +11,10 @@ CS1591;$(NoWarn) + + + + @@ -21,8 +25,6 @@ - - diff --git a/src/Kestrel.Transport.Abstractions/Kestrel.Transport.Abstractions.csproj b/src/Kestrel.Transport.Abstractions/Kestrel.Transport.Abstractions.csproj index ff0becfe..66f9a655 100644 --- a/src/Kestrel.Transport.Abstractions/Kestrel.Transport.Abstractions.csproj +++ b/src/Kestrel.Transport.Abstractions/Kestrel.Transport.Abstractions.csproj @@ -13,11 +13,12 @@ - + + + - - + diff --git a/src/Kestrel.Transport.Abstractions/Properties/AssemblyInfo.cs b/src/Kestrel.Transport.Abstractions/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..a4f9a58d --- /dev/null +++ b/src/Kestrel.Transport.Abstractions/Properties/AssemblyInfo.cs @@ -0,0 +1,8 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Server.Kestrel.Core.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Sockets.FunctionalTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Libuv.FunctionalTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/shared/ThrowHelper.cs b/src/shared/ThrowHelper.cs new file mode 100644 index 00000000..7439e63d --- /dev/null +++ b/src/shared/ThrowHelper.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace System.Buffers +{ + internal class BuffersThrowHelper + { + public static void ThrowArgumentOutOfRangeException(ExceptionArgument argument) + { + throw GetArgumentOutOfRangeException(argument); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static ArgumentOutOfRangeException GetArgumentOutOfRangeException(ExceptionArgument argument) + { + return new ArgumentOutOfRangeException(GetArgumentName(argument)); + } + + private static string GetArgumentName(ExceptionArgument argument) + { + Debug.Assert(Enum.IsDefined(typeof(ExceptionArgument), argument), "The enum value is not defined, please check the ExceptionArgument Enum."); + + return argument.ToString(); + } + + internal enum ExceptionArgument + { + length, + } + } +} diff --git a/test/Kestrel.Core.Tests/BufferReaderTests.cs b/test/Kestrel.Core.Tests/BufferReaderTests.cs new file mode 100644 index 00000000..294f61cd --- /dev/null +++ b/test/Kestrel.Core.Tests/BufferReaderTests.cs @@ -0,0 +1,300 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Xunit; + +namespace System.Buffers.Tests +{ + public abstract class ReadableBufferReaderFacts + { + public class Array : SingleSegment + { + public Array() : base(ReadOnlySequenceFactory.ArrayFactory) { } + internal Array(ReadOnlySequenceFactory factory) : base(factory) { } + } + + public class OwnedMemory : SingleSegment + { + public OwnedMemory() : base(ReadOnlySequenceFactory.OwnedMemoryFactory) { } + } + public class Memory : SingleSegment + { + public Memory() : base(ReadOnlySequenceFactory.MemoryFactory) { } + } + + public class SingleSegment : SegmentPerByte + { + public SingleSegment() : base(ReadOnlySequenceFactory.SingleSegmentFactory) { } + internal SingleSegment(ReadOnlySequenceFactory factory) : base(factory) { } + + [Fact] + public void AdvanceSingleBufferSkipsBytes() + { + var reader = new BufferReader(Factory.CreateWithContent(new byte[] { 1, 2, 3, 4, 5 })); + reader.Advance(2); + Assert.Equal(2, reader.CurrentSegmentIndex); + Assert.Equal(3, reader.CurrentSegment[reader.CurrentSegmentIndex]); + Assert.Equal(3, reader.Peek()); + reader.Advance(2); + Assert.Equal(5, reader.Peek()); + Assert.Equal(4, reader.CurrentSegmentIndex); + Assert.Equal(5, reader.CurrentSegment[reader.CurrentSegmentIndex]); + } + + [Fact] + public void TakeReturnsByteAndMoves() + { + var reader = new BufferReader(Factory.CreateWithContent(new byte[] { 1, 2 })); + Assert.Equal(0, reader.CurrentSegmentIndex); + Assert.Equal(1, reader.CurrentSegment[reader.CurrentSegmentIndex]); + Assert.Equal(1, reader.Read()); + Assert.Equal(1, reader.CurrentSegmentIndex); + Assert.Equal(2, reader.CurrentSegment[reader.CurrentSegmentIndex]); + Assert.Equal(2, reader.Read()); + Assert.Equal(-1, reader.Read()); + } + } + + public class SegmentPerByte : ReadableBufferReaderFacts + { + public SegmentPerByte() : base(ReadOnlySequenceFactory.SegmentPerByteFactory) { } + internal SegmentPerByte(ReadOnlySequenceFactory factory) : base(factory) { } + } + + internal ReadOnlySequenceFactory Factory { get; } + + internal ReadableBufferReaderFacts(ReadOnlySequenceFactory factory) + { + Factory = factory; + } + + [Fact] + public void PeekReturnsByteWithoutMoving() + { + var reader = new BufferReader(Factory.CreateWithContent(new byte[] { 1, 2 })); + Assert.Equal(1, reader.Peek()); + Assert.Equal(1, reader.Peek()); + } + + [Fact] + public void CursorIsCorrectAtEnd() + { + var reader = new BufferReader(Factory.CreateWithContent(new byte[] { 1, 2 })); + reader.Read(); + reader.Read(); + Assert.True(reader.End); + } + + [Fact] + public void CursorIsCorrectWithEmptyLastBlock() + { + var first = new BufferSegment(new byte[] { 1, 2 }); + var last = first.Append(new byte[4]); + + var reader = new BufferReader(new ReadOnlySequence(first, 0, last, 0)); + reader.Read(); + reader.Read(); + reader.Read(); + Assert.Same(last, reader.Position.GetObject()); + Assert.Equal(0, reader.Position.GetInteger()); + Assert.True(reader.End); + } + + [Fact] + public void PeekReturnsMinuOneByteInTheEnd() + { + var reader = new BufferReader(Factory.CreateWithContent(new byte[] { 1, 2 })); + Assert.Equal(1, reader.Read()); + Assert.Equal(2, reader.Read()); + Assert.Equal(-1, reader.Peek()); + } + + [Fact] + public void AdvanceToEndThenPeekReturnsMinusOne() + { + var reader = new BufferReader(Factory.CreateWithContent(new byte[] { 1, 2, 3, 4, 5 })); + reader.Advance(5); + Assert.True(reader.End); + Assert.Equal(-1, reader.Peek()); + } + + [Fact] + public void AdvancingPastLengthThrows() + { + var reader = new BufferReader(Factory.CreateWithContent(new byte[] { 1, 2, 3, 4, 5 })); + try + { + reader.Advance(6); + Assert.True(false); + } + catch (Exception ex) + { + Assert.True(ex is ArgumentOutOfRangeException); + } + } + + [Fact] + public void CtorFindsFirstNonEmptySegment() + { + var buffer = Factory.CreateWithContent(new byte[] { 1 }); + var reader = new BufferReader(buffer); + + Assert.Equal(1, reader.Peek()); + } + + [Fact] + public void EmptySegmentsAreSkippedOnMoveNext() + { + var buffer = Factory.CreateWithContent(new byte[] { 1, 2 }); + var reader = new BufferReader(buffer); + + Assert.Equal(1, reader.Peek()); + reader.Advance(1); + Assert.Equal(2, reader.Peek()); + } + + [Fact] + public void PeekGoesToEndIfAllEmptySegments() + { + var buffer = Factory.CreateOfSize(0); + var reader = new BufferReader(buffer); + + Assert.Equal(-1, reader.Peek()); + Assert.True(reader.End); + } + + [Fact] + public void AdvanceTraversesSegments() + { + var buffer = Factory.CreateWithContent(new byte[] { 1, 2, 3 }); + var reader = new BufferReader(buffer); + + reader.Advance(2); + Assert.Equal(3, reader.CurrentSegment[reader.CurrentSegmentIndex]); + Assert.Equal(3, reader.Read()); + } + + [Fact] + public void AdvanceThrowsPastLengthMultipleSegments() + { + var buffer = Factory.CreateWithContent(new byte[] { 1, 2, 3 }); + var reader = new BufferReader(buffer); + + try + { + reader.Advance(4); + Assert.True(false); + } + catch (Exception ex) + { + Assert.True(ex is ArgumentOutOfRangeException); + } + } + + [Fact] + public void TakeTraversesSegments() + { + var buffer = Factory.CreateWithContent(new byte[] { 1, 2, 3 }); + var reader = new BufferReader(buffer); + + Assert.Equal(1, reader.Read()); + Assert.Equal(2, reader.Read()); + Assert.Equal(3, reader.Read()); + Assert.Equal(-1, reader.Read()); + } + + [Fact] + public void PeekTraversesSegments() + { + var buffer = Factory.CreateWithContent(new byte[] { 1, 2 }); + var reader = new BufferReader(buffer); + + Assert.Equal(1, reader.CurrentSegment[reader.CurrentSegmentIndex]); + Assert.Equal(1, reader.Read()); + + Assert.Equal(2, reader.CurrentSegment[reader.CurrentSegmentIndex]); + Assert.Equal(2, reader.Peek()); + Assert.Equal(2, reader.Read()); + Assert.Equal(-1, reader.Peek()); + Assert.Equal(-1, reader.Read()); + } + + [Fact] + public void PeekWorkesWithEmptySegments() + { + var buffer = Factory.CreateWithContent(new byte[] { 1 }); + var reader = new BufferReader(buffer); + + Assert.Equal(0, reader.CurrentSegmentIndex); + Assert.Equal(1, reader.CurrentSegment.Length); + Assert.Equal(1, reader.Peek()); + Assert.Equal(1, reader.Read()); + Assert.Equal(-1, reader.Peek()); + Assert.Equal(-1, reader.Read()); + } + + [Fact] + public void WorkesWithEmptyBuffer() + { + var reader = new BufferReader(Factory.CreateWithContent(new byte[] { })); + + Assert.Equal(0, reader.CurrentSegmentIndex); + Assert.Equal(0, reader.CurrentSegment.Length); + Assert.Equal(-1, reader.Peek()); + Assert.Equal(-1, reader.Read()); + } + + [Theory] + [InlineData(0, false)] + [InlineData(5, false)] + [InlineData(10, false)] + [InlineData(11, true)] + [InlineData(12, true)] + [InlineData(15, true)] + public void ReturnsCorrectCursor(int takes, bool end) + { + var readableBuffer = Factory.CreateWithContent(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }); + var reader = new BufferReader(readableBuffer); + for (int i = 0; i < takes; i++) + { + reader.Read(); + } + + var expected = end ? new byte[] { } : readableBuffer.Slice((long)takes).ToArray(); + Assert.Equal(expected, readableBuffer.Slice(reader.Position).ToArray()); + } + + [Fact] + public void SlicingBufferReturnsCorrectCursor() + { + var buffer = Factory.CreateWithContent(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }); + var sliced = buffer.Slice(2L); + + var reader = new BufferReader(sliced); + Assert.Equal(sliced.ToArray(), buffer.Slice(reader.Position).ToArray()); + Assert.Equal(2, reader.Peek()); + Assert.Equal(0, reader.CurrentSegmentIndex); + } + + [Fact] + public void ReaderIndexIsCorrect() + { + var buffer = Factory.CreateWithContent(new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + var reader = new BufferReader(buffer); + + var counter = 1; + while (!reader.End) + { + var span = reader.CurrentSegment; + for (int i = reader.CurrentSegmentIndex; i < span.Length; i++) + { + Assert.Equal(counter++, reader.CurrentSegment[i]); + } + reader.Advance(span.Length); + } + Assert.Equal(buffer.Length, reader.ConsumedBytes); + } + } + +} diff --git a/test/Kestrel.Core.Tests/BufferWriterTests.cs b/test/Kestrel.Core.Tests/BufferWriterTests.cs new file mode 100644 index 00000000..4a79901a --- /dev/null +++ b/test/Kestrel.Core.Tests/BufferWriterTests.cs @@ -0,0 +1,228 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace System.IO.Pipelines.Tests +{ + public class BufferWriterTests : IDisposable + { + protected Pipe Pipe; + public BufferWriterTests() + { + Pipe = new Pipe(new PipeOptions(useSynchronizationContext: false, pauseWriterThreshold: 0, resumeWriterThreshold: 0)); + } + + public void Dispose() + { + Pipe.Writer.Complete(); + Pipe.Reader.Complete(); + } + + private byte[] Read() + { + Pipe.Writer.FlushAsync().GetAwaiter().GetResult(); + Pipe.Writer.Complete(); + ReadResult readResult = Pipe.Reader.ReadAsync().GetAwaiter().GetResult(); + byte[] data = readResult.Buffer.ToArray(); + Pipe.Reader.AdvanceTo(readResult.Buffer.End); + return data; + } + + [Theory] + [InlineData(3, -1, 0)] + [InlineData(3, 0, -1)] + [InlineData(3, 0, 4)] + [InlineData(3, 4, 0)] + [InlineData(3, -1, -1)] + [InlineData(3, 4, 4)] + public void ThrowsForInvalidParameters(int arrayLength, int offset, int length) + { + BufferWriter writer = new BufferWriter(Pipe.Writer); + var array = new byte[arrayLength]; + for (var i = 0; i < array.Length; i++) + { + array[i] = (byte)(i + 1); + } + + writer.Write(new Span(array, 0, 0)); + writer.Write(new Span(array, array.Length, 0)); + + try + { + writer.Write(new Span(array, offset, length)); + Assert.True(false); + } + catch (Exception ex) + { + Assert.True(ex is ArgumentOutOfRangeException); + } + + writer.Write(new Span(array, 0, array.Length)); + writer.Commit(); + + Assert.Equal(array, Read()); + } + + [Theory] + [InlineData(0, 3)] + [InlineData(1, 2)] + [InlineData(2, 1)] + [InlineData(1, 1)] + public void CanWriteWithOffsetAndLength(int offset, int length) + { + BufferWriter writer = new BufferWriter(Pipe.Writer); + var array = new byte[] { 1, 2, 3 }; + + writer.Write(new Span(array, offset, length)); + + Assert.Equal(0, writer.BytesCommitted); + + writer.Commit(); + + Assert.Equal(length, writer.BytesCommitted); + Assert.Equal(array.Skip(offset).Take(length).ToArray(), Read()); + Assert.Equal(length, writer.BytesCommitted); + } + + [Fact] + public void CanWriteEmpty() + { + BufferWriter writer = new BufferWriter(Pipe.Writer); + var array = new byte[] { }; + + writer.Write(array); + writer.Write(new Span(array, 0, array.Length)); + writer.Commit(); + + Assert.Equal(0, writer.BytesCommitted); + Assert.Equal(array, Read()); + } + + [Fact] + public void CanWriteIntoHeadlessBuffer() + { + BufferWriter writer = new BufferWriter(Pipe.Writer); + + writer.Write(new byte[] { 1, 2, 3 }); + writer.Commit(); + + Assert.Equal(3, writer.BytesCommitted); + Assert.Equal(new byte[] { 1, 2, 3 }, Read()); + } + + [Fact] + public void CanWriteMultipleTimes() + { + BufferWriter writer = new BufferWriter(Pipe.Writer); + + writer.Write(new byte[] { 1 }); + writer.Write(new byte[] { 2 }); + writer.Write(new byte[] { 3 }); + writer.Commit(); + + Assert.Equal(3, writer.BytesCommitted); + Assert.Equal(new byte[] { 1, 2, 3 }, Read()); + } + + [Fact] + public void CanWriteOverTheBlockLength() + { + Memory memory = Pipe.Writer.GetMemory(); + BufferWriter writer = new BufferWriter(Pipe.Writer); + + IEnumerable source = Enumerable.Range(0, memory.Length).Select(i => (byte)i); + byte[] expectedBytes = source.Concat(source).Concat(source).ToArray(); + + writer.Write(expectedBytes); + writer.Commit(); + + Assert.Equal(expectedBytes.LongLength, writer.BytesCommitted); + Assert.Equal(expectedBytes, Read()); + } + + [Fact] + public void EnsureAllocatesSpan() + { + BufferWriter writer = new BufferWriter(Pipe.Writer); + writer.Ensure(10); + Assert.True(writer.Span.Length > 10); + Assert.Equal(0, writer.BytesCommitted); + Assert.Equal(new byte[] { }, Read()); + } + + [Fact] + public void ExposesSpan() + { + int initialLength = Pipe.Writer.GetMemory().Length; + BufferWriter writer = new BufferWriter(Pipe.Writer); + Assert.Equal(initialLength, writer.Span.Length); + Assert.Equal(new byte[] { }, Read()); + } + + [Fact] + public void SlicesSpanAndAdvancesAfterWrite() + { + int initialLength = Pipe.Writer.GetMemory().Length; + + BufferWriter writer = new BufferWriter(Pipe.Writer); + + writer.Write(new byte[] { 1, 2, 3 }); + writer.Commit(); + + Assert.Equal(3, writer.BytesCommitted); + Assert.Equal(initialLength - 3, writer.Span.Length); + Assert.Equal(Pipe.Writer.GetMemory().Length, writer.Span.Length); + Assert.Equal(new byte[] { 1, 2, 3 }, Read()); + } + + [Fact] + public void BufferWriterCountsBytesCommitted() + { + BufferWriter writer = new BufferWriter(Pipe.Writer); + + writer.Write(new byte[] { 1, 2, 3 }); + Assert.Equal(0, writer.BytesCommitted); + + writer.Commit(); + Assert.Equal(3, writer.BytesCommitted); + + writer.Ensure(10); + writer.Advance(10); + Assert.Equal(3, writer.BytesCommitted); + + writer.Commit(); + Assert.Equal(13, writer.BytesCommitted); + + Pipe.Writer.FlushAsync().GetAwaiter().GetResult(); + var readResult = Pipe.Reader.ReadAsync().GetAwaiter().GetResult(); + + // Consuming the buffer does not change BytesCommitted + Assert.Equal(13, readResult.Buffer.Length); + Assert.Equal(13, writer.BytesCommitted); + } + + [Theory] + [InlineData(5)] + [InlineData(50)] + [InlineData(500)] + [InlineData(5000)] + [InlineData(50000)] + public void WriteLargeDataBinary(int length) + { + var data = new byte[length]; + new Random(length).NextBytes(data); + + BufferWriter writer = new BufferWriter(Pipe.Writer); + writer.Write(data); + writer.Commit(); + + Assert.Equal(length, writer.BytesCommitted); + Assert.Equal(data, Read()); + } + } +} diff --git a/test/Kestrel.Core.Tests/DiagnosticMemoryPoolTests.cs b/test/Kestrel.Core.Tests/DiagnosticMemoryPoolTests.cs new file mode 100644 index 00000000..51c9a9fc --- /dev/null +++ b/test/Kestrel.Core.Tests/DiagnosticMemoryPoolTests.cs @@ -0,0 +1,228 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Buffers; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.Internal.Test +{ + public class DiagnosticMemoryPoolTests: MemoryPoolTests + { + protected override MemoryPool CreatePool() => new DiagnosticMemoryPool(new SlabMemoryPool()); + + [Fact] + public void DoubleDisposeThrows() + { + var memoryPool = CreatePool(); + memoryPool.Dispose(); + var exception = Assert.Throws(() => memoryPool.Dispose()); + Assert.Equal("Object is being disposed twice", exception.Message); + } + + [Fact] + public void DisposeWithActiveBlocksThrows() + { + var memoryPool = CreatePool(); + var block = memoryPool.Rent(); + ExpectDisposeException(memoryPool); + + var exception = Assert.Throws(() => block.Dispose()); + Assert.Equal("Block is being returned to disposed pool", exception.Message); + } + + [Fact] + public void DoubleBlockDisposeThrows() + { + var memoryPool = CreatePool(); + var block = memoryPool.Rent(); + block.Dispose(); + var exception = Assert.Throws(() => block.Dispose()); + Assert.Equal("Block is being disposed twice", exception.Message); + + ExpectDisposeAggregateException(memoryPool, exception); + } + + [Fact] + public void GetMemoryOfDisposedPoolThrows() + { + var memoryPool = CreatePool(); + var block = memoryPool.Rent(); + + ExpectDisposeException(memoryPool); + + var exception = Assert.Throws(() => block.Memory); + Assert.Equal("Block is backed by disposed slab", exception.Message); + } + + [Fact] + public void GetMemoryPinOfDisposedPoolThrows() + { + var memoryPool = CreatePool(); + var block = memoryPool.Rent(); + var memory = block.Memory; + + ExpectDisposeException(memoryPool); + + var exception = Assert.Throws(() => memory.Pin()); + Assert.Equal("Block is backed by disposed slab", exception.Message); + } + + [Fact] + public void GetMemorySpanOfDisposedPoolThrows() + { + var memoryPool = CreatePool(); + var block = memoryPool.Rent(); + var memory = block.Memory; + + ExpectDisposeException(memoryPool); + + var threw = false; + try + { + _ = memory.Span; + } + catch (InvalidOperationException ode) + { + threw = true; + Assert.Equal("Block is backed by disposed slab", ode.Message); + } + Assert.True(threw); + } + + [Fact] + public void GetMemoryTryGetArrayOfDisposedPoolThrows() + { + var memoryPool = CreatePool(); + var block = memoryPool.Rent(); + var memory = block.Memory; + + ExpectDisposeException(memoryPool); + + var exception = Assert.Throws(() => MemoryMarshal.TryGetArray(memory, out _)); + Assert.Equal("Block is backed by disposed slab", exception.Message); + } + + [Fact] + public void GetMemoryOfDisposedThrows() + { + var memoryPool = CreatePool(); + var block = memoryPool.Rent(); + + block.Dispose(); + + var exception = Assert.Throws(() => block.Memory); + Assert.Equal($"Cannot access a disposed object.{Environment.NewLine}Object name: 'MemoryPoolBlock'.", exception.Message); + + ExpectDisposeAggregateException(memoryPool, exception); + } + + [Fact] + public void GetMemoryPinOfDisposedThrows() + { + var memoryPool = CreatePool(); + var block = memoryPool.Rent(); + var memory = block.Memory; + + block.Dispose(); + + var exception = Assert.Throws(() => memory.Pin()); + Assert.Equal($"Cannot access a disposed object.{Environment.NewLine}Object name: 'MemoryPoolBlock'.", exception.Message); + + ExpectDisposeAggregateException(memoryPool, exception); + } + + [Fact] + public void GetMemorySpanOfDisposedThrows() + { + var memoryPool = CreatePool(); + var block = memoryPool.Rent(); + var memory = block.Memory; + + block.Dispose(); + + Exception exception = null; + try + { + _ = memory.Span; + } + catch (ObjectDisposedException ode) + { + exception = ode; + Assert.Equal($"Cannot access a disposed object.{Environment.NewLine}Object name: 'MemoryPoolBlock'.", ode.Message); + } + Assert.NotNull(exception); + + ExpectDisposeAggregateException(memoryPool, exception); + } + + [Fact] + public void GetMemoryTryGetArrayOfDisposedThrows() + { + var memoryPool = CreatePool(); + var block = memoryPool.Rent(); + var memory = block.Memory; + + block.Dispose(); + + var exception = Assert.Throws(() => MemoryMarshal.TryGetArray(memory, out _)); + Assert.Equal($"Cannot access a disposed object.{Environment.NewLine}Object name: 'MemoryPoolBlock'.", exception.Message); + + ExpectDisposeAggregateException(memoryPool, exception); + } + + [Fact] + public async Task DoesNotThrowWithLateReturns() + { + var memoryPool = new DiagnosticMemoryPool(new SlabMemoryPool(), allowLateReturn: true); + var block = memoryPool.Rent(); + memoryPool.Dispose(); + block.Dispose(); + await memoryPool.WhenAllBlocksReturnedAsync(TimeSpan.FromSeconds(5)); + } + + [Fact] + public async Task ThrowsOnAccessToLateBlocks() + { + var memoryPool = new DiagnosticMemoryPool(new SlabMemoryPool(), allowLateReturn: true); + var block = memoryPool.Rent(); + memoryPool.Dispose(); + + var exception = Assert.Throws(() => block.Memory); + Assert.Equal("Block is backed by disposed slab", exception.Message); + + block.Dispose(); + var aggregateException = await Assert.ThrowsAsync(async () => await memoryPool.WhenAllBlocksReturnedAsync(TimeSpan.FromSeconds(5))); + + Assert.Equal(new Exception [] { exception }, aggregateException.InnerExceptions); + } + + [Fact] + public void ExceptionsContainStackTraceWhenEnabled() + { + var memoryPool = new DiagnosticMemoryPool(new SlabMemoryPool(), rentTracking: true); + var block = memoryPool.Rent(); + + ExpectDisposeException(memoryPool); + + var exception = Assert.Throws(() => block.Memory); + Assert.Contains("Block is backed by disposed slab", exception.Message); + Assert.Contains("ExceptionsContainStackTraceWhenEnabled", exception.Message); + } + + private static void ExpectDisposeException(MemoryPool memoryPool) + { + var exception = Assert.Throws(() => memoryPool.Dispose()); + Assert.Contains("Memory pool with active blocks is being disposed, 0 of 1 returned", exception.Message); + } + + private static void ExpectDisposeAggregateException(MemoryPool memoryPool, params Exception[] inner) + { + var exception = Assert.Throws(() => memoryPool.Dispose()); + + Assert.Equal(inner, exception.InnerExceptions); + } + } +} \ No newline at end of file diff --git a/test/Kestrel.Core.Tests/Kestrel.Core.Tests.csproj b/test/Kestrel.Core.Tests/Kestrel.Core.Tests.csproj index 3229fd0f..2074ed65 100644 --- a/test/Kestrel.Core.Tests/Kestrel.Core.Tests.csproj +++ b/test/Kestrel.Core.Tests/Kestrel.Core.Tests.csproj @@ -9,6 +9,7 @@ + @@ -23,7 +24,6 @@ - diff --git a/test/Kestrel.Core.Tests/MemoryPoolTests.cs b/test/Kestrel.Core.Tests/MemoryPoolTests.cs new file mode 100644 index 00000000..8b56019d --- /dev/null +++ b/test/Kestrel.Core.Tests/MemoryPoolTests.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Buffers; +using Xunit; + +namespace Microsoft.Extensions.Internal.Test +{ + public abstract class MemoryPoolTests + { + protected abstract MemoryPool CreatePool(); + + [Fact] + public void CanDisposeAfterCreation() + { + var memoryPool = CreatePool(); + memoryPool.Dispose(); + } + + [Fact] + public void CanDisposeAfterReturningBlock() + { + var memoryPool = CreatePool(); + var block = memoryPool.Rent(); + block.Dispose(); + memoryPool.Dispose(); + } + + [Fact] + public void CanDisposeAfterPinUnpinBlock() + { + var memoryPool = CreatePool(); + var block = memoryPool.Rent(); + block.Memory.Pin().Dispose(); + block.Dispose(); + memoryPool.Dispose(); + } + + [Fact] + public void LeasingFromDisposedPoolThrows() + { + var memoryPool = CreatePool(); + memoryPool.Dispose(); + + var exception = Assert.Throws(() => memoryPool.Rent()); + Assert.Equal($"Cannot access a disposed object.{Environment.NewLine}Object name: 'MemoryPool'.", exception.Message); + } + } +} diff --git a/test/Kestrel.Core.Tests/PipelineExtensionTests.cs b/test/Kestrel.Core.Tests/PipelineExtensionTests.cs index 2e09a3d2..11f7209a 100644 --- a/test/Kestrel.Core.Tests/PipelineExtensionTests.cs +++ b/test/Kestrel.Core.Tests/PipelineExtensionTests.cs @@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests public void WritesNumericToAscii(ulong number) { var writerBuffer = _pipe.Writer; - var writer = new CountingBufferWriter(writerBuffer); + var writer = new BufferWriter(writerBuffer); writer.WriteNumeric(number); writer.Commit(); writerBuffer.FlushAsync().GetAwaiter().GetResult(); @@ -56,7 +56,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests public void WritesNumericAcrossSpanBoundaries(int gapSize) { var writerBuffer = _pipe.Writer; - var writer = new CountingBufferWriter(writerBuffer); + var writer = new BufferWriter(writerBuffer); // almost fill up the first block var spacer = new byte[writer.Span.Length - gapSize]; writer.Write(spacer); @@ -87,7 +87,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests public void EncodesAsAscii(string input, byte[] expected) { var pipeWriter = _pipe.Writer; - var writer = new CountingBufferWriter(pipeWriter); + var writer = new BufferWriter(pipeWriter); writer.WriteAsciiNoValidation(input); writer.Commit(); pipeWriter.FlushAsync().GetAwaiter().GetResult(); @@ -117,7 +117,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests // WriteAscii doesn't validate if characters are in the ASCII range // but it shouldn't produce more than one byte per character var writerBuffer = _pipe.Writer; - var writer = new CountingBufferWriter(writerBuffer); + var writer = new BufferWriter(writerBuffer); writer.WriteAsciiNoValidation(input); writer.Commit(); writerBuffer.FlushAsync().GetAwaiter().GetResult(); @@ -131,7 +131,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { const byte maxAscii = 0x7f; var writerBuffer = _pipe.Writer; - var writer = new CountingBufferWriter(writerBuffer); + var writer = new BufferWriter(writerBuffer); for (var i = 0; i < maxAscii; i++) { writer.WriteAsciiNoValidation(new string((char)i, 1)); @@ -161,7 +161,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { var testString = new string(' ', stringLength); var writerBuffer = _pipe.Writer; - var writer = new CountingBufferWriter(writerBuffer); + var writer = new BufferWriter(writerBuffer); // almost fill up the first block var spacer = new byte[writer.Span.Length - gapSize]; writer.Write(spacer); diff --git a/test/Kestrel.Core.Tests/SlabMemoryPoolTests.cs b/test/Kestrel.Core.Tests/SlabMemoryPoolTests.cs new file mode 100644 index 00000000..796dc2f5 --- /dev/null +++ b/test/Kestrel.Core.Tests/SlabMemoryPoolTests.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Buffers; +using Xunit; + +namespace Microsoft.Extensions.Internal.Test +{ + public class SlabMemoryPoolTests: MemoryPoolTests + { + protected override MemoryPool CreatePool() => new SlabMemoryPool(); + + [Fact] + public void DoubleDisposeWorks() + { + var memoryPool = CreatePool(); + memoryPool.Dispose(); + memoryPool.Dispose(); + } + + [Fact] + public void DisposeWithActiveBlocksWorks() + { + var memoryPool = CreatePool(); + var block = memoryPool.Rent(); + memoryPool.Dispose(); + } + } +} \ No newline at end of file diff --git a/tools/CodeGenerator/KnownHeaders.cs b/tools/CodeGenerator/KnownHeaders.cs index 84aec20e..9d84bab3 100644 --- a/tools/CodeGenerator/KnownHeaders.cs +++ b/tools/CodeGenerator/KnownHeaders.cs @@ -541,7 +541,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http return true; }} {(loop.ClassName == "HttpResponseHeaders" ? $@" - internal void CopyToFast(ref CountingBufferWriter output) + internal void CopyToFast(ref BufferWriter output) {{ var tempBits = _bits | (_contentLength.HasValue ? {1L << 63}L : 0); {Each(loop.Headers.Where(header => header.Identifier != "ContentLength").OrderBy(h => !h.PrimaryHeader), header => $@"