Implement and use MultiArrayBuffer (#44980)
authorGeoff Kizer <geoffrek@microsoft.com>
Wed, 6 Jan 2021 19:06:15 +0000 (11:06 -0800)
committerGitHub <noreply@github.com>
Wed, 6 Jan 2021 19:06:15 +0000 (11:06 -0800)
* add MultiArrayBuffer and use in StreamBuffer and Http2Stream

* rework EnsureAvailableSpace and add tests for it

* make dispose handling more robust, plus minor tweaks

* add AssertExtensions.SequenceEqual for providing more useful information about sequence differences, and use in StreamConformanceTests

* tweaks

* replace _blockCount with _allocatedEnd

* use uint instead of int internally to assure bitop optimizations

* fix _allocatedEnd calculation

* address PR feedback

* more PR feedback and some minor fixes/improvements

* small test improvements

* Apply suggestions from code review

Co-authored-by: Günther Foidl <gue@korporal.at>
* address review feedback

Co-authored-by: Geoffrey Kizer <geoffrek@windows.microsoft.com>
Co-authored-by: Günther Foidl <gue@korporal.at>
24 files changed:
src/libraries/Common/src/System/Net/MultiArrayBuffer.cs [new file with mode: 0644]
src/libraries/Common/src/System/Net/StreamBuffer.cs
src/libraries/Common/tests/Common.Tests.csproj
src/libraries/Common/tests/TestUtilities/System/AssertExtensions.cs
src/libraries/Common/tests/Tests/System/IO/StreamConformanceTests.cs
src/libraries/Common/tests/Tests/System/Net/MultiArrayBufferTests.cs [new file with mode: 0644]
src/libraries/System.IO.Compression.Brotli/tests/System.IO.Compression.Brotli.Tests.csproj
src/libraries/System.IO.Compression/tests/System.IO.Compression.Tests.csproj
src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj
src/libraries/System.IO.MemoryMappedFiles/tests/System.IO.MemoryMappedFiles.Tests.csproj
src/libraries/System.IO.Pipelines/tests/System.IO.Pipelines.Tests.csproj
src/libraries/System.IO.Pipes/tests/System.IO.Pipes.Tests.csproj
src/libraries/System.IO.UnmanagedMemoryStream/tests/System.IO.UnmanagedMemoryStream.Tests.csproj
src/libraries/System.IO/tests/System.IO.Tests.csproj
src/libraries/System.Net.Http/src/System.Net.Http.csproj
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs
src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj
src/libraries/System.Net.Quic/src/System.Net.Quic.csproj
src/libraries/System.Net.Quic/tests/FunctionalTests/System.Net.Quic.Functional.Tests.csproj
src/libraries/System.Net.Security/tests/EnterpriseTests/System.Net.Security.Enterprise.Tests.csproj
src/libraries/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj
src/libraries/System.Net.Sockets/tests/FunctionalTests/System.Net.Sockets.Tests.csproj
src/libraries/System.Security.Cryptography.Primitives/tests/System.Security.Cryptography.Primitives.Tests.csproj
src/libraries/System.Text.Encoding/tests/System.Text.Encoding.Tests.csproj

diff --git a/src/libraries/Common/src/System/Net/MultiArrayBuffer.cs b/src/libraries/Common/src/System/Net/MultiArrayBuffer.cs
new file mode 100644 (file)
index 0000000..b217970
--- /dev/null
@@ -0,0 +1,427 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#nullable enable
+using System.Buffers;
+using System.Diagnostics;
+
+namespace System.Net
+{
+    // Warning: Mutable struct!
+    // The purpose of this struct is to simplify buffer management in cases where the size of the buffer may grow large (e.g. >64K),
+    // thus making it worthwhile to add the overhead involved in managing multiple individual array allocations.
+    // Like ArrayBuffer, this manages a sliding buffer where bytes can be added at the end and removed at the beginning.
+    // Unlike ArrayBuffer, the buffer itself is managed using 16K blocks which are added/removed to the block list as necessary.
+
+    // 'ActiveBuffer' contains the current buffer contents; these bytes will be preserved on any call to TryEnsureAvailableBytesUpToLimit.
+    // 'AvailableBuffer' contains the available bytes past the end of the current content,
+    // and can be written to in order to add data to the end of the buffer.
+    // Commit(byteCount) will extend the ActiveBuffer by 'byteCount' bytes into the AvailableBuffer.
+    // Discard(byteCount) will discard 'byteCount' bytes as the beginning of the ActiveBuffer.
+    // TryEnsureAvailableBytesUpToLimit will grow the buffer if necessary; *however*, this may invalidate
+    // old values of 'ActiveBuffer' and 'AvailableBuffer', so they must be retrieved again.
+
+    internal struct MultiArrayBuffer : IDisposable
+    {
+        private byte[]?[]? _blocks;
+        private uint _allocatedEnd;
+        private uint _activeStart;
+        private uint _availableStart;
+
+        // Invariants:
+        // 0 <= _activeStart <= _availableStart <= total buffer size (i.e. _blockCount * BlockSize)
+
+        private const int BlockSize = 16 * 1024;
+
+        public MultiArrayBuffer(int initialBufferSize) : this()
+        {
+            // 'initialBufferSize' is ignored for now. Some callers are passing useful info here that we might want to act on in the future.
+            Debug.Assert(initialBufferSize >= 0);
+        }
+
+        public void Dispose()
+        {
+            _activeStart = 0;
+            _availableStart = 0;
+
+            if (_blocks is not null)
+            {
+                for (int i = 0; i < _blocks.Length; i++)
+                {
+                    if (_blocks[i] is byte[] toReturn)
+                    {
+                        _blocks[i] = null;
+                        ArrayPool<byte>.Shared.Return(toReturn);
+                    }
+                }
+
+                _blocks = null;
+                _allocatedEnd = 0;
+            }
+        }
+
+        public bool IsEmpty => _activeStart == _availableStart;
+
+        public MultiMemory ActiveMemory => new MultiMemory(_blocks, _activeStart, _availableStart - _activeStart);
+
+        public MultiMemory AvailableMemory => new MultiMemory(_blocks, _availableStart, _allocatedEnd - _availableStart);
+
+        public void Discard(int byteCount)
+        {
+            Debug.Assert(byteCount >= 0);
+            Debug.Assert(byteCount <= ActiveMemory.Length, $"MultiArrayBuffer.Discard: Expected byteCount={byteCount} <= {ActiveMemory.Length}");
+
+            if (byteCount == ActiveMemory.Length)
+            {
+                DiscardAll();
+                return;
+            }
+
+            CheckState();
+
+            uint ubyteCount = (uint)byteCount;
+
+            uint oldStartBlock = _activeStart / BlockSize;
+            _activeStart += ubyteCount;
+            uint newStartBlock = _activeStart / BlockSize;
+
+            FreeBlocks(oldStartBlock, newStartBlock);
+
+            CheckState();
+        }
+
+        public void DiscardAll()
+        {
+            CheckState();
+
+            uint firstAllocatedBlock = _activeStart / BlockSize;
+            uint firstUnallocatedBlock = _allocatedEnd / BlockSize;
+            FreeBlocks(firstAllocatedBlock, firstUnallocatedBlock);
+
+            _activeStart = _availableStart = _allocatedEnd = 0;
+
+            CheckState();
+
+        }
+
+        private void FreeBlocks(uint startBlock, uint endBlock)
+        {
+            byte[]?[] blocks = _blocks!;
+            for (uint i = startBlock; i < endBlock; i++)
+            {
+                byte[]? toReturn = blocks[i];
+                Debug.Assert(toReturn is not null);
+                blocks[i] = null;
+                ArrayPool<byte>.Shared.Return(toReturn);
+            }
+        }
+
+        public void Commit(int byteCount)
+        {
+            Debug.Assert(byteCount >= 0);
+            Debug.Assert(byteCount <= AvailableMemory.Length, $"MultiArrayBuffer.Commit: Expected byteCount={byteCount} <= {AvailableMemory.Length}");
+
+            uint ubyteCount = (uint)byteCount;
+
+            _availableStart += ubyteCount;
+        }
+
+        public void EnsureAvailableSpaceUpToLimit(int byteCount, int limit)
+        {
+            Debug.Assert(byteCount >= 0);
+            Debug.Assert(limit >= 0);
+
+            if (ActiveMemory.Length >= limit)
+            {
+                // Already past limit. Do nothing.
+                return;
+            }
+
+            // Enforce the limit.
+            byteCount = Math.Min(byteCount, limit - ActiveMemory.Length);
+
+            EnsureAvailableSpace(byteCount);
+        }
+
+        public void EnsureAvailableSpace(int byteCount)
+        {
+            Debug.Assert(byteCount >= 0);
+
+            if (byteCount > AvailableMemory.Length)
+            {
+                GrowAvailableSpace(byteCount);
+            }
+        }
+
+        public void GrowAvailableSpace(int byteCount)
+        {
+            Debug.Assert(byteCount > AvailableMemory.Length);
+
+            CheckState();
+
+            uint ubyteCount = (uint)byteCount;
+
+            uint newBytesNeeded = ubyteCount - (uint)AvailableMemory.Length;
+            uint newBlocksNeeded = (newBytesNeeded + BlockSize - 1) / BlockSize;
+
+            // Ensure we have enough space in the block array for the new blocks needed.
+            if (_blocks is null)
+            {
+                Debug.Assert(_allocatedEnd == 0);
+                Debug.Assert(_activeStart == 0);
+                Debug.Assert(_availableStart == 0);
+
+                int blockArraySize = 4;
+                while (blockArraySize < newBlocksNeeded)
+                {
+                    blockArraySize *= 2;
+                }
+
+                _blocks = new byte[]?[blockArraySize];
+            }
+            else
+            {
+                Debug.Assert(_allocatedEnd % BlockSize == 0);
+                Debug.Assert(_allocatedEnd <= _blocks.Length * BlockSize);
+
+                uint allocatedBlocks = _allocatedEnd / BlockSize;
+                uint blockArraySize = (uint)_blocks.Length;
+                if (allocatedBlocks + newBlocksNeeded > blockArraySize)
+                {
+                    // Not enough room in current block array.
+                    uint unusedInitialBlocks = _activeStart / BlockSize;
+                    uint usedBlocks = (allocatedBlocks - unusedInitialBlocks);
+                    uint blocksNeeded = usedBlocks + newBlocksNeeded;
+                    if (blocksNeeded > blockArraySize)
+                    {
+                        // Need to allocate a new array and copy.
+                        while (blockArraySize < blocksNeeded)
+                        {
+                            blockArraySize *= 2;
+                        }
+
+                        byte[]?[] newBlockArray = new byte[]?[blockArraySize];
+                        _blocks.AsSpan().Slice((int)unusedInitialBlocks, (int)usedBlocks).CopyTo(newBlockArray);
+                        _blocks = newBlockArray;
+                    }
+                    else
+                    {
+                        // We can shift the array down to make enough space
+                        _blocks.AsSpan().Slice((int)unusedInitialBlocks, (int)usedBlocks).CopyTo(_blocks);
+
+                        // Null out the part of the array left over from the shift, so that we aren't holding references to those blocks.
+                        _blocks.AsSpan().Slice((int)usedBlocks, (int)unusedInitialBlocks).Fill(null);
+                    }
+
+                    uint shift = unusedInitialBlocks * BlockSize;
+                    _allocatedEnd -= shift;
+                    _activeStart -= shift;
+                    _availableStart -= shift;
+
+                    Debug.Assert(_activeStart / BlockSize == 0, $"Start is not in first block after move or resize?? _activeStart={_activeStart}");
+                }
+            }
+
+            // Allocate new blocks
+            Debug.Assert(_allocatedEnd % BlockSize == 0);
+            uint allocatedBlockCount = _allocatedEnd / BlockSize;
+            Debug.Assert(allocatedBlockCount == 0 || _blocks[allocatedBlockCount - 1] is not null);
+            for (uint i = 0; i < newBlocksNeeded; i++)
+            {
+                Debug.Assert(_blocks[allocatedBlockCount] is null);
+                _blocks[allocatedBlockCount++] = ArrayPool<byte>.Shared.Rent(BlockSize);
+            }
+
+            _allocatedEnd = allocatedBlockCount * BlockSize;
+
+            // After all of that, we should have enough available memory now
+            Debug.Assert(byteCount <= AvailableMemory.Length);
+
+            CheckState();
+        }
+
+        [Conditional("DEBUG")]
+        private void CheckState()
+        {
+            if (_blocks == null)
+            {
+                Debug.Assert(_activeStart == 0);
+                Debug.Assert(_availableStart == 0);
+                Debug.Assert(_allocatedEnd == 0);
+            }
+            else
+            {
+                Debug.Assert(_activeStart <= _availableStart);
+                Debug.Assert(_availableStart <= _allocatedEnd);
+                Debug.Assert(_allocatedEnd <= _blocks.Length * BlockSize);
+
+                Debug.Assert(_allocatedEnd % BlockSize == 0, $"_allocatedEnd={_allocatedEnd} not at block boundary?");
+
+                uint firstAllocatedBlock = _activeStart / BlockSize;
+                uint firstUnallocatedBlock = _allocatedEnd / BlockSize;
+
+                for (uint i = 0; i < firstAllocatedBlock; i++)
+                {
+                    Debug.Assert(_blocks[i] is null);
+                }
+
+                for (uint i = firstAllocatedBlock; i < firstUnallocatedBlock; i++)
+                {
+                    Debug.Assert(_blocks[i] is not null);
+                }
+
+                for (uint i = firstUnallocatedBlock; i < _blocks.Length; i++)
+                {
+                    Debug.Assert(_blocks[i] is null);
+                }
+
+                if (_activeStart == _availableStart)
+                {
+                    Debug.Assert(_activeStart == 0, $"No active bytes but _activeStart={_activeStart}");
+                }
+            }
+        }
+    }
+
+    // This is a Memory-like struct for handling multi-array segments from MultiArrayBuffer above.
+    // It supports standard Span/Memory operations like indexing, Slice, Length, etc
+    // It also supports CopyTo/CopyFrom Span<byte>
+
+    internal readonly struct MultiMemory
+    {
+        private readonly byte[]?[]? _blocks;
+        private readonly uint _start;
+        private readonly uint _length;
+
+        private const int BlockSize = 16 * 1024;
+
+        internal MultiMemory(byte[]?[]? blocks, uint start, uint length)
+        {
+            if (length == 0)
+            {
+                _blocks = null;
+                _start = 0;
+                _length = 0;
+            }
+            else
+            {
+                Debug.Assert(blocks is not null);
+                Debug.Assert(start <= int.MaxValue);
+                Debug.Assert(length <= int.MaxValue);
+                Debug.Assert(start + length <= blocks.Length * BlockSize);
+
+                _blocks = blocks;
+                _start = start;
+                _length = length;
+            }
+        }
+
+        private static uint GetBlockIndex(uint offset) => offset / BlockSize;
+        private static uint GetOffsetInBlock(uint offset) => offset % BlockSize;
+
+        public bool IsEmpty => _length == 0;
+
+        public int Length => (int)_length;
+
+        public ref byte this[int index]
+        {
+            get
+            {
+                uint uindex = (uint)index;
+                if (uindex >= _length)
+                {
+                    throw new IndexOutOfRangeException();
+                }
+
+                uint offset = _start + uindex;
+                return ref _blocks![GetBlockIndex(offset)]![GetOffsetInBlock(offset)];
+            }
+        }
+
+        public int BlockCount => (int)(GetBlockIndex(_start + _length + (BlockSize - 1)) - GetBlockIndex(_start));
+
+        public Memory<byte> GetBlock(int blockIndex)
+        {
+            if ((uint)blockIndex >= BlockCount)
+            {
+                throw new IndexOutOfRangeException();
+            }
+
+            Debug.Assert(_length > 0, "Length should never be 0 here because BlockCount would be 0");
+            Debug.Assert(_blocks is not null);
+
+            uint startInBlock = (blockIndex == 0 ? GetOffsetInBlock(_start) : 0);
+            uint endInBlock = (blockIndex == BlockCount - 1 ? GetOffsetInBlock(_start + _length - 1) + 1 : BlockSize);
+
+            Debug.Assert(0 <= startInBlock, $"Invalid startInBlock={startInBlock}. blockIndex={blockIndex}, _blocks.Length={_blocks.Length}, _start={_start}, _length={_length}");
+            Debug.Assert(startInBlock < endInBlock, $"Invalid startInBlock={startInBlock}, endInBlock={endInBlock}. blockIndex={blockIndex}, _blocks.Length={_blocks.Length}, _start={_start}, _length={_length}");
+            Debug.Assert(endInBlock <= BlockSize, $"Invalid endInBlock={endInBlock}. blockIndex={blockIndex}, _blocks.Length={_blocks.Length}, _start={_start}, _length={_length}");
+
+            return new Memory<byte>(_blocks[GetBlockIndex(_start) + blockIndex], (int)startInBlock, (int)(endInBlock - startInBlock));
+        }
+
+        public MultiMemory Slice(int start)
+        {
+            uint ustart = (uint)start;
+            if (ustart > _length)
+            {
+                throw new IndexOutOfRangeException();
+            }
+
+            return new MultiMemory(_blocks, _start + ustart, _length - ustart);
+        }
+
+        public MultiMemory Slice(int start, int length)
+        {
+            uint ustart = (uint)start;
+            uint ulength = (uint)length;
+            if (ustart > _length || ulength > _length - ustart)
+            {
+                throw new IndexOutOfRangeException();
+            }
+
+            return new MultiMemory(_blocks, _start + ustart, ulength);
+        }
+
+        public void CopyTo(Span<byte> destination)
+        {
+            if (destination.Length < _length)
+            {
+                throw new ArgumentOutOfRangeException(nameof(destination));
+            }
+
+            int blockCount = BlockCount;
+            for (int blockIndex = 0; blockIndex < blockCount; blockIndex++)
+            {
+                Memory<byte> block = GetBlock(blockIndex);
+                block.Span.CopyTo(destination);
+                destination = destination.Slice(block.Length);
+            }
+        }
+
+        public void CopyFrom(ReadOnlySpan<byte> source)
+        {
+            if (_length < source.Length)
+            {
+                throw new ArgumentOutOfRangeException(nameof(source));
+            }
+
+            int blockCount = BlockCount;
+            for (int blockIndex = 0; blockIndex < blockCount; blockIndex++)
+            {
+                Memory<byte> block = GetBlock(blockIndex);
+
+                if (source.Length <= block.Length)
+                {
+                    source.CopyTo(block.Span);
+                    break;
+                }
+
+                source.Slice(0, block.Length).CopyTo(block.Span);
+                source = source.Slice(block.Length);
+            }
+        }
+
+        public static MultiMemory Empty => default;
+    }
+}
index 09845cd..bdd1ff2 100644 (file)
@@ -13,7 +13,7 @@ namespace System.IO
 {
     internal sealed class StreamBuffer : IDisposable
     {
-        private ArrayBuffer _buffer; // mutable struct, do not make this readonly
+        private MultiArrayBuffer _buffer; // mutable struct, do not make this readonly
         private readonly int _maxBufferSize;
         private bool _writeEnded;
         private bool _readAborted;
@@ -25,7 +25,7 @@ namespace System.IO
 
         public StreamBuffer(int initialBufferSize = DefaultInitialBufferSize, int maxBufferSize = DefaultMaxBufferSize)
         {
-            _buffer = new ArrayBuffer(initialBufferSize, usePool: true);
+            _buffer = new MultiArrayBuffer(initialBufferSize);
             _maxBufferSize = maxBufferSize;
             _readTaskSource = new ResettableValueTaskSource();
             _writeTaskSource = new ResettableValueTaskSource();
@@ -40,7 +40,7 @@ namespace System.IO
                 Debug.Assert(!Monitor.IsEntered(SyncObject));
                 lock (SyncObject)
                 {
-                    return (_writeEnded && _buffer.ActiveLength == 0);
+                    return (_writeEnded && _buffer.IsEmpty);
                 }
             }
         }
@@ -69,7 +69,7 @@ namespace System.IO
                         return 0;
                     }
 
-                    return _buffer.ActiveLength;
+                    return _buffer.ActiveMemory.Length;
                 }
             }
         }
