From 128eefdef3825e1a2b8313f76b235c465b10b682 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Wed, 11 Jul 2018 22:31:23 -0700 Subject: [PATCH] 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); + } + } +}