From a57a0462bb1e23d2851b638757cb49278622b677 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Wed, 8 Sep 2021 14:04:23 -0700
Subject: [PATCH] [release/6.0] Use ArrayPool as default pool fallback (#36253)

* Use ArrayPool as SlabMemoryPool fallback

Fixes #30364

* Fix comments

* Typos

[ci skip]

* Support increase buffer size

* Add function tests, rename argument

* Update src/Servers/Kestrel/Core/src/Internal/Http3/Http3OutputProducer.cs

Co-authored-by: Stephen Halter <halter73@gmail.com>

* Update src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs

Co-authored-by: Stephen Halter <halter73@gmail.com>

* Reduce allocations

Co-authored-by: Sebastien Ros <sebastienros@gmail.com>
Co-authored-by: Stephen Halter <halter73@gmail.com>
---
 .../src/Internal/Http/Http1OutputProducer.cs  | 50 ++++++++++-
 .../src/Internal/Http2/Http2OutputProducer.cs | 49 ++++++++++-
 .../src/Internal/Http3/Http3OutputProducer.cs | 49 ++++++++++-
 .../PipeWriterHelpers/ConcurrentPipeWriter.cs |  8 +-
 ...erTests.cs => Http1OutputProducerTests.cs} | 87 ++++++++++++++++++-
 .../Http2/Http2StreamTests.cs                 | 48 ++++++++++
 .../Http3/Http3StreamTests.cs                 | 25 ++++++
 7 files changed, 298 insertions(+), 18 deletions(-)
 rename src/Servers/Kestrel/Core/test/{OutputProducerTests.cs => Http1OutputProducerTests.cs} (56%)

diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1OutputProducer.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1OutputProducer.cs
index 1e5d98d6887..41abf1f6c53 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1OutputProducer.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1OutputProducer.cs
@@ -49,6 +49,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
 
         private readonly ConcurrentPipeWriter _pipeWriter;
         private IMemoryOwner<byte>? _fakeMemoryOwner;
+        private byte[]? _fakeMemory;
 
         // Chunked responses need to be treated uniquely when using GetMemory + Advance.
         // We need to know the size of the data written to the chunk before calling Advance on the
@@ -414,6 +415,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                     _fakeMemoryOwner = null;
                 }
 
+                if (_fakeMemory != null)
+                {
+                    ArrayPool<byte>.Shared.Return(_fakeMemory);
+                    _fakeMemory = null;
+                }
+
                 // Call dispose on any memory that wasn't written.
                 if (_completedSegments != null)
                 {
@@ -650,13 +657,48 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
             _advancedBytesForChunk = 0;
         }
 
-        private Memory<byte> GetFakeMemory(int sizeHint)
+        internal Memory<byte> GetFakeMemory(int minSize)
         {
-            if (_fakeMemoryOwner == null)
+            // Try to reuse _fakeMemoryOwner
+            if (_fakeMemoryOwner != null)
+            {
+                if (_fakeMemoryOwner.Memory.Length < minSize)
+                {
+                    _fakeMemoryOwner.Dispose();
+                    _fakeMemoryOwner = null;
+                }
+                else
+                {
+                    return _fakeMemoryOwner.Memory;
+                }
+            }
+
+            // Try to reuse _fakeMemory
+            if (_fakeMemory != null)
+            {
+                if (_fakeMemory.Length < minSize)
+                {
+                    ArrayPool<byte>.Shared.Return(_fakeMemory);
+                    _fakeMemory = null;
+                }
+                else
+                {
+                    return _fakeMemory;
+                }
+            }
+
+            // Requesting a bigger buffer could throw.
+            if (minSize <= _memoryPool.MaxBufferSize)
+            {
+                // Use the specified pool as it fits.
+                _fakeMemoryOwner = _memoryPool.Rent(minSize);
+                return _fakeMemoryOwner.Memory;
+            }
+            else
             {
-                _fakeMemoryOwner = _memoryPool.Rent(sizeHint);
+                // Use the array pool. Its MaxBufferSize is int.MaxValue.
+                return _fakeMemory = ArrayPool<byte>.Shared.Rent(minSize);
             }
-            return _fakeMemoryOwner.Memory;
         }
 
         private Memory<byte> LeasedMemory(int sizeHint)
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs
index 6a17db8934e..7be6cc65209 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2OutputProducer.cs
@@ -36,6 +36,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
         private readonly PipeReader _pipeReader;
         private readonly ManualResetValueTaskSource<object?> _resetAwaitable = new ManualResetValueTaskSource<object?>();
         private IMemoryOwner<byte>? _fakeMemoryOwner;