@@ -86,7 +86,7 @@ namespace System.IO
                         throw new InvalidOperationException();
                     }
 
-                    return _maxBufferSize - _buffer.ActiveLength;
+                    return _maxBufferSize - _buffer.ActiveMemory.Length;
                 }
             }
         }
@@ -108,12 +108,12 @@ namespace System.IO
                     return (false, buffer.Length);
                 }
 
-                _buffer.TryEnsureAvailableSpaceUpToLimit(buffer.Length, _maxBufferSize);
+                _buffer.EnsureAvailableSpaceUpToLimit(buffer.Length, _maxBufferSize);
 
-                int bytesWritten = Math.Min(buffer.Length, _buffer.AvailableLength);
+                int bytesWritten = Math.Min(buffer.Length, _buffer.AvailableMemory.Length);
                 if (bytesWritten > 0)
                 {
-                    buffer.Slice(0, bytesWritten).CopyTo(_buffer.AvailableSpan);
+                    _buffer.AvailableMemory.CopyFrom(buffer.Slice(0, bytesWritten));
                     _buffer.Commit(bytesWritten);
 
                     _readTaskSource.SignalWaiter();
@@ -203,10 +203,10 @@ namespace System.IO
                     return (false, 0);
                 }
 
-                if (_buffer.ActiveLength > 0)
+                if (!_buffer.IsEmpty)
                 {
-                    int bytesRead = Math.Min(buffer.Length, _buffer.ActiveLength);
-                    _buffer.ActiveSpan.Slice(0, bytesRead).CopyTo(buffer);
+                    int bytesRead = Math.Min(buffer.Length, _buffer.ActiveMemory.Length);
+                    _buffer.ActiveMemory.Slice(0, bytesRead).CopyTo(buffer);
                     _buffer.Discard(bytesRead);
 
                     _writeTaskSource.SignalWaiter();
@@ -277,10 +277,7 @@ namespace System.IO
                 }
 
                 _readAborted = true;
