From d448461805e68566a33611acccf7589dad73976a Mon Sep 17 00:00:00 2001 From: Geoff Kizer Date: Wed, 6 Jan 2021 11:06:15 -0800 Subject: [PATCH] Implement and use MultiArrayBuffer (#44980) MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit * 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 * address review feedback Co-authored-by: Geoffrey Kizer Co-authored-by: Günther Foidl --- .../Common/src/System/Net/MultiArrayBuffer.cs | 427 ++++++++++++++++++ .../Common/src/System/Net/StreamBuffer.cs | 32 +- src/libraries/Common/tests/Common.Tests.csproj | 2 + .../tests/TestUtilities/System/AssertExtensions.cs | 49 +++ .../Tests/System/IO/StreamConformanceTests.cs | 14 +- .../Tests/System/Net/MultiArrayBufferTests.cs | 482 +++++++++++++++++++++ .../System.IO.Compression.Brotli.Tests.csproj | 1 + .../tests/System.IO.Compression.Tests.csproj | 1 + .../tests/System.IO.FileSystem.Tests.csproj | 1 + .../tests/System.IO.MemoryMappedFiles.Tests.csproj | 1 + .../tests/System.IO.Pipelines.Tests.csproj | 1 + .../tests/System.IO.Pipes.Tests.csproj | 1 + .../System.IO.UnmanagedMemoryStream.Tests.csproj | 1 + .../System.IO/tests/System.IO.Tests.csproj | 1 + .../System.Net.Http/src/System.Net.Http.csproj | 2 + .../Net/Http/SocketsHttpHandler/Http2Stream.cs | 29 +- .../System.Net.Http.Functional.Tests.csproj | 2 + .../System.Net.Quic/src/System.Net.Quic.csproj | 1 + .../System.Net.Quic.Functional.Tests.csproj | 1 + .../System.Net.Security.Enterprise.Tests.csproj | 2 + .../System.Net.Security.Tests.csproj | 2 + .../System.Net.Sockets.Tests.csproj | 1 + ...m.Security.Cryptography.Primitives.Tests.csproj | 1 + .../tests/System.Text.Encoding.Tests.csproj | 1 + 24 files changed, 1019 insertions(+), 37 deletions(-) create mode 100644 src/libraries/Common/src/System/Net/MultiArrayBuffer.cs create mode 100644 src/libraries/Common/tests/Tests/System/Net/MultiArrayBufferTests.cs diff --git a/src/libraries/Common/src/System/Net/MultiArrayBuffer.cs b/src/libraries/Common/src/System/Net/MultiArrayBuffer.cs new file mode 100644 index 0000000..b217970 --- /dev/null +++ b/src/libraries/Common/src/System/Net/MultiArrayBuffer.cs @@ -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.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.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.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 + + 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 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(_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 destination) + { + if (destination.Length < _length) + { + throw new ArgumentOutOfRangeException(nameof(destination)); + } + + int blockCount = BlockCount; + for (int blockIndex = 0; blockIndex < blockCount; blockIndex++) + { + Memory block = GetBlock(blockIndex); + block.Span.CopyTo(destination); + destination = destination.Slice(block.Length); + } + } + + public void CopyFrom(ReadOnlySpan source) + { + if (_length < source.Length) + { + throw new ArgumentOutOfRangeException(nameof(source)); + } + + int blockCount = BlockCount; + for (int blockIndex = 0; blockIndex < blockCount; blockIndex++) + { + Memory 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; + } +} diff --git a/src/libraries/Common/src/System/Net/StreamBuffer.cs b/src/libraries/Common/src/System/Net/StreamBuffer.cs index 09845cd..bdd1ff2 100644 --- a/src/libraries/Common/src/System/Net/StreamBuffer.cs +++ b/src/libraries/Common/src/System/Net/StreamBuffer.cs @@ -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 diff --git a/src/libraries/Common/tests/Common.Tests.csproj b/src/libraries/Common/tests/Common.Tests.csproj index 9dd6f6f..a71e031 100644 --- a/src/libraries/Common/tests/Common.Tests.csproj +++ b/src/libraries/Common/tests/Common.Tests.csproj @@ -90,6 +90,7 @@ + @@ -106,6 +107,7 @@ + diff --git a/src/libraries/Common/tests/TestUtilities/System/AssertExtensions.cs b/src/libraries/Common/tests/TestUtilities/System/AssertExtensions.cs index 2b0d037..940c4de 100644 --- a/src/libraries/Common/tests/TestUtilities/System/AssertExtensions.cs +++ b/src/libraries/Common/tests/TestUtilities/System/AssertExtensions.cs @@ -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. /// /// 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 } } + /// + /// 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. + /// + /// The array that should be equal to. + /// + public static void SequenceEqual(ReadOnlySpan expected, ReadOnlySpan actual) where T : IEquatable + { + // 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(Span expected, Span actual) where T : IEquatable => SequenceEqual((ReadOnlySpan)expected, (ReadOnlySpan)actual); + + public static void SequenceEqual(T[] expected, T[] actual) where T : IEquatable => SequenceEqual(expected.AsSpan(), actual.AsSpan()); + public static void AtLeastOneEquals(T expected1, T expected2, T value) { EqualityComparer comparer = EqualityComparer.Default; diff --git a/src/libraries/Common/tests/Tests/System/IO/StreamConformanceTests.cs b/src/libraries/Common/tests/Tests/System/IO/StreamConformanceTests.cs index 68f5b3e..947c0b8 100644 --- a/src/libraries/Common/tests/Tests/System/IO/StreamConformanceTests.cs +++ b/src/libraries/Common/tests/Tests/System/IO/StreamConformanceTests.cs @@ -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 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 index 0000000..1459e9c --- /dev/null +++ b/src/libraries/Common/tests/Tests/System/Net/MultiArrayBufferTests.cs @@ -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 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 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 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 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); + } + } +} diff --git a/src/libraries/System.IO.Compression.Brotli/tests/System.IO.Compression.Brotli.Tests.csproj b/src/libraries/System.IO.Compression.Brotli/tests/System.IO.Compression.Brotli.Tests.csproj index 03b55d5..f66427a 100644 --- a/src/libraries/System.IO.Compression.Brotli/tests/System.IO.Compression.Brotli.Tests.csproj +++ b/src/libraries/System.IO.Compression.Brotli/tests/System.IO.Compression.Brotli.Tests.csproj @@ -22,6 +22,7 @@ + diff --git a/src/libraries/System.IO.Compression/tests/System.IO.Compression.Tests.csproj b/src/libraries/System.IO.Compression/tests/System.IO.Compression.Tests.csproj index 84bd180..7e56fb2 100644 --- a/src/libraries/System.IO.Compression/tests/System.IO.Compression.Tests.csproj +++ b/src/libraries/System.IO.Compression/tests/System.IO.Compression.Tests.csproj @@ -30,6 +30,7 @@ + diff --git a/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj b/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj index fe34586..ffebd2d 100644 --- a/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj +++ b/src/libraries/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj @@ -166,6 +166,7 @@ + diff --git a/src/libraries/System.IO.MemoryMappedFiles/tests/System.IO.MemoryMappedFiles.Tests.csproj b/src/libraries/System.IO.MemoryMappedFiles/tests/System.IO.MemoryMappedFiles.Tests.csproj index 20a8928..2c69eff 100644 --- a/src/libraries/System.IO.MemoryMappedFiles/tests/System.IO.MemoryMappedFiles.Tests.csproj +++ b/src/libraries/System.IO.MemoryMappedFiles/tests/System.IO.MemoryMappedFiles.Tests.csproj @@ -24,6 +24,7 @@ + diff --git a/src/libraries/System.IO.Pipelines/tests/System.IO.Pipelines.Tests.csproj b/src/libraries/System.IO.Pipelines/tests/System.IO.Pipelines.Tests.csproj index ac34979..f3f7dc6 100644 --- a/src/libraries/System.IO.Pipelines/tests/System.IO.Pipelines.Tests.csproj +++ b/src/libraries/System.IO.Pipelines/tests/System.IO.Pipelines.Tests.csproj @@ -47,6 +47,7 @@ + diff --git a/src/libraries/System.IO.Pipes/tests/System.IO.Pipes.Tests.csproj b/src/libraries/System.IO.Pipes/tests/System.IO.Pipes.Tests.csproj index ca262a1..b1c055c 100644 --- a/src/libraries/System.IO.Pipes/tests/System.IO.Pipes.Tests.csproj +++ b/src/libraries/System.IO.Pipes/tests/System.IO.Pipes.Tests.csproj @@ -24,6 +24,7 @@ + diff --git a/src/libraries/System.IO.UnmanagedMemoryStream/tests/System.IO.UnmanagedMemoryStream.Tests.csproj b/src/libraries/System.IO.UnmanagedMemoryStream/tests/System.IO.UnmanagedMemoryStream.Tests.csproj index 1869c07..af24f75 100644 --- a/src/libraries/System.IO.UnmanagedMemoryStream/tests/System.IO.UnmanagedMemoryStream.Tests.csproj +++ b/src/libraries/System.IO.UnmanagedMemoryStream/tests/System.IO.UnmanagedMemoryStream.Tests.csproj @@ -22,6 +22,7 @@ + diff --git a/src/libraries/System.IO/tests/System.IO.Tests.csproj b/src/libraries/System.IO/tests/System.IO.Tests.csproj index edfadc9..99db655 100644 --- a/src/libraries/System.IO/tests/System.IO.Tests.csproj +++ b/src/libraries/System.IO/tests/System.IO.Tests.csproj @@ -54,6 +54,7 @@ + diff --git a/src/libraries/System.Net.Http/src/System.Net.Http.csproj b/src/libraries/System.Net.Http/src/System.Net.Http.csproj index e38b91e..43afb44 100644 --- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj @@ -122,6 +122,8 @@ Link="Common\System\HexConverter.cs" /> + diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs index 3eed838..c1ef8c1 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs @@ -35,7 +35,7 @@ namespace System.Net.Http /// Stores any trailers received after returning the response content to the caller. 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) => diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj index 518affd..bed4ab3 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/System.Net.Http.Functional.Tests.csproj @@ -172,6 +172,8 @@ Link="Common\System\IO\ConnectedStreams.cs" /> + diff --git a/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj b/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj index 2a9f204..2b75f1c 100644 --- a/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj +++ b/src/libraries/System.Net.Quic/src/System.Net.Quic.csproj @@ -21,6 +21,7 @@ + diff --git a/src/libraries/System.Net.Quic/tests/FunctionalTests/System.Net.Quic.Functional.Tests.csproj b/src/libraries/System.Net.Quic/tests/FunctionalTests/System.Net.Quic.Functional.Tests.csproj index a3e3181..84ea8ff 100644 --- a/src/libraries/System.Net.Quic/tests/FunctionalTests/System.Net.Quic.Functional.Tests.csproj +++ b/src/libraries/System.Net.Quic/tests/FunctionalTests/System.Net.Quic.Functional.Tests.csproj @@ -13,6 +13,7 @@ + diff --git a/src/libraries/System.Net.Security/tests/EnterpriseTests/System.Net.Security.Enterprise.Tests.csproj b/src/libraries/System.Net.Security/tests/EnterpriseTests/System.Net.Security.Enterprise.Tests.csproj index 3c358bc..ae538e6 100644 --- a/src/libraries/System.Net.Security/tests/EnterpriseTests/System.Net.Security.Enterprise.Tests.csproj +++ b/src/libraries/System.Net.Security/tests/EnterpriseTests/System.Net.Security.Enterprise.Tests.csproj @@ -9,6 +9,8 @@ + + + diff --git a/src/libraries/System.Security.Cryptography.Primitives/tests/System.Security.Cryptography.Primitives.Tests.csproj b/src/libraries/System.Security.Cryptography.Primitives/tests/System.Security.Cryptography.Primitives.Tests.csproj index 9fde6f5..c576910 100644 --- a/src/libraries/System.Security.Cryptography.Primitives/tests/System.Security.Cryptography.Primitives.Tests.csproj +++ b/src/libraries/System.Security.Cryptography.Primitives/tests/System.Security.Cryptography.Primitives.Tests.csproj @@ -28,6 +28,7 @@ + diff --git a/src/libraries/System.Text.Encoding/tests/System.Text.Encoding.Tests.csproj b/src/libraries/System.Text.Encoding/tests/System.Text.Encoding.Tests.csproj index 74bcfdc..0918f9d 100644 --- a/src/libraries/System.Text.Encoding/tests/System.Text.Encoding.Tests.csproj +++ b/src/libraries/System.Text.Encoding/tests/System.Text.Encoding.Tests.csproj @@ -84,6 +84,7 @@ + -- 2.7.4