From 128eefdef3825e1a2b8313f76b235c465b10b682 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Wed, 11 Jul 2018 22:31:23 -0700 Subject: [PATCH 1/5] Remove usage of the Microsoft.Extensions.Buffers.Sources package, copy the source into Kestrel This moves source code that used to be in aspnet/Common. It was only used here, so this simplifies the process of working with these internal-only APIs. cref https://github.com/aspnet/Common/pull/386 --- build/dependencies.props | 2 - src/Kestrel.Core/Internal/BufferReader.cs | 125 ++++++++ src/Kestrel.Core/Internal/BufferWriter.cs | 93 ++++++ src/Kestrel.Core/Kestrel.Core.csproj | 5 +- .../Internal/MemoryPoolBlock.Debug.cs | 125 ++++++++ .../Internal/MemoryPoolBlock.Release.cs | 65 ++++ .../Internal/MemoryPoolSlab.cs | 97 ++++++ .../Internal/SlabMemoryPool.cs | 193 +++++++++++ .../Kestrel.Transport.Abstractions.csproj | 5 +- src/shared/ThrowHelper.cs | 87 +++++ test/Kestrel.Core.Tests/BufferReaderTests.cs | 300 ++++++++++++++++++ test/Kestrel.Core.Tests/BufferWriterTests.cs | 223 +++++++++++++ .../Kestrel.Core.Tests.csproj | 1 - test/shared/BufferSegment.cs | 24 ++ test/shared/CustomMemoryForTest.cs | 45 +++ test/shared/ReadOnlySequenceFactory.cs | 148 +++++++++ 16 files changed, 1532 insertions(+), 6 deletions(-) create mode 100644 src/Kestrel.Core/Internal/BufferReader.cs create mode 100644 src/Kestrel.Core/Internal/BufferWriter.cs create mode 100644 src/Kestrel.Transport.Abstractions/Internal/MemoryPoolBlock.Debug.cs create mode 100644 src/Kestrel.Transport.Abstractions/Internal/MemoryPoolBlock.Release.cs create mode 100644 src/Kestrel.Transport.Abstractions/Internal/MemoryPoolSlab.cs create mode 100644 src/Kestrel.Transport.Abstractions/Internal/SlabMemoryPool.cs create mode 100644 src/shared/ThrowHelper.cs create mode 100644 test/Kestrel.Core.Tests/BufferReaderTests.cs create mode 100644 test/Kestrel.Core.Tests/BufferWriterTests.cs create mode 100644 test/shared/BufferSegment.cs create mode 100644 test/shared/CustomMemoryForTest.cs create mode 100644 test/shared/ReadOnlySequenceFactory.cs diff --git a/build/dependencies.props b/build/dependencies.props index a53bc20e1f..90b286111a 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -18,8 +18,6 @@ 2.1.0 2.1.1 2.1.1 - 2.1.1 - 2.1.1 2.1.1 2.1.1 2.1.1 diff --git a/src/Kestrel.Core/Internal/BufferReader.cs b/src/Kestrel.Core/Internal/BufferReader.cs new file mode 100644 index 0000000000..5a1e75b69b --- /dev/null +++ b/src/Kestrel.Core/Internal/BufferReader.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; +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; + + public BufferReader(ReadOnlySequence buffer) + { + _end = false; + _index = 0; + _consumedBytes = 0; + _sequence = buffer; + _currentSequencePosition = _sequence.Start; + _nextSequencePosition = _currentSequencePosition; + _currentSpan = ReadOnlySpan.Empty; + MoveNext(); + } + + 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; + } + + public void Advance(int byteCount) + { + if (byteCount < 0) + { + ThrowHelper.ThrowArgumentOutOfRangeException(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) + { + ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.length); + } + } + } +} diff --git a/src/Kestrel.Core/Internal/BufferWriter.cs b/src/Kestrel.Core/Internal/BufferWriter.cs new file mode 100644 index 0000000000..2b63ea920c --- /dev/null +++ b/src/Kestrel.Core/Internal/BufferWriter.cs @@ -0,0 +1,93 @@ +// 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.Runtime.CompilerServices; + +namespace System.Buffers +{ + internal ref struct BufferWriter where T: IBufferWriter + { + private T _output; + private Span _span; + private int _buffered; + + public BufferWriter(T output) + { + _buffered = 0; + _output = output; + _span = output.GetSpan(); + } + + public Span Span => _span; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Commit() + { + var buffered = _buffered; + if (buffered > 0) + { + _buffered = 0; + _output.Advance(buffered); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Advance(int count) + { + _buffered += count; + _span = _span.Slice(count); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(ReadOnlySpan source) + { + if (_span.Length >= source.Length) + { + source.CopyTo(_span); + Advance(source.Length); + } + else + { + WriteMultiBuffer(source); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Ensure(int count = 1) + { + if (_span.Length < count) + { + EnsureMore(count); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void EnsureMore(int count = 0) + { + if (_buffered > 0) + { + Commit(); + } + + _output.GetMemory(count); + _span = _output.GetSpan(); + } + + private void WriteMultiBuffer(ReadOnlySpan source) + { + while (source.Length > 0) + { + if (_span.Length == 0) + { + EnsureMore(); + } + + var writable = Math.Min(source.Length, _span.Length); + source.Slice(0, writable).CopyTo(_span); + source = source.Slice(writable); + Advance(writable); + } + } + } +} diff --git a/src/Kestrel.Core/Kestrel.Core.csproj b/src/Kestrel.Core/Kestrel.Core.csproj index b81428efa5..8551a56002 100644 --- a/src/Kestrel.Core/Kestrel.Core.csproj +++ b/src/Kestrel.Core/Kestrel.Core.csproj @@ -11,6 +11,10 @@ CS1591;$(NoWarn) + + + + @@ -21,7 +25,6 @@ - diff --git a/src/Kestrel.Transport.Abstractions/Internal/MemoryPoolBlock.Debug.cs b/src/Kestrel.Transport.Abstractions/Internal/MemoryPoolBlock.Debug.cs new file mode 100644 index 0000000000..6f815716d4 --- /dev/null +++ b/src/Kestrel.Transport.Abstractions/Internal/MemoryPoolBlock.Debug.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. + +#if DEBUG + +using System.Threading; +using System.Diagnostics; + +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 : MemoryManager + { + private readonly int _offset; + private readonly int _length; + + private int _pinCount; + + /// + /// 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; + } + + /// + /// 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 override Memory Memory + { + get + { + if (!Slab.IsActive) ThrowHelper.ThrowObjectDisposedException(ExceptionArgument.MemoryPoolBlock); + + return CreateMemory(_length); + } + } + + +#if BLOCK_LEASE_TRACKING + public bool IsLeased { get; set; } + public string Leaser { get; set; } +#endif + + ~MemoryPoolBlock() + { + if (Slab != null && Slab.IsActive) + { + Debug.Assert(false, $"{Environment.NewLine}{Environment.NewLine}*** Block being garbage collected instead of returned to pool" + +#if BLOCK_LEASE_TRACKING + $": {Leaser}" + +#endif + $" ***{ Environment.NewLine}"); + + // Need to make a new object because this one is being finalized + Pool.Return(new MemoryPoolBlock(Pool, Slab, _offset, _length)); + } + } + + protected override void Dispose(bool disposing) + { + if (!Slab.IsActive) ThrowHelper.ThrowObjectDisposedException(ExceptionArgument.MemoryPoolBlock); + + if (Volatile.Read(ref _pinCount) > 0) + { + ThrowHelper.ThrowInvalidOperationException_ReturningPinnedBlock(); + } + + Pool.Return(this); + } + + public override Span GetSpan() => new Span(Slab.Array, _offset, _length); + + public override MemoryHandle Pin(int byteOffset = 0) + { + if (!Slab.IsActive) ThrowHelper.ThrowObjectDisposedException(ExceptionArgument.MemoryPoolBlock); + if (byteOffset < 0 || byteOffset > _length) ThrowHelper.ThrowArgumentOutOfRangeException(_length, byteOffset); + + Interlocked.Increment(ref _pinCount); + unsafe + { + return new MemoryHandle((Slab.NativePointer + _offset + byteOffset).ToPointer(), default, this); + } + } + + protected override bool TryGetArray(out ArraySegment segment) + { + segment = new ArraySegment(Slab.Array, _offset, _length); + return true; + } + + public override void Unpin() + { + if (Interlocked.Decrement(ref _pinCount) < 0) + { + ThrowHelper.ThrowInvalidOperationException_ReferenceCountZero(); + } + } + + public void Lease() + { +#if BLOCK_LEASE_TRACKING + Leaser = Environment.StackTrace; + IsLeased = true; +#endif + } + } +} + +#endif \ No newline at end of file diff --git a/src/Kestrel.Transport.Abstractions/Internal/MemoryPoolBlock.Release.cs b/src/Kestrel.Transport.Abstractions/Internal/MemoryPoolBlock.Release.cs new file mode 100644 index 0000000000..93784df05e --- /dev/null +++ b/src/Kestrel.Transport.Abstractions/Internal/MemoryPoolBlock.Release.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#if RELEASE + +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() + { + } + } +} + +#endif diff --git a/src/Kestrel.Transport.Abstractions/Internal/MemoryPoolSlab.cs b/src/Kestrel.Transport.Abstractions/Internal/MemoryPoolSlab.cs new file mode 100644 index 0000000000..4b15f88233 --- /dev/null +++ b/src/Kestrel.Transport.Abstractions/Internal/MemoryPoolSlab.cs @@ -0,0 +1,97 @@ +// 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 readonly GCHandle _gcHandle; + private readonly IntPtr _nativePointer; + private byte[] _data; + + private bool _isActive; + private bool _disposedValue; + + public MemoryPoolSlab(byte[] data) + { + _data = data; + _gcHandle = GCHandle.Alloc(data, GCHandleType.Pinned); + _nativePointer = _gcHandle.AddrOfPinnedObject(); + _isActive = true; + } + + /// + /// 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 => _isActive; + + public IntPtr NativePointer => _nativePointer; + + public byte[] Array => _data; + + public int Length => _data.Length; + + 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 (!_disposedValue) + { + if (disposing) + { + // N/A: dispose managed state (managed objects). + } + + _isActive = false; + + if (_gcHandle.IsAllocated) + { + _gcHandle.Free(); + } + + // set large fields to null. + _data = null; + + _disposedValue = true; + } + } + + // override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. + ~MemoryPoolSlab() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(false); + } + + // This code added to correctly implement the disposable pattern. + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + Dispose(true); + // uncomment the following line if the finalizer is overridden above. + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Kestrel.Transport.Abstractions/Internal/SlabMemoryPool.cs b/src/Kestrel.Transport.Abstractions/Internal/SlabMemoryPool.cs new file mode 100644 index 0000000000..e36ec70adb --- /dev/null +++ b/src/Kestrel.Transport.Abstractions/Internal/SlabMemoryPool.cs @@ -0,0 +1,193 @@ +// 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; + +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 _disposedValue = false; // To detect redundant calls + + /// + /// 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 == AnySize) size = _blockSize; + else if (size > _blockSize) + { + ThrowHelper.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() + { + Debug.Assert(!_disposedValue, "Block being leased from disposed pool!"); + + 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 firstOffset = (int)((((ulong)basePtr + (uint)_blockSize - 1) & ~((uint)_blockSize - 1)) - (ulong)basePtr); + // Ensure page aligned + Debug.Assert((((ulong)basePtr + (uint)firstOffset) & (uint)(_blockSize - 1)) == 0); + + var blockAllocationLength = ((_slabLength - firstOffset) & ~(_blockSize - 1)); + var offset = firstOffset; + for (; + offset + _blockSize < blockAllocationLength; + offset += _blockSize) + { + var block = new MemoryPoolBlock( + this, + slab, + offset, + _blockSize); +#if BLOCK_LEASE_TRACKING + block.IsLeased = true; +#endif + Return(block); + } + + Debug.Assert(offset + _blockSize - firstOffset == blockAllocationLength); + // return last block rather than adding to pool + var newBlock = new MemoryPoolBlock( + this, + slab, + offset, + _blockSize); + + return newBlock; + } + + /// + /// 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 (block.Slab != null && block.Slab.IsActive) + { + _blocks.Enqueue(block); + } + else + { + GC.SuppressFinalize(block); + } + } + + protected override void Dispose(bool disposing) + { + if (!_disposedValue) + { + _disposedValue = true; +#if DEBUG && !INNER_LOOP + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); +#endif + 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); + } + + // N/A: free unmanaged resources (unmanaged objects) and override a finalizer below. + + // N/A: set large fields to null. + + } + } + } +} diff --git a/src/Kestrel.Transport.Abstractions/Kestrel.Transport.Abstractions.csproj b/src/Kestrel.Transport.Abstractions/Kestrel.Transport.Abstractions.csproj index 58529f72d0..3dc81350fb 100644 --- a/src/Kestrel.Transport.Abstractions/Kestrel.Transport.Abstractions.csproj +++ b/src/Kestrel.Transport.Abstractions/Kestrel.Transport.Abstractions.csproj @@ -13,10 +13,11 @@ - + + - + diff --git a/src/shared/ThrowHelper.cs b/src/shared/ThrowHelper.cs new file mode 100644 index 0000000000..da31583777 --- /dev/null +++ b/src/shared/ThrowHelper.cs @@ -0,0 +1,87 @@ +// 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 ThrowHelper + { + 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 ThrowArgumentOutOfRangeException(ExceptionArgument argument) + { + throw GetArgumentOutOfRangeException(argument); + } + + public static void ThrowInvalidOperationException_ReferenceCountZero() + { + throw new InvalidOperationException("Can't release when reference count is already zero"); + } + + public static void ThrowInvalidOperationException_ReturningPinnedBlock() + { + throw new InvalidOperationException("Can't release when reference count is already zero"); + } + + 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(ExceptionArgument argument) + { + return new ArgumentOutOfRangeException(GetArgumentName(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 + } +} diff --git a/test/Kestrel.Core.Tests/BufferReaderTests.cs b/test/Kestrel.Core.Tests/BufferReaderTests.cs new file mode 100644 index 0000000000..294f61cdb0 --- /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 0000000000..8a071385a4 --- /dev/null +++ b/test/Kestrel.Core.Tests/BufferWriterTests.cs @@ -0,0 +1,223 @@ +// 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 CanWriteWithOffsetAndLenght(int offset, int length) + { + BufferWriter writer = new BufferWriter(Pipe.Writer); + var array = new byte[] { 1, 2, 3 }; + + writer.Write(new Span(array, offset, length)); + writer.Commit(); + + Assert.Equal(array.Skip(offset).Take(length).ToArray(), Read()); + } + + [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(array, Read()); + } + + [Fact] + public void CanWriteIntoHeadlessBuffer() + { + BufferWriter writer = new BufferWriter(Pipe.Writer); + + writer.Write(new byte[] { 1, 2, 3 }); + writer.Commit(); + + 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(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, Read()); + } + + [Fact] + public void EnsureAllocatesSpan() + { + BufferWriter writer = new BufferWriter(Pipe.Writer); + writer.Ensure(10); + Assert.True(writer.Span.Length > 10); + 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(initialLength - 3, writer.Span.Length); + Assert.Equal(Pipe.Writer.GetMemory().Length, writer.Span.Length); + Assert.Equal(new byte[] { 1, 2, 3 }, Read()); + } + + [Theory] + [InlineData(5)] + [InlineData(50)] + [InlineData(500)] + [InlineData(5000)] + [InlineData(50000)] + public async Task WriteLargeDataBinary(int length) + { + var data = new byte[length]; + new Random(length).NextBytes(data); + PipeWriter output = Pipe.Writer; + output.Write(data); + await output.FlushAsync(); + + ReadResult result = await Pipe.Reader.ReadAsync(); + ReadOnlySequence input = result.Buffer; + Assert.Equal(data, input.ToArray()); + Pipe.Reader.AdvanceTo(input.End); + } + + [Fact] + public async Task CanWriteNothingToBuffer() + { + PipeWriter buffer = Pipe.Writer; + buffer.GetMemory(0); + buffer.Advance(0); // doing nothing, the hard way + await buffer.FlushAsync(); + } + + [Fact] + public void EmptyWriteDoesNotThrow() + { + Pipe.Writer.Write(new byte[0]); + } + + [Fact] + public void ThrowsOnAdvanceOverMemorySize() + { + Memory buffer = Pipe.Writer.GetMemory(1); + var exception = Assert.Throws(() => Pipe.Writer.Advance(buffer.Length + 1)); + Assert.Equal("Can't advance past buffer size.", exception.Message); + } + + [Fact] + public void ThrowsOnAdvanceWithNoMemory() + { + PipeWriter buffer = Pipe.Writer; + var exception = Assert.Throws(() => buffer.Advance(1)); + Assert.Equal("No writing operation. Make sure GetMemory() was called.", exception.Message); + } + } +} diff --git a/test/Kestrel.Core.Tests/Kestrel.Core.Tests.csproj b/test/Kestrel.Core.Tests/Kestrel.Core.Tests.csproj index 3229fd0fa7..fdbbd000ef 100644 --- a/test/Kestrel.Core.Tests/Kestrel.Core.Tests.csproj +++ b/test/Kestrel.Core.Tests/Kestrel.Core.Tests.csproj @@ -23,7 +23,6 @@ - diff --git a/test/shared/BufferSegment.cs b/test/shared/BufferSegment.cs new file mode 100644 index 0000000000..d89f4addd5 --- /dev/null +++ b/test/shared/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/test/shared/CustomMemoryForTest.cs b/test/shared/CustomMemoryForTest.cs new file mode 100644 index 0000000000..20406f0a99 --- /dev/null +++ b/test/shared/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/test/shared/ReadOnlySequenceFactory.cs b/test/shared/ReadOnlySequenceFactory.cs new file mode 100644 index 0000000000..0fc0c6585f --- /dev/null +++ b/test/shared/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); + } + } +} From f179339a79165b40e337d737f80a006407bf88fc Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Thu, 12 Jul 2018 11:58:49 -0700 Subject: [PATCH 2/5] Combine BufferWriter and CountingBufferWriter --- .../BenchmarkApplication.cs | 6 +- src/Kestrel.Core/Internal/BufferWriter.cs | 11 ++- src/Kestrel.Core/Internal/Http/ChunkWriter.cs | 4 +- .../Internal/Http/CountingBufferWriter.cs | 99 ------------------- .../Internal/Http/Http1OutputProducer.cs | 4 +- .../Internal/Http/HttpHeaders.Generated.cs | 2 +- .../Internal/Http/HttpProtocol.cs | 2 +- .../Internal/Http/HttpResponseHeaders.cs | 2 +- .../Internal/Http/PipelineExtensions.cs | 8 +- test/Kestrel.Core.Tests/BufferWriterTests.cs | 58 ++++------- .../PipelineExtensionTests.cs | 12 +-- tools/CodeGenerator/KnownHeaders.cs | 2 +- 12 files changed, 46 insertions(+), 164 deletions(-) delete mode 100644 src/Kestrel.Core/Internal/Http/CountingBufferWriter.cs diff --git a/benchmarkapps/PlatformBenchmarks/BenchmarkApplication.cs b/benchmarkapps/PlatformBenchmarks/BenchmarkApplication.cs index d65452c13c..6551bee358 100644 --- a/benchmarkapps/PlatformBenchmarks/BenchmarkApplication.cs +++ b/benchmarkapps/PlatformBenchmarks/BenchmarkApplication.cs @@ -78,7 +78,7 @@ namespace PlatformBenchmarks } private static void PlainText(PipeWriter pipeWriter) { - var writer = new CountingBufferWriter(pipeWriter); + var writer = new BufferWriter(pipeWriter); // HTTP 1.1 OK writer.Write(_http11OK); @@ -105,7 +105,7 @@ namespace PlatformBenchmarks private static void Json(PipeWriter pipeWriter) { - var writer = new CountingBufferWriter(pipeWriter); + var writer = new BufferWriter(pipeWriter); // HTTP 1.1 OK writer.Write(_http11OK); @@ -134,7 +134,7 @@ namespace PlatformBenchmarks private static void Default(PipeWriter pipeWriter) { - var writer = new CountingBufferWriter(pipeWriter); + var writer = new BufferWriter(pipeWriter); // HTTP 1.1 OK writer.Write(_http11OK); diff --git a/src/Kestrel.Core/Internal/BufferWriter.cs b/src/Kestrel.Core/Internal/BufferWriter.cs index 2b63ea920c..1f33f3e4cb 100644 --- a/src/Kestrel.Core/Internal/BufferWriter.cs +++ b/src/Kestrel.Core/Internal/BufferWriter.cs @@ -1,25 +1,27 @@ -// 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. +// 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; namespace System.Buffers { - internal ref struct BufferWriter where T: IBufferWriter + internal ref struct BufferWriter where T : IBufferWriter { private T _output; private Span _span; private int _buffered; + private long _bytesCommitted; public BufferWriter(T output) { _buffered = 0; + _bytesCommitted = 0; _output = output; _span = output.GetSpan(); } public Span Span => _span; + public long BytesCommitted => _bytesCommitted; [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Commit() @@ -27,6 +29,7 @@ namespace System.Buffers var buffered = _buffered; if (buffered > 0) { + _bytesCommitted += buffered; _buffered = 0; _output.Advance(buffered); } diff --git a/src/Kestrel.Core/Internal/Http/ChunkWriter.cs b/src/Kestrel.Core/Internal/Http/ChunkWriter.cs index 2184937b07..3d8cc4566b 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/CountingBufferWriter.cs b/src/Kestrel.Core/Internal/Http/CountingBufferWriter.cs deleted file mode 100644 index e3299c6027..0000000000 --- a/src/Kestrel.Core/Internal/Http/CountingBufferWriter.cs +++ /dev/null @@ -1,99 +0,0 @@ - -// 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; - -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 - { - private T _output; - private Span _span; - private int _buffered; - private long _bytesCommitted; - - public CountingBufferWriter(T output) - { - _buffered = 0; - _bytesCommitted = 0; - _output = output; - _span = output.GetSpan(); - } - - public Span Span => _span; - public long BytesCommitted => _bytesCommitted; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Commit() - { - var buffered = _buffered; - if (buffered > 0) - { - _bytesCommitted += buffered; - _buffered = 0; - _output.Advance(buffered); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Advance(int count) - { - _buffered += count; - _span = _span.Slice(count); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Write(ReadOnlySpan source) - { - if (_span.Length >= source.Length) - { - source.CopyTo(_span); - Advance(source.Length); - } - else - { - WriteMultiBuffer(source); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Ensure(int count = 1) - { - if (_span.Length < count) - { - EnsureMore(count); - } - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private void EnsureMore(int count = 0) - { - if (_buffered > 0) - { - Commit(); - } - - _output.GetMemory(count); - _span = _output.GetSpan(); - } - - private void WriteMultiBuffer(ReadOnlySpan source) - { - while (source.Length > 0) - { - if (_span.Length == 0) - { - EnsureMore(); - } - - var writable = Math.Min(source.Length, _span.Length); - source.Slice(0, writable).CopyTo(_span); - source = source.Slice(writable); - Advance(writable); - } - } - } -} diff --git a/src/Kestrel.Core/Internal/Http/Http1OutputProducer.cs b/src/Kestrel.Core/Internal/Http/Http1OutputProducer.cs index 0fd62301bd..685980d1c6 100644 --- a/src/Kestrel.Core/Internal/Http/Http1OutputProducer.cs +++ b/src/Kestrel.Core/Internal/Http/Http1OutputProducer.cs @@ -128,7 +128,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); @@ -210,7 +210,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http } writableBuffer = _pipeWriter; - var writer = new CountingBufferWriter(writableBuffer); + var writer = new BufferWriter(writableBuffer); 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 f81e87a06c..d84f15706d 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 0c759c4451..b0ae93147d 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 a4b81cf69a..1df80f3dc6 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 fce822b6c5..e56c43b23c 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/test/Kestrel.Core.Tests/BufferWriterTests.cs b/test/Kestrel.Core.Tests/BufferWriterTests.cs index 8a071385a4..2060ccdb32 100644 --- a/test/Kestrel.Core.Tests/BufferWriterTests.cs +++ b/test/Kestrel.Core.Tests/BufferWriterTests.cs @@ -4,7 +4,6 @@ using System.Buffers; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Xunit; namespace System.IO.Pipelines.Tests @@ -73,15 +72,20 @@ namespace System.IO.Pipelines.Tests [InlineData(1, 2)] [InlineData(2, 1)] [InlineData(1, 1)] - public void CanWriteWithOffsetAndLenght(int offset, int length) + 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] @@ -94,6 +98,7 @@ namespace System.IO.Pipelines.Tests writer.Write(new Span(array, 0, array.Length)); writer.Commit(); + Assert.Equal(0, writer.BytesCommitted); Assert.Equal(array, Read()); } @@ -105,6 +110,7 @@ namespace System.IO.Pipelines.Tests writer.Write(new byte[] { 1, 2, 3 }); writer.Commit(); + Assert.Equal(3, writer.BytesCommitted); Assert.Equal(new byte[] { 1, 2, 3 }, Read()); } @@ -118,6 +124,7 @@ namespace System.IO.Pipelines.Tests writer.Write(new byte[] { 3 }); writer.Commit(); + Assert.Equal(3, writer.BytesCommitted); Assert.Equal(new byte[] { 1, 2, 3 }, Read()); } @@ -133,6 +140,7 @@ namespace System.IO.Pipelines.Tests writer.Write(expectedBytes); writer.Commit(); + Assert.Equal(expectedBytes.LongLength, writer.BytesCommitted); Assert.Equal(expectedBytes, Read()); } @@ -142,6 +150,7 @@ namespace System.IO.Pipelines.Tests 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()); } @@ -164,6 +173,7 @@ namespace System.IO.Pipelines.Tests 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()); @@ -175,49 +185,17 @@ namespace System.IO.Pipelines.Tests [InlineData(500)] [InlineData(5000)] [InlineData(50000)] - public async Task WriteLargeDataBinary(int length) + public void WriteLargeDataBinary(int length) { var data = new byte[length]; new Random(length).NextBytes(data); - PipeWriter output = Pipe.Writer; - output.Write(data); - await output.FlushAsync(); - ReadResult result = await Pipe.Reader.ReadAsync(); - ReadOnlySequence input = result.Buffer; - Assert.Equal(data, input.ToArray()); - Pipe.Reader.AdvanceTo(input.End); - } + BufferWriter writer = new BufferWriter(Pipe.Writer); + writer.Write(data); + writer.Commit(); - [Fact] - public async Task CanWriteNothingToBuffer() - { - PipeWriter buffer = Pipe.Writer; - buffer.GetMemory(0); - buffer.Advance(0); // doing nothing, the hard way - await buffer.FlushAsync(); - } - - [Fact] - public void EmptyWriteDoesNotThrow() - { - Pipe.Writer.Write(new byte[0]); - } - - [Fact] - public void ThrowsOnAdvanceOverMemorySize() - { - Memory buffer = Pipe.Writer.GetMemory(1); - var exception = Assert.Throws(() => Pipe.Writer.Advance(buffer.Length + 1)); - Assert.Equal("Can't advance past buffer size.", exception.Message); - } - - [Fact] - public void ThrowsOnAdvanceWithNoMemory() - { - PipeWriter buffer = Pipe.Writer; - var exception = Assert.Throws(() => buffer.Advance(1)); - Assert.Equal("No writing operation. Make sure GetMemory() was called.", exception.Message); + Assert.Equal(length, writer.BytesCommitted); + Assert.Equal(data, Read()); } } } diff --git a/test/Kestrel.Core.Tests/PipelineExtensionTests.cs b/test/Kestrel.Core.Tests/PipelineExtensionTests.cs index 43c385772e..e3a89832da 100644 --- a/test/Kestrel.Core.Tests/PipelineExtensionTests.cs +++ b/test/Kestrel.Core.Tests/PipelineExtensionTests.cs @@ -36,7 +36,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(); @@ -54,7 +54,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); @@ -85,7 +85,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(); @@ -115,7 +115,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(); @@ -129,7 +129,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)); @@ -159,7 +159,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/tools/CodeGenerator/KnownHeaders.cs b/tools/CodeGenerator/KnownHeaders.cs index badf9087a7..08646dc61a 100644 --- a/tools/CodeGenerator/KnownHeaders.cs +++ b/tools/CodeGenerator/KnownHeaders.cs @@ -548,7 +548,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 => $@" From d8c77335e835f9b633f994de6f7ba563ac4d603d Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Thu, 12 Jul 2018 13:17:28 -0700 Subject: [PATCH 3/5] Reorganize code so Kestrel now produces the Microsoft.Extensions.Buffers.Testing.Sources package --- NuGetPackageVerifier.json | 8 +++++++- .../BufferSegment.cs | 0 .../CustomMemoryForTest.cs | 0 .../ReadOnlySequenceFactory.cs | 0 test/Kestrel.Core.Tests/Kestrel.Core.Tests.csproj | 1 + 5 files changed, 8 insertions(+), 1 deletion(-) rename {test/shared => shared/Microsoft.Extensions.Buffers.Testing.Sources}/BufferSegment.cs (100%) rename {test/shared => shared/Microsoft.Extensions.Buffers.Testing.Sources}/CustomMemoryForTest.cs (100%) rename {test/shared => shared/Microsoft.Extensions.Buffers.Testing.Sources}/ReadOnlySequenceFactory.cs (100%) diff --git a/NuGetPackageVerifier.json b/NuGetPackageVerifier.json index b153ab1515..4f55b78b16 100644 --- a/NuGetPackageVerifier.json +++ b/NuGetPackageVerifier.json @@ -1,7 +1,13 @@ { + "adx-nonshipping": { + "rules": [], + "packages": { + "Microsoft.Extensions.Buffers.Testing.Sources": {} + } + }, "Default": { "rules": [ "DefaultCompositeRule" ] } -} \ No newline at end of file +} diff --git a/test/shared/BufferSegment.cs b/shared/Microsoft.Extensions.Buffers.Testing.Sources/BufferSegment.cs similarity index 100% rename from test/shared/BufferSegment.cs rename to shared/Microsoft.Extensions.Buffers.Testing.Sources/BufferSegment.cs diff --git a/test/shared/CustomMemoryForTest.cs b/shared/Microsoft.Extensions.Buffers.Testing.Sources/CustomMemoryForTest.cs similarity index 100% rename from test/shared/CustomMemoryForTest.cs rename to shared/Microsoft.Extensions.Buffers.Testing.Sources/CustomMemoryForTest.cs diff --git a/test/shared/ReadOnlySequenceFactory.cs b/shared/Microsoft.Extensions.Buffers.Testing.Sources/ReadOnlySequenceFactory.cs similarity index 100% rename from test/shared/ReadOnlySequenceFactory.cs rename to shared/Microsoft.Extensions.Buffers.Testing.Sources/ReadOnlySequenceFactory.cs diff --git a/test/Kestrel.Core.Tests/Kestrel.Core.Tests.csproj b/test/Kestrel.Core.Tests/Kestrel.Core.Tests.csproj index fdbbd000ef..2074ed6526 100644 --- a/test/Kestrel.Core.Tests/Kestrel.Core.Tests.csproj +++ b/test/Kestrel.Core.Tests/Kestrel.Core.Tests.csproj @@ -9,6 +9,7 @@ + From eb7835f4c83a6d7e06d9d06d0f89748aa761727a Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Wed, 11 Jul 2018 18:49:09 -0700 Subject: [PATCH 4/5] Updating dependencies to 2.1.2 and adding a section for pinned variable versions --- build/dependencies.props | 15 +++++++++++---- korebuild-lock.txt | 4 ++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 90b286111a..7a5f07df07 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,11 +3,13 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - + + + 0.10.13 - 2.1.1-rtm-15793 + 2.1.3-rtm-15802 1.10.0 - 2.1.1 + 2.1.2 2.1.1 2.1.1 2.1.1 @@ -28,7 +30,7 @@ 2.1.1 2.1.1 2.0.0 - 2.1.1 + 2.1.2 2.1.1 15.6.1 4.7.49 @@ -46,5 +48,10 @@ 2.3.1 2.4.0-beta.1.build3945 + + + + + diff --git a/korebuild-lock.txt b/korebuild-lock.txt index bc84e0cd53..251c227c83 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.1-rtm-15793 -commithash:988313f4b064d6c69fc6f7b845b6384a6af3447a +version:2.1.3-rtm-15802 +commithash:a7c08b45b440a7d2058a0aa1eaa3eb6ba811976a From 85bf01da82a2e6fd20c7dd3b9693468fcb2552e2 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Thu, 12 Jul 2018 11:54:51 -0700 Subject: [PATCH 5/5] Pin version variables to the ASP.NET Core 2.1.2 baseline This reverts our previous policy of cascading versions on all servicing updates. This moves variables into the 'pinned' section, and points them to the latest stable release (versions that were used at the time of the 2.1.2 release). --- build/dependencies.props | 49 ++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 7a5f07df07..b855ca56d0 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -5,33 +5,12 @@ - + 0.10.13 2.1.3-rtm-15802 1.10.0 - 2.1.2 - 2.1.1 - 2.1.1 - 2.1.1 - 2.1.1 - 2.1.1 - 2.1.1 - 2.1.1 - 2.1.0 - 2.1.1 - 2.1.1 - 2.1.1 - 2.1.1 - 2.1.1 - 2.1.1 - 2.1.1 - 2.1.1 - 2.1.1 - 2.1.1 - 2.1.1 2.0.0 2.1.2 - 2.1.1 15.6.1 4.7.49 2.0.3 @@ -53,5 +32,27 @@ - - + + 2.1.2 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.0 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + + \ No newline at end of file