-                if (_buffer.ActiveLength != 0)
-                {
-                    _buffer.Discard(_buffer.ActiveLength);
-                }
+                _buffer.DiscardAll();
 
                 _readTaskSource.SignalWaiter();
                 _writeTaskSource.SignalWaiter();
@@ -292,7 +289,10 @@ namespace System.IO
             AbortRead();
             EndWrite();
 
-            _buffer.Dispose();
+            lock (SyncObject)
+            {
+                _buffer.Dispose();
+            }
         }
 
         private sealed class ResettableValueTaskSource : IValueTaskSource
index 9dd6f6f..a71e031 100644 (file)
@@ -90,6 +90,7 @@
     <Compile Include="Tests\System\Collections\Generic\LargeArrayBuilderTests.cs" />
     <Compile Include="Tests\System\IO\RowConfigReaderTests.cs" />
     <Compile Include="Tests\System\Net\HttpKnownHeaderNamesTests.cs" />
+    <Compile Include="Tests\System\Net\MultiArrayBufferTests.cs" />
     <Compile Include="Tests\System\Net\aspnetcore\Http2\DynamicTableTest.cs" />
     <Compile Include="Tests\System\Net\aspnetcore\Http2\HPackDecoderTest.cs" />
     <Compile Include="Tests\System\Net\aspnetcore\Http2\HPackIntegerTest.cs" />
     <Compile Include="Tests\System\IO\ConnectedStreamsTests.cs" />
     <Compile Include="Tests\System\IO\StreamConformanceTests.cs" />
     <Compile Include="$(CommonPath)System\Net\ArrayBuffer.cs" Link="Common\System\Net\ArrayBuffer.cs" />
+    <Compile Include="$(CommonPath)System\Net\MultiArrayBuffer.cs" Link="Common\System\Net\MultiArrayBuffer.cs" />
     <Compile Include="$(CommonPath)System\Net\StreamBuffer.cs" Link="Common\System\Net\StreamBuffer.cs" />
     <Compile Include="$(CommonTestPath)System\IO\CallTrackingStream.cs" Link="Common\System\IO\CallTrackingStream.cs" />
   </ItemGroup>
index 2b0d037..940c4de 100644 (file)
@@ -351,6 +351,8 @@ namespace System
                 throw new XunitException(AddOptionalUserMessage($"Expected: {actual} to be greater than or equal to {greaterThanOrEqualTo}", userMessage));
         }
 
+        // NOTE: Consider using SequenceEqual below instead, as it will give more useful information about what
+        // the actual differences are, especially for large arrays/spans.
         /// <summary>
         /// Validates that the actual array is equal to the expected array. XUnit only displays the first 5 values
         /// of each collection if the test fails. This doesn't display at what point or how the equality assertion failed.
@@ -430,6 +432,53 @@ namespace System
             }
         }
                
+        /// <summary>
+        /// Validates that the actual span is equal to the expected span.
+        /// If this fails, determine where the differences are and create an exception with that information.
+        /// </summary>
+        /// <param name="expected">The array that <paramref name="actual"/> should be equal to.</param>
+        /// <param name="actual"></param>
+        public static void SequenceEqual<T>(ReadOnlySpan<T> expected, ReadOnlySpan<T> actual) where T : IEquatable<T>
+        {
+            // Use the SequenceEqual to compare the arrays for better performance. The default Assert.Equal method compares
+            // the arrays by boxing each element that is very slow for large arrays.
+            if (!expected.SequenceEqual(actual))
+            {
+                if (expected.Length != actual.Length)
+                {
+                    throw new XunitException($"Expected: Span of length {expected.Length}{Environment.NewLine}Actual: Span of length {actual.Length}");
+                }
+                else
+                {
+                    const int MaxDiffsToShow = 10;      // arbitrary; enough to be useful, hopefully, but still manageable
+
+                    int diffCount = 0;
+                    string message = $"Showing first {MaxDiffsToShow} differences{Environment.NewLine}";
+                    for (int i = 0; i < expected.Length; i++)
+                    {
+                        if (!expected[i].Equals(actual[i]))
+                        {
+                            diffCount++;
+
+                            // Add up to 10 differences to the exception message
+                            if (diffCount <= MaxDiffsToShow)
+                            {
+                                message += $"  Position {i}: Expected: {expected[i]}, Actual: {actual[i]}{Environment.NewLine}";
+                            }
+                        }
+                    }
+
+                    message += $"Total number of differences: {diffCount} out of {expected.Length}";
+
+                    throw new XunitException(message);
+                }
+            }
+        }
+
+        public static void SequenceEqual<T>(Span<T> expected, Span<T> actual) where T : IEquatable<T> => SequenceEqual((ReadOnlySpan<T>)expected, (ReadOnlySpan<T>)actual);
+
+        public static void SequenceEqual<T>(T[] expected, T[] actual) where T : IEquatable<T> => SequenceEqual(expected.AsSpan(), actual.AsSpan());
+
         public static void AtLeastOneEquals<T>(T expected1, T expected2, T value)
         {
             EqualityComparer<T> comparer = EqualityComparer<T>.Default;
index 68f5b3e..947c0b8 100644 (file)
@@ -905,7 +905,7 @@ namespace System.IO.Tests
                 Assert.Equal(size, stream.Seek(0, SeekOrigin.Current));
             }
 