+        private byte[]? _fakeMemory;
         private bool _startedWritingDataFrames;
         private bool _streamCompleted;
         private bool _suffixSent;
@@ -120,6 +121,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
                     _fakeMemoryOwner.Dispose();
                     _fakeMemoryOwner = null;
                 }
+
+                if (_fakeMemory != null)
+                {
+                    ArrayPool<byte>.Shared.Return(_fakeMemory);
+                    _fakeMemory = null;
+                }
             }
         }
 
@@ -486,14 +493,48 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
             }
         }
 
-        private Memory<byte> GetFakeMemory(int sizeHint)
+        internal Memory<byte> GetFakeMemory(int minSize)
         {
-            if (_fakeMemoryOwner == null)
+            // Try to reuse _fakeMemoryOwner
+            if (_fakeMemoryOwner != null)
+            {
+                if (_fakeMemoryOwner.Memory.Length < minSize)
+                {
+                    _fakeMemoryOwner.Dispose();
+                    _fakeMemoryOwner = null;
+                }
+                else
+                {
+                    return _fakeMemoryOwner.Memory;
+                }
+            }
+
+            // Try to reuse _fakeMemory
+            if (_fakeMemory != null)
             {
-                _fakeMemoryOwner = _memoryPool.Rent(sizeHint);
+                if (_fakeMemory.Length < minSize)
+                {
+                    ArrayPool<byte>.Shared.Return(_fakeMemory);
+                    _fakeMemory = null;
+                }
+                else
+                {
+                    return _fakeMemory;
+                }
             }
 
-            return _fakeMemoryOwner.Memory;
+            // Requesting a bigger buffer could throw.
+            if (minSize <= _memoryPool.MaxBufferSize)
+            {
+                // Use the specified pool as it fits.
+                _fakeMemoryOwner = _memoryPool.Rent(minSize);
+                return _fakeMemoryOwner.Memory;
+            }
+            else
+            {
+                // Use the array pool. Its MaxBufferSize is int.MaxValue.
+                return _fakeMemory = ArrayPool<byte>.Shared.Rent(minSize);
+            }
         }
 
         [StackTraceHidden]
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3OutputProducer.cs b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3OutputProducer.cs
index 9bc96871f8a..52b05501688 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Http3/Http3OutputProducer.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Http3/Http3OutputProducer.cs
@@ -34,6 +34,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
         private bool _disposed;
         private bool _suffixSent;
         private IMemoryOwner<byte>? _fakeMemoryOwner;
+        private byte[]? _fakeMemory;
 
         public Http3OutputProducer(
              Http3FrameWriter frameWriter,
@@ -88,6 +89,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
                     _fakeMemoryOwner.Dispose();
                     _fakeMemoryOwner = null;
                 }
+
+                if (_fakeMemory != null)
+                {
+                    ArrayPool<byte>.Shared.Return(_fakeMemory);
+                    _fakeMemory = null;
+                }
             }
         }
 
@@ -208,14 +215,48 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3
             }
         }
 
-        private Memory<byte> GetFakeMemory(int sizeHint)
+        internal Memory<byte> GetFakeMemory(int minSize)
         {
-            if (_fakeMemoryOwner == null)
+            // Try to reuse _fakeMemoryOwner
+            if (_fakeMemoryOwner != null)
+            {
+                if (_fakeMemoryOwner.Memory.Length < minSize)
+                {
+                    _fakeMemoryOwner.Dispose();
+                    _fakeMemoryOwner = null;
+                }
+                else
+                {
+                    return _fakeMemoryOwner.Memory;
+                }
+            }
+
+            // Try to reuse _fakeMemory
+            if (_fakeMemory != null)
             {
-                _fakeMemoryOwner = _memoryPool.Rent(sizeHint);
+                if (_fakeMemory.Length < minSize)
+                {
+                    ArrayPool<byte>.Shared.Return(_fakeMemory);
+                    _fakeMemory = null;
+                }
+                else
+                {
+                    return _fakeMemory;
+                }
             }
 
-            return _fakeMemoryOwner.Memory;
+            // Requesting a bigger buffer could throw.
+            if (minSize <= _memoryPool.MaxBufferSize)
+            {
+                // Use the specified pool as it fits.
+                _fakeMemoryOwner = _memoryPool.Rent(minSize);
+                return _fakeMemoryOwner.Memory;
+            }
+            else
+            {
+                // Use the array pool. Its MaxBufferSize is int.MaxValue.
+                return _fakeMemory = ArrayPool<byte>.Shared.Rent(minSize);
+            }
         }
 
         [StackTraceHidden]