-            AssertExtensions.Equal(expected, actual.ToArray());
+            AssertExtensions.SequenceEqual(expected, actual.ToArray());
         }
 
         [Theory]
@@ -990,7 +990,7 @@ namespace System.IO.Tests
             stream.Position = 0;
             byte[] actual = (byte[])expected.Clone();
             Assert.Equal(actual.Length, await ReadAllAsync(ReadWriteMode.AsyncMemory, stream, actual, 0, actual.Length));
-            AssertExtensions.Equal(expected, actual);
+            AssertExtensions.SequenceEqual(expected, actual);
         }
 
         [Theory]
@@ -1032,7 +1032,7 @@ namespace System.IO.Tests
                 Assert.Equal(expected.Length, stream.Position);
             }
 
-            AssertExtensions.Equal(expected.AsSpan(position).ToArray(), destination.ToArray());
+            AssertExtensions.SequenceEqual(expected.AsSpan(position).ToArray(), destination.ToArray());
         }
 
         public static IEnumerable<object[]> CopyTo_CopiesAllDataFromRightPosition_Success_MemberData()
@@ -1073,7 +1073,7 @@ namespace System.IO.Tests
             for (int i = 0; i < Copies; i++)
             {
                 int bytesRead = await ReadAllAsync(mode, stream, actual, 0, actual.Length);
-                AssertExtensions.Equal(expected, actual);
+                AssertExtensions.SequenceEqual(expected, actual);
                 Array.Clear(actual, 0, actual.Length);
             }
         }
@@ -1686,7 +1686,7 @@ namespace System.IO.Tests
                     readerBytes[i] = (byte)r;
                 }
 
-                AssertExtensions.Equal(writerBytes, readerBytes);
+                AssertExtensions.SequenceEqual(writerBytes, readerBytes);
 
                 await writes;
 
@@ -1760,7 +1760,7 @@ namespace System.IO.Tests
                     }
 
                     Assert.Equal(readerBytes.Length, n);
-                    AssertExtensions.Equal(writerBytes, readerBytes);
+                    AssertExtensions.SequenceEqual(writerBytes, readerBytes);
 
                     await writes;
 
@@ -2369,7 +2369,7 @@ namespace System.IO.Tests
             writeable.Dispose();
             await copyTask;
 
-            AssertExtensions.Equal(dataToCopy, results.ToArray());
+            AssertExtensions.SequenceEqual(dataToCopy, results.ToArray());
         }
 
         [OuterLoop("May take several seconds")]
diff --git a/src/libraries/Common/tests/Tests/System/Net/MultiArrayBufferTests.cs b/src/libraries/Common/tests/Tests/System/Net/MultiArrayBufferTests.cs
new file mode 100644 (file)
index 0000000..1459e9c
--- /dev/null
@@ -0,0 +1,482 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Linq;
+using Xunit;
+
+namespace Tests.System.Net
+{
+    public sealed class MultiArrayBufferTests
+    {
+        const int BlockSize = 16 * 1024;
+
+        [Fact]
+        public void BasicTest()
+        {
+            MultiArrayBuffer buffer = new MultiArrayBuffer(0);
+
+            Assert.True(buffer.IsEmpty);
+            Assert.True(buffer.ActiveMemory.IsEmpty);
+            Assert.Equal(0, buffer.ActiveMemory.Length);
+            Assert.Equal(0, buffer.ActiveMemory.BlockCount);
+            Assert.True(buffer.AvailableMemory.IsEmpty);
+            Assert.Equal(0, buffer.AvailableMemory.Length);
+            Assert.Equal(0, buffer.AvailableMemory.BlockCount);
+
+            buffer.EnsureAvailableSpace(3);
+
+            Assert.True(buffer.IsEmpty);
+            Assert.True(buffer.ActiveMemory.IsEmpty);
+            Assert.Equal(0, buffer.ActiveMemory.Length);
+            Assert.Equal(0, buffer.ActiveMemory.BlockCount);
+            Assert.False(buffer.AvailableMemory.IsEmpty);
+            Assert.NotEqual(0, buffer.AvailableMemory.Length);
+            Assert.NotEqual(0, buffer.AvailableMemory.BlockCount);
+
+            int available = buffer.AvailableMemory.Length;
+            Assert.True(available >= 3);
+
+            buffer.AvailableMemory[0] = 10;
+            buffer.Commit(1);
+
+            Assert.False(buffer.IsEmpty);
+            Assert.False(buffer.ActiveMemory.IsEmpty);
+            Assert.Equal(1, buffer.ActiveMemory.Length);
+            Assert.Equal(10, buffer.ActiveMemory[0]);
+            Assert.Equal(available - 1, buffer.AvailableMemory.Length);
+            Assert.Equal(1, buffer.ActiveMemory.BlockCount);
+            Assert.Equal(10, buffer.ActiveMemory.GetBlock(0).Span[0]);
+
+            buffer.AvailableMemory[0] = 20;
+            buffer.Commit(1);
+
+            Assert.False(buffer.IsEmpty);
+            Assert.False(buffer.ActiveMemory.IsEmpty);
+            Assert.Equal(2, buffer.ActiveMemory.Length);
+            Assert.Equal(20, buffer.ActiveMemory[1]);
+            Assert.Equal(available - 2, buffer.AvailableMemory.Length);
+            Assert.InRange(buffer.ActiveMemory.BlockCount, 1, 2);
+
+            buffer.AvailableMemory[0] = 30;
+            buffer.Commit(1);
+
+            Assert.False(buffer.IsEmpty);
+            Assert.False(buffer.ActiveMemory.IsEmpty);
+            Assert.Equal(3, buffer.ActiveMemory.Length);
+            Assert.Equal(30, buffer.ActiveMemory[2]);
+            Assert.Equal(available - 3, buffer.AvailableMemory.Length);
+            Assert.InRange(buffer.ActiveMemory.BlockCount, 1, 2);
+
+            buffer.Discard(1);
+            Assert.False(buffer.IsEmpty);
+            Assert.False(buffer.ActiveMemory.IsEmpty);
+            Assert.Equal(2, buffer.ActiveMemory.Length);
+            Assert.Equal(20, buffer.ActiveMemory[0]);
+            Assert.InRange(buffer.ActiveMemory.BlockCount, 1, 2);
+            Assert.Equal(20, buffer.ActiveMemory.GetBlock(0).Span[0]);
+
+            buffer.Discard(1);
+            Assert.False(buffer.IsEmpty);
+            Assert.False(buffer.ActiveMemory.IsEmpty);
+            Assert.Equal(1, buffer.ActiveMemory.Length);
+            Assert.Equal(30, buffer.ActiveMemory[0]);
+            Assert.Equal(1, buffer.ActiveMemory.BlockCount);
+            Assert.Equal(30, buffer.ActiveMemory.GetBlock(0).Span[0]);
+
+            buffer.Discard(1);
+            Assert.True(buffer.IsEmpty);
+            Assert.True(buffer.ActiveMemory.IsEmpty);
+            Assert.Equal(0, buffer.ActiveMemory.Length);
+            Assert.Equal(0, buffer.ActiveMemory.BlockCount);
+        }
+
+        [Fact]
+        public void AddByteByByteAndConsumeByteByByte_Success()
+        {
+            const int Size = 64 * 1024 + 1;
+
+            MultiArrayBuffer buffer = new MultiArrayBuffer(0);
+
+            for (int i = 0; i < Size; i++)
+            {
+                buffer.EnsureAvailableSpace(1);
+                buffer.AvailableMemory[0] = (byte)i;
+                buffer.Commit(1);
+            }
+
+            for (int i = 0; i < Size; i++)
+            {
+                Assert.Equal((byte)i, buffer.ActiveMemory[0]);
+                buffer.Discard(1);
+            }
+
+            Assert.Equal(0, buffer.ActiveMemory.Length);
+            Assert.True(buffer.IsEmpty);
+            Assert.True(buffer.ActiveMemory.IsEmpty);
+        }
+
+        [Fact]
+        public void AddSeveralBytesRepeatedlyAndConsumeSeveralBytesRepeatedly_Success()
+        {
+            const int ByteCount = 7;
+            const int RepeatCount = 8 * 1024;       // enough to ensure we cross several block boundaries
+
+            MultiArrayBuffer buffer = new MultiArrayBuffer(0);
+
+            for (int i = 0; i < RepeatCount; i++)
+            {
+                buffer.EnsureAvailableSpace(ByteCount);
+                for (int j = 0; j < ByteCount; j++)
+                {
+                    buffer.AvailableMemory[j] = (byte)(j + 1);
+                }
+                buffer.Commit(ByteCount);
+            }
+
+            for (int i = 0; i < RepeatCount; i++)
+            {
+                for (int j = 0; j < ByteCount; j++)
+                {
+                    Assert.Equal(j + 1, buffer.ActiveMemory[j]);
+                }
+                buffer.Discard(ByteCount);
+            }
+
+            Assert.Equal(0, buffer.ActiveMemory.Length);
+            Assert.True(buffer.IsEmpty);
+            Assert.True(buffer.ActiveMemory.IsEmpty);
+        }
+
+        [Fact]
+        public void AddSeveralBytesRepeatedlyAndConsumeSeveralBytesRepeatedly_UsingSlice_Success()
+        {
+            const int ByteCount = 7;
+            const int RepeatCount = 8 * 1024;       // enough to ensure we cross several block boundaries
+
+            MultiArrayBuffer buffer = new MultiArrayBuffer(0);
+
+            for (int i = 0; i < RepeatCount; i++)
+            {
+                buffer.EnsureAvailableSpace(ByteCount);
+                for (int j = 0; j < ByteCount; j++)
+                {
+                    buffer.AvailableMemory.Slice(j)[0] = (byte)(j + 1);
+                }
+                buffer.Commit(ByteCount);
+            }
+
+            for (int i = 0; i < RepeatCount; i++)
+            {
+                for (int j = 0; j < ByteCount; j++)
+                {
+                    Assert.Equal(j + 1, buffer.ActiveMemory.Slice(j)[0]);
+                }
+                buffer.Discard(ByteCount);
+            }
+
+            Assert.Equal(0, buffer.ActiveMemory.Length);
+            Assert.True(buffer.IsEmpty);
+            Assert.True(buffer.ActiveMemory.IsEmpty);
+        }
+
+        [Fact]
+        public void AddSeveralBytesRepeatedlyAndConsumeSeveralBytesRepeatedly_UsingSliceWithLength_Success()
+        {
+            const int ByteCount = 7;
+            const int RepeatCount = 8 * 1024;       // enough to ensure we cross several block boundaries
+
+            MultiArrayBuffer buffer = new MultiArrayBuffer(0);
+
+            for (int i = 0; i < RepeatCount; i++)
+            {
+                buffer.EnsureAvailableSpace(ByteCount);
+                for (int j = 0; j < ByteCount; j++)
+                {
+                    buffer.AvailableMemory.Slice(j, ByteCount - j)[0] = (byte)(j + 1);
+                }
+                buffer.Commit(ByteCount);
+            }
+
+            for (int i = 0; i < RepeatCount; i++)
+            {
+                for (int j = 0; j < ByteCount; j++)
+                {
+                    Assert.Equal(j + 1, buffer.ActiveMemory.Slice(j, ByteCount - j)[0]);
+                }
+                buffer.Discard(ByteCount);
+            }
+
+            Assert.Equal(0, buffer.ActiveMemory.Length);
+            Assert.True(buffer.IsEmpty);
+            Assert.True(buffer.ActiveMemory.IsEmpty);
+        }
+
+        [Fact]
+        public void CopyFromRepeatedlyAndCopyToRepeatedly_Success()
+        {
+            ReadOnlySpan<byte> source = new byte[] { 1, 2, 3, 4, 5, 6, 7 }.AsSpan();
+
+            const int RepeatCount = 8 * 1024;       // enough to ensure we cross several block boundaries
+
+            MultiArrayBuffer buffer = new MultiArrayBuffer(0);
+
+            for (int i = 0; i < RepeatCount; i++)
+            {
+                buffer.EnsureAvailableSpace(source.Length);
+                buffer.AvailableMemory.CopyFrom(source);
+                buffer.Commit(source.Length);
+            }
+
+            Span<byte> destination = new byte[source.Length].AsSpan();
+            for (int i = 0; i < RepeatCount; i++)
+            {
+                buffer.ActiveMemory.Slice(0, source.Length).CopyTo(destination);
+                Assert.True(source.SequenceEqual(destination));
+                buffer.Discard(source.Length);
+            }
+
+            Assert.Equal(0, buffer.ActiveMemory.Length);
+            Assert.True(buffer.IsEmpty);
+            Assert.True(buffer.ActiveMemory.IsEmpty);
+        }
+
+        [Fact]
+        public void CopyFromRepeatedlyAndCopyToRepeatedly_LargeCopies_Success()
+        {
+            ReadOnlySpan<byte> source = Enumerable.Range(0, 64 * 1024 - 1).Select(x => (byte)x).ToArray().AsSpan();
+
+            const int RepeatCount = 13;
+
+            MultiArrayBuffer buffer = new MultiArrayBuffer(0);
+
+            for (int i = 0; i < RepeatCount; i++)
+            {
+                buffer.EnsureAvailableSpace(source.Length);
+                buffer.AvailableMemory.CopyFrom(source);
+                buffer.Commit(source.Length);
+            }
+
+            Span<byte> destination = new byte[source.Length].AsSpan();
+            for (int i = 0; i < RepeatCount; i++)
+            {
+                buffer.ActiveMemory.Slice(0, source.Length).CopyTo(destination);
+                Assert.True(source.SequenceEqual(destination));
+                buffer.Discard(source.Length);
+            }
+
+            Assert.Equal(0, buffer.ActiveMemory.Length);
+            Assert.True(buffer.IsEmpty);
+            Assert.True(buffer.ActiveMemory.IsEmpty);
+        }
+
+        [Fact]
+        public void EmptyMultiMemoryTest()
+        {
+            MultiMemory mm = MultiMemory.Empty;
+
+            Assert.Equal(0, mm.Length);
+            Assert.True(mm.IsEmpty);
+            Assert.Equal(0, mm.BlockCount);
+            Assert.Equal(0, mm.Slice(0).Length);
+            Assert.Equal(0, mm.Slice(0, 0).Length);
+
+            // These should not throw
+            mm.CopyTo(new byte[0]);
+            mm.CopyFrom(new byte[0]);
+        }
+
+        [Fact]
+        public void EnsureAvailableSpaceTest()
+        {
+            MultiArrayBuffer buffer = new MultiArrayBuffer(0);
+
+            Assert.Equal(0, buffer.ActiveMemory.Length);
+            Assert.Equal(0, buffer.AvailableMemory.Length);
+
+            buffer.EnsureAvailableSpace(0);
+            Assert.Equal(0, buffer.AvailableMemory.Length);
+
+            buffer.EnsureAvailableSpace(1);
+            Assert.Equal(BlockSize, buffer.AvailableMemory.Length);
+
+            buffer.EnsureAvailableSpace(2);
+            Assert.Equal(BlockSize, buffer.AvailableMemory.Length);
+
+            buffer.EnsureAvailableSpace(BlockSize - 1);
+            Assert.Equal(BlockSize, buffer.AvailableMemory.Length);
+
+            buffer.EnsureAvailableSpace(BlockSize);
+            Assert.Equal(BlockSize, buffer.AvailableMemory.Length);
+
+            buffer.EnsureAvailableSpace(BlockSize + 1);
+            Assert.Equal(BlockSize * 2, buffer.AvailableMemory.Length);
+
+            buffer.Commit(BlockSize - 1);
+            Assert.Equal(BlockSize - 1, buffer.ActiveMemory.Length);
+            Assert.Equal(BlockSize + 1, buffer.AvailableMemory.Length);
+
+            buffer.Commit(BlockSize);
+            Assert.Equal(BlockSize * 2 - 1, buffer.ActiveMemory.Length);
+            Assert.Equal(1, buffer.AvailableMemory.Length);
+
+            buffer.EnsureAvailableSpace(0);
+            Assert.Equal(1, buffer.AvailableMemory.Length);
+
+            buffer.EnsureAvailableSpace(1);
+            Assert.Equal(1, buffer.AvailableMemory.Length);
+
+            buffer.EnsureAvailableSpace(2);
+            Assert.Equal(BlockSize + 1, buffer.AvailableMemory.Length);
+
+            buffer.Commit(2);
+            Assert.Equal(BlockSize * 2 + 1, buffer.ActiveMemory.Length);
+            Assert.Equal(BlockSize - 1, buffer.AvailableMemory.Length);
+
+            buffer.Discard(1);
+            Assert.Equal(BlockSize * 2, buffer.ActiveMemory.Length);
+            Assert.Equal(BlockSize - 1, buffer.AvailableMemory.Length);
+
+            buffer.Discard(1);
+            Assert.Equal(BlockSize * 2 - 1, buffer.ActiveMemory.Length);
+            Assert.Equal(BlockSize - 1, buffer.AvailableMemory.Length);
+
+            // This should not free the first block
+            buffer.Discard(BlockSize - 3);
+            Assert.Equal(BlockSize + 2, buffer.ActiveMemory.Length);
+            Assert.Equal(BlockSize - 1, buffer.AvailableMemory.Length);
+
+            // This should free the first block
+            buffer.Discard(1);
+            Assert.Equal(BlockSize + 1, buffer.ActiveMemory.Length);
+            Assert.Equal(BlockSize - 1, buffer.AvailableMemory.Length);
+
+            buffer.EnsureAvailableSpace(BlockSize - 1);
+            Assert.Equal(BlockSize - 1, buffer.AvailableMemory.Length);
+
+            buffer.EnsureAvailableSpace(BlockSize);
+            Assert.Equal(BlockSize * 2 - 1, buffer.AvailableMemory.Length);
+
+            buffer.Discard(BlockSize - 1);
+            Assert.Equal(2, buffer.ActiveMemory.Length);
+            Assert.Equal(BlockSize * 2 - 1, buffer.AvailableMemory.Length);
+
+            // This will cause shifting the block array down, but not reallocating
+            buffer.EnsureAvailableSpace(BlockSize * 2);
+            Assert.Equal(BlockSize * 3 - 1, buffer.AvailableMemory.Length);
+
+            buffer.Commit(BlockSize - 2);
+            Assert.Equal(BlockSize, buffer.ActiveMemory.Length);
+            Assert.Equal(BlockSize * 2 + 1, buffer.AvailableMemory.Length);
+
+            buffer.Commit(1);
+            Assert.Equal(BlockSize + 1, buffer.ActiveMemory.Length);
+            Assert.Equal(BlockSize * 2, buffer.AvailableMemory.Length);
+
+            buffer.Commit(1);
+            Assert.Equal(BlockSize + 2, buffer.ActiveMemory.Length);
+            Assert.Equal(BlockSize * 2 - 1, buffer.AvailableMemory.Length);
+
+            buffer.Discard(1);
+            Assert.Equal(BlockSize + 1, buffer.ActiveMemory.Length);
+            Assert.Equal(BlockSize * 2 - 1, buffer.AvailableMemory.Length);
+
+            // This will cause reallocating the block array, and dealing with an unused block in the first slot
+            buffer.EnsureAvailableSpace(BlockSize * 4);
+            Assert.Equal(BlockSize + 1, buffer.ActiveMemory.Length);
+            Assert.Equal(BlockSize * 5 - 1, buffer.AvailableMemory.Length);
+
+            buffer.Discard(2);
+            Assert.Equal(BlockSize - 1, buffer.ActiveMemory.Length);
+            Assert.Equal(BlockSize * 5 - 1, buffer.AvailableMemory.Length);
+
+            buffer.Commit(1);
+            Assert.Equal(BlockSize, buffer.ActiveMemory.Length);
+            Assert.Equal(BlockSize * 5 - 2, buffer.AvailableMemory.Length);
+
+            // This will discard all active bytes, which will reset the buffer
+            buffer.Discard(BlockSize);
+            Assert.Equal(0, buffer.ActiveMemory.Length);
+            Assert.Equal(0, buffer.AvailableMemory.Length);
+
+            buffer.EnsureAvailableSpace(2);
+            buffer.Commit(2);
+            Assert.Equal(2, buffer.ActiveMemory.Length);
+            Assert.Equal(BlockSize - 2, buffer.AvailableMemory.Length);
+
+            buffer.Discard(1);
+            Assert.Equal(1, buffer.ActiveMemory.Length);
+            Assert.Equal(BlockSize - 2, buffer.AvailableMemory.Length);
+
+            // Request a very large amount of available space.
+            buffer.EnsureAvailableSpace(BlockSize * 64 + 1);
+            Assert.Equal(1, buffer.ActiveMemory.Length);
+            Assert.Equal(BlockSize * 65 - 2, buffer.AvailableMemory.Length);
+
+            buffer.DiscardAll();
+            Assert.Equal(0, buffer.ActiveMemory.Length);
+            Assert.Equal(0, buffer.AvailableMemory.Length);
+        }
+
+        [Fact]
+        public void EnsureAvailableSpaceUpToLimitTest()
+        {
+            MultiArrayBuffer buffer = new MultiArrayBuffer(0);
+
+            Assert.Equal(0, buffer.ActiveMemory.Length);
+            Assert.Equal(0, buffer.AvailableMemory.Length);
+
+            buffer.EnsureAvailableSpaceUpToLimit(0, 0);
+            Assert.Equal(0, buffer.AvailableMemory.Length);
+
+            buffer.EnsureAvailableSpaceUpToLimit(0, 1);
+            Assert.Equal(0, buffer.AvailableMemory.Length);
+
+            buffer.EnsureAvailableSpaceUpToLimit(1, 0);
+            Assert.Equal(0, buffer.AvailableMemory.Length);
+
+            buffer.EnsureAvailableSpaceUpToLimit(1, 1);
+            Assert.Equal(BlockSize, buffer.AvailableMemory.Length);
+
+            buffer.EnsureAvailableSpaceUpToLimit(1, 2);
+            Assert.Equal(BlockSize, buffer.AvailableMemory.Length);
+
+            buffer.EnsureAvailableSpaceUpToLimit(BlockSize, 0);
+            Assert.Equal(BlockSize, buffer.AvailableMemory.Length);
+
+            buffer.EnsureAvailableSpaceUpToLimit(BlockSize + 1, 0);
+            Assert.Equal(BlockSize, buffer.AvailableMemory.Length);
+
+            buffer.EnsureAvailableSpaceUpToLimit(BlockSize, BlockSize);
+            Assert.Equal(BlockSize, buffer.AvailableMemory.Length);
+
+            buffer.EnsureAvailableSpaceUpToLimit(BlockSize + 1, BlockSize);
+            Assert.Equal(BlockSize, buffer.AvailableMemory.Length);
+
+            buffer.EnsureAvailableSpaceUpToLimit(BlockSize + 1, BlockSize + 1);
+            Assert.Equal(BlockSize * 2, buffer.AvailableMemory.Length);
+
+            buffer.Commit(2);
+            buffer.Discard(1);
+            Assert.Equal(1, buffer.ActiveMemory.Length);
+            Assert.Equal(BlockSize * 2 - 2, buffer.AvailableMemory.Length);
+
+            buffer.EnsureAvailableSpaceUpToLimit(BlockSize * 2 - 2, BlockSize * 2 - 3);
+            Assert.Equal(BlockSize * 2 - 2, buffer.AvailableMemory.Length);
+
+            buffer.EnsureAvailableSpaceUpToLimit(BlockSize * 2 - 2, BlockSize * 2 - 2);
+            Assert.Equal(BlockSize * 2 - 2, buffer.AvailableMemory.Length);
+
+            buffer.EnsureAvailableSpaceUpToLimit(BlockSize * 2 - 2, BlockSize * 2 - 1);
+            Assert.Equal(BlockSize * 2 - 2, buffer.AvailableMemory.Length);
+
+            buffer.EnsureAvailableSpaceUpToLimit(BlockSize * 2 - 1, BlockSize * 2 - 1);
+            Assert.Equal(BlockSize * 2 - 2, buffer.AvailableMemory.Length);
+
+            buffer.EnsureAvailableSpaceUpToLimit(BlockSize * 2 - 1, BlockSize * 2);
+            Assert.Equal(BlockSize * 3 - 2, buffer.AvailableMemory.Length);
+        }
+    }
+}
index 03b55d5..f66427a 100644 (file)
@@ -22,6 +22,7 @@
     <Compile Include="$(CommonTestPath)Tests\System\IO\StreamConformanceTests.cs" Link="Common\System\IO\StreamConformanceTests.cs" />
     <Compile Include="$(CommonTestPath)System\IO\ConnectedStreams.cs" Link="Common\System\IO\ConnectedStreams.cs" />
     <Compile Include="$(CommonPath)System\Net\ArrayBuffer.cs" Link="ProductionCode\Common\System\Net\ArrayBuffer.cs" />