diff --git a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/PipeWriterHelpers/ConcurrentPipeWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/PipeWriterHelpers/ConcurrentPipeWriter.cs
index c74e5f33975..ea652c1bc3e 100644
--- a/src/Servers/Kestrel/Core/src/Internal/Infrastructure/PipeWriterHelpers/ConcurrentPipeWriter.cs
+++ b/src/Servers/Kestrel/Core/src/Internal/Infrastructure/PipeWriterHelpers/ConcurrentPipeWriter.cs
@@ -346,19 +346,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure.PipeW
             }
         }
 
-        private BufferSegment AllocateSegmentUnsynchronized(int sizeHint)
+        private BufferSegment AllocateSegmentUnsynchronized(int minSize)
         {
             BufferSegment newSegment = CreateSegmentUnsynchronized();
 
-            if (sizeHint <= _pool.MaxBufferSize)
+            if (minSize <= _pool.MaxBufferSize)
             {
                 // Use the specified pool if it fits
-                newSegment.SetOwnedMemory(_pool.Rent(GetSegmentSize(sizeHint, _pool.MaxBufferSize)));
+                newSegment.SetOwnedMemory(_pool.Rent(GetSegmentSize(minSize, _pool.MaxBufferSize)));
             }
             else
             {
                 // We can't use the recommended pool so use the ArrayPool
-                newSegment.SetOwnedMemory(ArrayPool<byte>.Shared.Rent(sizeHint));
+                newSegment.SetOwnedMemory(ArrayPool<byte>.Shared.Rent(minSize));
             }
 
             _tailMemory = newSegment.AvailableMemory;
diff --git a/src/Servers/Kestrel/Core/test/OutputProducerTests.cs b/src/Servers/Kestrel/Core/test/Http1OutputProducerTests.cs
similarity index 56%
rename from src/Servers/Kestrel/Core/test/OutputProducerTests.cs
rename to src/Servers/Kestrel/Core/test/Http1OutputProducerTests.cs
index 3204cedd340..272fca47786 100644
--- a/src/Servers/Kestrel/Core/test/OutputProducerTests.cs
+++ b/src/Servers/Kestrel/Core/test/Http1OutputProducerTests.cs
@@ -15,11 +15,11 @@ using Xunit;
 
 namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
 {
-    public class OutputProducerTests : IDisposable
+    public class Http1OutputProducerTests : IDisposable
     {
         private readonly MemoryPool<byte> _memoryPool;
 
-        public OutputProducerTests()
+        public Http1OutputProducerTests()
         {
             _memoryPool = PinnedBlockMemoryPoolFactory.Create();
         }
@@ -74,6 +74,89 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
             mockConnectionContext.Verify(f => f.Abort(null), Times.Once());
         }
 
+        [Fact]
+        public void AllocatesFakeMemorySmallerThanMaxBufferSize()
+        {
+            var pipeOptions = new PipeOptions
+            (
+                pool: _memoryPool,
+                readerScheduler: Mock.Of<PipeScheduler>(),
+                writerScheduler: PipeScheduler.Inline,
+                useSynchronizationContext: false
+            );
+
+            using (var socketOutput = CreateOutputProducer(pipeOptions))
+            {
+                var bufferSize = 1;
+                var fakeMemory = socketOutput.GetFakeMemory(bufferSize);
+
+                Assert.True(fakeMemory.Length >= bufferSize);
+            }
+        }
+
+        [Fact]
+        public void AllocatesFakeMemoryBiggerThanMaxBufferSize()
+        {
+            var pipeOptions = new PipeOptions
+            (
+                pool: _memoryPool,
+                readerScheduler: Mock.Of<PipeScheduler>(),
+                writerScheduler: PipeScheduler.Inline,
+                useSynchronizationContext: false
+            );
+
+            using (var socketOutput = CreateOutputProducer(pipeOptions))
+            {
+                var bufferSize = _memoryPool.MaxBufferSize * 2;
+                var fakeMemory = socketOutput.GetFakeMemory(bufferSize);
+
+                Assert.True(fakeMemory.Length >= bufferSize);
+            }
+        }
+
+        [Fact]
+        public void AllocatesIncreasingFakeMemory()
+        {
+            var pipeOptions = new PipeOptions
+            (
+                pool: _memoryPool,
+                readerScheduler: Mock.Of<PipeScheduler>(),
+                writerScheduler: PipeScheduler.Inline,
+                useSynchronizationContext: false
+            );
+
+            using (var socketOutput = CreateOutputProducer(pipeOptions))
+            {
+                var bufferSize1 = 1024;
+                var bufferSize2 = 2048;
+                var fakeMemory = socketOutput.GetFakeMemory(bufferSize1);
+                fakeMemory = socketOutput.GetFakeMemory(bufferSize2);
+
+                Assert.True(fakeMemory.Length >= bufferSize2);
+            }
+        }
+
+        [Fact]
+        public void ReusesFakeMemory()
+        {
+            var pipeOptions = new PipeOptions
+            (
+                pool: _memoryPool,
+                readerScheduler: Mock.Of<PipeScheduler>(),
+                writerScheduler: PipeScheduler.Inline,
+                useSynchronizationContext: false
+            );
+
+            using (var socketOutput = CreateOutputProducer(pipeOptions))
+            {
+                var bufferSize = 1024;
+                var fakeMemory1 = socketOutput.GetFakeMemory(bufferSize);
+                var fakeMemory2 = socketOutput.GetFakeMemory(bufferSize);
+
+                Assert.Equal(fakeMemory1, fakeMemory2);
+            }
+        }
+
         private TestHttpOutputProducer CreateOutputProducer(
             PipeOptions pipeOptions = null,
             ConnectionContext connectionContext = null)
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs
index 284440c1c3b..0fe7184e2e9 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2StreamTests.cs
@@ -5101,5 +5101,53 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
 
             Assert.Contains(LogMessages, m => m.Message.Equals("One or more of the following response headers have been removed because they are invalid for HTTP/2 and HTTP/3 responses: 'Connection', 'Transfer-Encoding', 'Keep-Alive', 'Upgrade' and 'Proxy-Connection'."));
         }
+
+        [Theory]
+        [InlineData(1000)]
+        [InlineData(4096)]
+        [InlineData(8000)] // Greater than the default max pool size (4096)
+        public async Task GetMemory_AfterAbort_GetsFakeMemory(int sizeHint)
+        {
+            var headers = new[]
+            {
+                new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
+                new KeyValuePair<string, string>(HeaderNames.Path, "/"),
+                new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
+            };
+            await InitializeConnectionAsync(async httpContext =>
+            {
+                var response = httpContext.Response;
+
+                await response.BodyWriter.FlushAsync();
+
+                httpContext.Abort();
+
+                var memory = response.BodyWriter.GetMemory(sizeHint);
+                Assert.True(memory.Length >= sizeHint);
+
+                var fisrtPartOfResponse = Encoding.ASCII.GetBytes(new String('a', sizeHint));
+                fisrtPartOfResponse.CopyTo(memory);
+                response.BodyWriter.Advance(sizeHint);
+            });
+
+            await StartStreamAsync(1, headers, endStream: true);
+
+            var headersFrame = await ExpectAsync(Http2FrameType.HEADERS,
+                withLength: 32,
+                withFlags: (byte)Http2HeadersFrameFlags.END_HEADERS,
+                withStreamId: 1);
+            await ExpectAsync(Http2FrameType.RST_STREAM,
+                withLength: 4,
+                withFlags: (byte)Http2DataFrameFlags.NONE,
+                withStreamId: 1);
+
+            await StopConnectionAsync(expectedLastStreamId: 1, ignoreNonGoAwayFrames: false);
+
+            _hpackDecoder.Decode(headersFrame.PayloadSequence, endHeaders: false, handler: this);
+
+            Assert.Equal(2, _decodedHeaders.Count);
+            Assert.Contains("date", _decodedHeaders.Keys, StringComparer.OrdinalIgnoreCase);
+            Assert.Equal("200", _decodedHeaders[HeaderNames.Status]);
+        }
     }
 }
diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs
index e6f66f75b76..6a82f44102c 100644
--- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs
+++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http3/Http3StreamTests.cs
@@ -2927,5 +2927,30 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
             var responseHeaders = await requestStream.ExpectHeadersAsync(expectEnd: true);
             Assert.Equal("200", responseHeaders[HeaderNames.Status]);
         }
+
+        [Theory]
+        [InlineData(1000)]
+        [InlineData(4096)]
+        [InlineData(8000)] // Greater than the default max pool size (4096)
+        public async Task GetMemory_AfterAbort_GetsFakeMemory(int sizeHint)
+        {
+            var tcs = new TaskCompletionSource();
+            var headers = new[]
+            {
+                new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
+                new KeyValuePair<string, string>(HeaderNames.Path, "/"),
+                new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
+            };
+            var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(async context =>
+            {
+                context.Abort();
+
+                var memory = context.Response.BodyWriter.GetMemory(sizeHint);
+
+                Assert.True(memory.Length >= sizeHint);
+                await context.Response.CompleteAsync();
+                context.Response.BodyWriter.Advance(memory.Length);
+            });
+        }
     }
 }
-- 
GitLab