+    <Compile Include="$(CommonPath)System\Net\MultiArrayBuffer.cs" Link="ProductionCode\Common\System\Net\MultiArrayBuffer.cs" />
     <Compile Include="$(CommonPath)System\Net\StreamBuffer.cs" Link="ProductionCode\Common\System\Net\StreamBuffer.cs" />
     <Compile Include="$(CommonTestPath)System\IO\CallTrackingStream.cs" Link="Common\System\IO\CallTrackingStream.cs" />
   </ItemGroup>
index 84bd180..7e56fb2 100644 (file)
@@ -30,6 +30,7 @@
     <Compile Include="$(CommonTestPath)System\IO\CallTrackingStream.cs" Link="Common\System\IO\CallTrackingStream.cs" />
     <Compile Include="$(CommonTestPath)System\IO\ConnectedStreams.cs" Link="Common\System\IO\ConnectedStreams.cs" />
     <Compile Include="$(CommonPath)System\Net\ArrayBuffer.cs" Link="ProductionCode\Common\System\Net\ArrayBuffer.cs" />
+    <Compile Include="$(CommonPath)System\Net\MultiArrayBuffer.cs" Link="ProductionCode\Common\System\Net\MultiArrayBuffer.cs" />
     <Compile Include="$(CommonPath)System\Net\StreamBuffer.cs" Link="ProductionCode\Common\System\Net\StreamBuffer.cs" />
   </ItemGroup>
   <ItemGroup>
index fe34586..ffebd2d 100644 (file)
     <Compile Include="$(CommonTestPath)System\IO\ConnectedStreams.cs" Link="Common\System\IO\ConnectedStreams.cs" />
     <Compile Include="$(CommonTestPath)System\IO\CallTrackingStream.cs" Link="Common\System\IO\CallTrackingStream.cs" />
     <Compile Include="$(CommonPath)System\Net\ArrayBuffer.cs" Link="ProductionCode\Common\System\Net\ArrayBuffer.cs" />
+    <Compile Include="$(CommonPath)System\Net\MultiArrayBuffer.cs" Link="ProductionCode\Common\System\Net\MultiArrayBuffer.cs" />
     <Compile Include="$(CommonPath)System\Net\StreamBuffer.cs" Link="ProductionCode\Common\System\Net\StreamBuffer.cs" />
     <Compile Include="$(CommonPath)System\Threading\Tasks\TaskToApm.cs" Link="Common\System\Threading\Tasks\TaskToApm.cs" />
   </ItemGroup>
index 20a8928..2c69eff 100644 (file)
@@ -24,6 +24,7 @@
     <Compile Include="$(CommonTestPath)System\IO\CallTrackingStream.cs" Link="Common\System\IO\CallTrackingStream.cs" />
     <Compile Include="$(CommonTestPath)System\IO\ConnectedStreams.cs" Link="Common\System\IO\ConnectedStreams.cs" />
     <Compile Include="$(CommonPath)System\Net\ArrayBuffer.cs" Link="ProductionCode\Common\System\Net\ArrayBuffer.cs" />
+    <Compile Include="$(CommonPath)System\Net\MultiArrayBuffer.cs" Link="ProductionCode\Common\System\Net\MultiArrayBuffer.cs" />
     <Compile Include="$(CommonPath)System\Net\StreamBuffer.cs" Link="ProductionCode\Common\System\Net\StreamBuffer.cs" />
     <Compile Include="$(CommonPath)Interop\Windows\Interop.BOOL.cs" Link="ProductionCode\Common\Interop\Windows\Interop.BOOL.cs" />
     <Compile Include="$(CommonPath)Interop\Windows\Interop.Libraries.cs" Link="ProductionCode\Common\Interop\Windows\Interop.Libraries.cs" />
index ac34979..f3f7dc6 100644 (file)
@@ -47,6 +47,7 @@
     <Compile Include="$(CommonTestPath)Tests\System\IO\StreamConformanceTests.cs" Link="Common\System\IO\StreamConformanceTests.cs" />
     <Compile Include="$(CommonTestPath)System\IO\ConnectedStreams.cs" Link="Common\System\IO\ConnectedStreams.cs" />
     <Compile Include="$(CommonPath)System\Net\ArrayBuffer.cs" Link="ProductionCode\Common\System\Net\ArrayBuffer.cs" />
+    <Compile Include="$(CommonPath)System\Net\MultiArrayBuffer.cs" Link="ProductionCode\Common\System\Net\MultiArrayBuffer.cs" />
     <Compile Include="$(CommonPath)System\Net\StreamBuffer.cs" Link="ProductionCode\Common\System\Net\StreamBuffer.cs" />
     <Compile Include="$(CommonTestPath)System\IO\CallTrackingStream.cs" Link="Common\System\IO\CallTrackingStream.cs" />
   </ItemGroup>
index ca262a1..b1c055c 100644 (file)
@@ -24,6 +24,7 @@
     <Compile Include="$(CommonTestPath)Tests\System\IO\StreamConformanceTests.cs" Link="Common\System\IO\StreamConformanceTests.cs" />
     <Compile Include="$(CommonTestPath)System\IO\ConnectedStreams.cs" Link="Common\System\IO\ConnectedStreams.cs" />
     <Compile Include="$(CommonPath)System\Net\ArrayBuffer.cs" Link="ProductionCode\Common\System\Net\ArrayBuffer.cs" />
+    <Compile Include="$(CommonPath)System\Net\MultiArrayBuffer.cs" Link="ProductionCode\Common\System\Net\MultiArrayBuffer.cs" />
     <Compile Include="$(CommonPath)System\Net\StreamBuffer.cs" Link="ProductionCode\Common\System\Net\StreamBuffer.cs" />
     <Compile Include="$(CommonPath)System\Threading\Tasks\TaskToApm.cs" Link="Common\System\Threading\Tasks\TaskToApm.cs" />
     <Compile Include="$(CommonTestPath)System\IO\CallTrackingStream.cs" Link="Common\System\IO\CallTrackingStream.cs" />
index 1869c07..af24f75 100644 (file)
@@ -22,6 +22,7 @@
     <Compile Include="$(CommonTestPath)Tests\System\IO\StreamConformanceTests.cs" Link="Common\System\IO\StreamConformanceTests.cs" />
     <Compile Include="$(CommonTestPath)System\IO\ConnectedStreams.cs" Link="Common\System\IO\ConnectedStreams.cs" />
     <Compile Include="$(CommonPath)System\Net\ArrayBuffer.cs" Link="ProductionCode\Common\System\Net\ArrayBuffer.cs" />
+    <Compile Include="$(CommonPath)System\Net\MultiArrayBuffer.cs" Link="ProductionCode\Common\System\Net\MultiArrayBuffer.cs" />
     <Compile Include="$(CommonPath)System\Net\StreamBuffer.cs" Link="ProductionCode\Common\System\Net\StreamBuffer.cs" />
     <Compile Include="$(CommonPath)System\Threading\Tasks\TaskToApm.cs" Link="Common\System\Threading\Tasks\TaskToApm.cs" />
     <Compile Include="$(CommonTestPath)System\IO\CallTrackingStream.cs" Link="Common\System\IO\CallTrackingStream.cs" />
index edfadc9..99db655 100644 (file)
@@ -54,6 +54,7 @@
     <Compile Include="$(CommonTestPath)Tests\System\IO\StreamConformanceTests.cs" Link="Common\System\IO\StreamConformanceTests.cs" />
     <Compile Include="$(CommonTestPath)System\IO\ConnectedStreams.cs" Link="Common\System\IO\ConnectedStreams.cs" />
     <Compile Include="$(CommonPath)System\Net\ArrayBuffer.cs" Link="ProductionCode\Common\System\Net\ArrayBuffer.cs" />
+    <Compile Include="$(CommonPath)System\Net\MultiArrayBuffer.cs" Link="ProductionCode\Common\System\Net\MultiArrayBuffer.cs" />
     <Compile Include="$(CommonPath)System\Net\StreamBuffer.cs" Link="ProductionCode\Common\System\Net\StreamBuffer.cs" />
     <Compile Include="$(CommonPath)System\Threading\Tasks\TaskToApm.cs" Link="Common\System\Threading\Tasks\TaskToApm.cs" />
   </ItemGroup>
index e38b91e..43afb44 100644 (file)
              Link="Common\System\HexConverter.cs" />
     <Compile Include="$(CommonPath)System\Net\ArrayBuffer.cs"
              Link="Common\System\Net\ArrayBuffer.cs"/>
+    <Compile Include="$(CommonPath)System\Net\MultiArrayBuffer.cs"
+             Link="Common\System\Net\MultiArrayBuffer.cs"/>
   </ItemGroup>
   <!-- SocketsHttpHandler implementation -->
   <ItemGroup Condition="'$(TargetsBrowser)' != 'true'">
index 3eed838..c1ef8c1 100644 (file)
@@ -35,7 +35,7 @@ namespace System.Net.Http
             /// <summary>Stores any trailers received after returning the response content to the caller.</summary>
             private HttpResponseHeaders? _trailers;
 
-            private ArrayBuffer _responseBuffer; // mutable struct, do not make this readonly
+            private MultiArrayBuffer _responseBuffer; // mutable struct, do not make this readonly
             private int _pendingWindowUpdate;
             private CreditWaiter? _creditWaiter;
             private int _availableCredit;
@@ -99,7 +99,7 @@ namespace System.Net.Http
 
                 _responseProtocolState = ResponseProtocolState.ExpectingStatus;
 
-                _responseBuffer = new ArrayBuffer(InitialStreamBufferSize, usePool: true);
+                _responseBuffer = new MultiArrayBuffer(InitialStreamBufferSize);
 
                 _pendingWindowUpdate = 0;
                 _headerBudgetRemaining = connection._pool.Settings._maxResponseHeadersLength * 1024;
@@ -409,10 +409,7 @@ namespace System.Net.Http
                 }
 
                 // Discard any remaining buffered response data
-                if (_responseBuffer.ActiveLength != 0)
-                {
-                    _responseBuffer.Discard(_responseBuffer.ActiveLength);
-                }
+                _responseBuffer.DiscardAll();
 
                 _responseProtocolState = ResponseProtocolState.Aborted;
 
@@ -804,14 +801,14 @@ namespace System.Net.Http
                             break;
                     }
 
-                    if (_responseBuffer.ActiveLength + buffer.Length > StreamWindowSize)
+                    if (_responseBuffer.ActiveMemory.Length + buffer.Length > StreamWindowSize)
                     {
                         // Window size exceeded.
                         ThrowProtocolError(Http2ProtocolErrorCode.FlowControlError);
                     }
 
                     _responseBuffer.EnsureAvailableSpace(buffer.Length);
-                    buffer.CopyTo(_responseBuffer.AvailableSpan);
+                    _responseBuffer.AvailableMemory.CopyFrom(buffer);
                     _responseBuffer.Commit(buffer.Length);
 
                     if (endStream)
@@ -957,7 +954,7 @@ namespace System.Net.Http
                     else
                     {
                         Debug.Assert(_responseProtocolState == ResponseProtocolState.Complete);
-                        return (false, _responseBuffer.ActiveLength == 0);
+                        return (false, _responseBuffer.IsEmpty);
                     }
                 }
             }
@@ -1045,10 +1042,11 @@ namespace System.Net.Http
                 {
                     CheckResponseBodyState();
 
-                    if (_responseBuffer.ActiveLength > 0)
+                    if (!_responseBuffer.IsEmpty)
                     {
-                        int bytesRead = Math.Min(buffer.Length, _responseBuffer.ActiveLength);
-                        _responseBuffer.ActiveSpan.Slice(0, bytesRead).CopyTo(buffer);
+                        MultiMemory activeBuffer = _responseBuffer.ActiveMemory;
+                        int bytesRead = Math.Min(buffer.Length, activeBuffer.Length);
+                        activeBuffer.Slice(0, bytesRead).CopyTo(buffer);
                         _responseBuffer.Discard(bytesRead);
 
                         return (false, bytesRead);
@@ -1268,7 +1266,7 @@ namespace System.Net.Http
                 Debug.Assert(!Monitor.IsEntered(SyncObject));
                 lock (SyncObject)
                 {
-                    if (_responseBuffer.ActiveLength == 0 && _responseProtocolState == ResponseProtocolState.Complete)
+                    if (_responseBuffer.IsEmpty && _responseProtocolState == ResponseProtocolState.Complete)
                     {
                         fullyConsumed = true;
                     }
@@ -1280,7 +1278,10 @@ namespace System.Net.Http
                     Cancel();
                 }
 
-                _responseBuffer.Dispose();
+                lock (SyncObject)
+                {
+                    _responseBuffer.Dispose();
+                }
             }
 
             private CancellationTokenRegistration RegisterRequestBodyCancellation(CancellationToken cancellationToken) =>
index 518affd..bed4ab3 100644 (file)
              Link="Common\System\IO\ConnectedStreams.cs" />
     <Compile Include="$(CommonPath)System\Net\ArrayBuffer.cs"
              Link="ProductionCode\Common\System\Net\ArrayBuffer.cs" />
+    <Compile Include="$(CommonPath)System\Net\MultiArrayBuffer.cs"
+             Link="ProductionCode\Common\System\Net\MultiArrayBuffer.cs" />
     <Compile Include="$(CommonPath)System\Net\StreamBuffer.cs"
              Link="ProductionCode\Common\System\Net\StreamBuffer.cs" />
   </ItemGroup>
index 2a9f204..2b75f1c 100644 (file)
@@ -21,6 +21,7 @@
   <ItemGroup Condition="'$(TargetsAnyOS)' != 'true'">
     <Compile Include="$(CommonPath)System\Threading\Tasks\TaskToApm.cs" Link="Common\System\Threading\Tasks\TaskToApm.cs" />
     <Compile Include="$(CommonPath)System\Net\ArrayBuffer.cs" Link="Common\System\Net\ArrayBuffer.cs" />
+    <Compile Include="$(CommonPath)System\Net\MultiArrayBuffer.cs" Link="Common\System\Net\MultiArrayBuffer.cs" />
     <Compile Include="$(CommonPath)System\Net\Logging\NetEventSource.Common.cs" Link="Common\System\Net\Logging\NetEventSource.Common.cs" />
     <Compile Include="$(CommonPath)System\Net\StreamBuffer.cs" Link="Common\System\Net\StreamBuffer.cs" />
   </ItemGroup>
index a3e3181..84ea8ff 100644 (file)
@@ -13,6 +13,7 @@
     <Compile Include="$(CommonTestPath)System\IO\ConnectedStreams.cs" Link="Common\System\IO\ConnectedStreams.cs" />
     <Compile Include="$(CommonTestPath)System\Threading\Tasks\TaskTimeoutExtensions.cs" Link="TestCommon\System\Threading\Tasks\TaskTimeoutExtensions.cs" />
     <Compile Include="$(CommonPath)System\Net\ArrayBuffer.cs" Link="ProductionCode\Common\System\Net\ArrayBuffer.cs" />
+    <Compile Include="$(CommonPath)System\Net\MultiArrayBuffer.cs" Link="ProductionCode\Common\System\Net\MultiArrayBuffer.cs" />
     <Compile Include="$(CommonPath)System\Net\StreamBuffer.cs" Link="ProductionCode\Common\System\Net\StreamBuffer.cs" />
     <Compile Include="$(CommonPath)System\Threading\Tasks\TaskToApm.cs" Link="Common\System\Threading\Tasks\TaskToApm.cs" />
   </ItemGroup>
index 3c358bc..ae538e6 100644 (file)
@@ -9,6 +9,8 @@
     <!-- Common test files -->
     <Compile Include="$(CommonPath)System\Net\ArrayBuffer.cs"
              Link="ProductionCode\Common\System\Net\ArrayBuffer.cs" />
+    <Compile Include="$(CommonPath)System\Net\MultiArrayBuffer.cs"
+             Link="ProductionCode\Common\System\Net\MultiArrayBuffer.cs" />
     <Compile Include="$(CommonPath)System\Net\StreamBuffer.cs"
              Link="ProductionCode\Common\System\Net\StreamBuffer.cs" />
     <Compile Include="$(CommonTestPath)System\IO\ConnectedStreams.cs"
index ebf5614..f35aa1a 100644 (file)
@@ -83,6 +83,8 @@
              Link="ProductionCode\Common\System\IO\DelegatingStream.cs" />
     <Compile Include="$(CommonPath)System\Net\ArrayBuffer.cs"
              Link="ProductionCode\Common\System\Net\ArrayBuffer.cs" />
+    <Compile Include="$(CommonPath)System\Net\MultiArrayBuffer.cs"
+             Link="ProductionCode\Common\System\Net\MultiArrayBuffer.cs" />
     <Compile Include="$(CommonPath)System\Net\StreamBuffer.cs"
              Link="ProductionCode\Common\System\Net\StreamBuffer.cs" />
     <Compile Include="..\..\src\System\Net\Security\TlsFrameHelper.cs"
index bda9103..a7e0c5f 100644 (file)
     <Compile Include="$(CommonTestPath)System\IO\CallTrackingStream.cs" Link="Common\System\IO\CallTrackingStream.cs" />
     <Compile Include="$(CommonTestPath)System\IO\ConnectedStreams.cs" Link="Common\System\IO\ConnectedStreams.cs" />
     <Compile Include="$(CommonPath)System\Net\ArrayBuffer.cs" Link="ProductionCode\Common\System\Net\ArrayBuffer.cs" />
+    <Compile Include="$(CommonPath)System\Net\MultiArrayBuffer.cs" Link="ProductionCode\Common\System\Net\MultiArrayBuffer.cs" />
     <Compile Include="$(CommonPath)System\Net\StreamBuffer.cs" Link="ProductionCode\Common\System\Net\StreamBuffer.cs" />
   </ItemGroup>
 </Project>
index 9fde6f5..c576910 100644 (file)
@@ -28,6 +28,7 @@
     <Compile Include="$(CommonTestPath)Tests\System\IO\StreamConformanceTests.cs" Link="Common\System\IO\StreamConformanceTests.cs" />
     <Compile Include="$(CommonTestPath)System\IO\ConnectedStreams.cs" Link="Common\System\IO\ConnectedStreams.cs" />
     <Compile Include="$(CommonPath)System\Net\ArrayBuffer.cs" Link="ProductionCode\Common\System\Net\ArrayBuffer.cs" />
+    <Compile Include="$(CommonPath)System\Net\MultiArrayBuffer.cs" Link="ProductionCode\Common\System\Net\MultiArrayBuffer.cs" />
     <Compile Include="$(CommonPath)System\Net\StreamBuffer.cs" Link="ProductionCode\Common\System\Net\StreamBuffer.cs" />
     <Compile Include="$(CommonTestPath)System\IO\CallTrackingStream.cs" Link="Common\System\IO\CallTrackingStream.cs" />
   </ItemGroup>
index 74bcfdc..0918f9d 100644 (file)
@@ -84,6 +84,7 @@
     <Compile Include="$(CommonTestPath)System\IO\CallTrackingStream.cs" Link="Common\System\IO\CallTrackingStream.cs" />
     <Compile Include="$(CommonTestPath)System\IO\ConnectedStreams.cs" Link="Common\System\IO\ConnectedStreams.cs" />
     <Compile Include="$(CommonPath)System\Net\ArrayBuffer.cs" Link="ProductionCode\Common\System\Net\ArrayBuffer.cs" />
+    <Compile Include="$(CommonPath)System\Net\MultiArrayBuffer.cs" Link="ProductionCode\Common\System\Net\MultiArrayBuffer.cs" />
     <Compile Include="$(CommonPath)System\Net\StreamBuffer.cs" Link="ProductionCode\Common\System\Net\StreamBuffer.cs" />
   </ItemGroup>
   <ItemGroup>