From 57b814b05dd9be8063a600855be0b83a770c5220 Mon Sep 17 00:00:00 2001
From: James Newton-King <james@newtonking.com>
Date: Sat, 2 Jul 2022 07:32:39 +0800
Subject: [PATCH] Change QUIC transport to use multiple calls to
 QuicStream.WriteAsync (#42464)

---
 src/Servers/Kestrel/Kestrel.slnf              | 22 ++++++++++++++++++-
 .../src/Internal/QuicStreamContext.cs         | 21 +++++++++++++++++-
 .../test/QuicStreamContextTests.cs            |  9 +++++++-
 3 files changed, 49 insertions(+), 3 deletions(-)

diff --git a/src/Servers/Kestrel/Kestrel.slnf b/src/Servers/Kestrel/Kestrel.slnf
index 8a60f5f91b9..db0f9a4af54 100644
--- a/src/Servers/Kestrel/Kestrel.slnf
+++ b/src/Servers/Kestrel/Kestrel.slnf
@@ -2,19 +2,38 @@
   "solution": {
     "path": "..\\..\\..\\AspNetCore.sln",
     "projects": [
+      "src\\DataProtection\\Abstractions\\src\\Microsoft.AspNetCore.DataProtection.Abstractions.csproj",
+      "src\\DataProtection\\Cryptography.Internal\\src\\Microsoft.AspNetCore.Cryptography.Internal.csproj",
+      "src\\DataProtection\\DataProtection\\src\\Microsoft.AspNetCore.DataProtection.csproj",
+      "src\\DefaultBuilder\\src\\Microsoft.AspNetCore.csproj",
       "src\\Extensions\\Features\\src\\Microsoft.Extensions.Features.csproj",
       "src\\Extensions\\Features\\test\\Microsoft.Extensions.Features.Tests.csproj",
       "src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj",
       "src\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj",
       "src\\Hosting\\Server.Abstractions\\src\\Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj",
+      "src\\Http\\Authentication.Abstractions\\src\\Microsoft.AspNetCore.Authentication.Abstractions.csproj",
+      "src\\Http\\Authentication.Core\\src\\Microsoft.AspNetCore.Authentication.Core.csproj",
       "src\\Http\\Headers\\src\\Microsoft.Net.Http.Headers.csproj",
       "src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj",
       "src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj",
       "src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj",
       "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj",
+      "src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj",
+      "src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj",
+      "src\\Http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj",
       "src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj",
+      "src\\Middleware\\Diagnostics.Abstractions\\src\\Microsoft.AspNetCore.Diagnostics.Abstractions.csproj",
+      "src\\Middleware\\Diagnostics\\src\\Microsoft.AspNetCore.Diagnostics.csproj",
+      "src\\Middleware\\HostFiltering\\src\\Microsoft.AspNetCore.HostFiltering.csproj",
+      "src\\Middleware\\HttpOverrides\\src\\Microsoft.AspNetCore.HttpOverrides.csproj",
       "src\\ObjectPool\\src\\Microsoft.Extensions.ObjectPool.csproj",
+      "src\\Security\\Authentication\\Core\\src\\Microsoft.AspNetCore.Authentication.csproj",
+      "src\\Security\\Authorization\\Core\\src\\Microsoft.AspNetCore.Authorization.csproj",
+      "src\\Security\\Authorization\\Policy\\src\\Microsoft.AspNetCore.Authorization.Policy.csproj",
       "src\\Servers\\Connections.Abstractions\\src\\Microsoft.AspNetCore.Connections.Abstractions.csproj",
+      "src\\Servers\\HttpSys\\src\\Microsoft.AspNetCore.Server.HttpSys.csproj",
+      "src\\Servers\\IIS\\IISIntegration\\src\\Microsoft.AspNetCore.Server.IISIntegration.csproj",
+      "src\\Servers\\IIS\\IIS\\src\\Microsoft.AspNetCore.Server.IIS.csproj",
       "src\\Servers\\Kestrel\\Core\\src\\Microsoft.AspNetCore.Server.Kestrel.Core.csproj",
       "src\\Servers\\Kestrel\\Core\\test\\Microsoft.AspNetCore.Server.Kestrel.Core.Tests.csproj",
       "src\\Servers\\Kestrel\\Kestrel\\src\\Microsoft.AspNetCore.Server.Kestrel.csproj",
@@ -37,7 +56,8 @@
       "src\\Servers\\Kestrel\\test\\Sockets.BindTests\\Sockets.BindTests.csproj",
       "src\\Servers\\Kestrel\\test\\Sockets.FunctionalTests\\Sockets.FunctionalTests.csproj",
       "src\\Servers\\Kestrel\\tools\\CodeGenerator\\CodeGenerator.csproj",
-      "src\\Testing\\src\\Microsoft.AspNetCore.Testing.csproj"
+      "src\\Testing\\src\\Microsoft.AspNetCore.Testing.csproj",
+      "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj"
     ]
   }
 }
\ No newline at end of file
diff --git a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs
index d7a47b65d53..ec2b4107522 100644
--- a/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs
+++ b/src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs
@@ -378,7 +378,26 @@ internal partial class QuicStreamContext : TransportConnection, IPooledStream, I
                 var isCompleted = result.IsCompleted;
                 if (!buffer.IsEmpty)
                 {
-                    await _stream.WriteAsync(buffer, endStream: isCompleted);
+                    if (buffer.IsSingleSegment)
+                    {
+                        // Fast path when the buffer is a single segment.
+                        await _stream.WriteAsync(buffer.First, endStream: isCompleted);
+                    }
+                    else
+                    {
+                        // When then buffer has multiple segments then write them in a loop.
+                        // We're not using a standard foreach here because we want to detect
+                        // the final write and pass end stream flag with that write.
+                        var enumerator = buffer.GetEnumerator();
+                        var isLastSegment = !enumerator.MoveNext();
+
+                        while (!isLastSegment)
+                        {
+                            var currentSegment = enumerator.Current;
+                            isLastSegment = !enumerator.MoveNext();
+                            await _stream.WriteAsync(currentSegment, endStream: isLastSegment && isCompleted);
+                        }
+                    }
                 }
 
                 output.AdvanceTo(end);
diff --git a/src/Servers/Kestrel/Transport.Quic/test/QuicStreamContextTests.cs b/src/Servers/Kestrel/Transport.Quic/test/QuicStreamContextTests.cs
index 1cfe24d2a75..b024e588845 100644
--- a/src/Servers/Kestrel/Transport.Quic/test/QuicStreamContextTests.cs
+++ b/src/Servers/Kestrel/Transport.Quic/test/QuicStreamContextTests.cs
@@ -112,6 +112,8 @@ public class QuicStreamContextTests : TestApplicationErrorLoggerLoggedTest
         var clientStream = await clientConnection.OpenBidirectionalStreamAsync();
         await clientStream.WriteAsync(TestData).DefaultTimeout();
 
+        var readTask = clientStream.ReadUntilEndAsync();
+
         var serverStream = await serverConnection.AcceptAsync().DefaultTimeout();
         var readResult = await serverStream.Transport.Input.ReadAtLeastAsync(TestData.Length).DefaultTimeout();
         serverStream.Transport.Input.AdvanceTo(readResult.Buffer.End);
@@ -123,6 +125,8 @@ public class QuicStreamContextTests : TestApplicationErrorLoggerLoggedTest
         await serverStream.Transport.Input.CompleteAsync();
         await serverStream.Transport.Output.CompleteAsync();
 
+        await readTask.DefaultTimeout();
+
         var quicStreamContext = Assert.IsType<QuicStreamContext>(serverStream);
 
         // Server starts disposing
@@ -178,6 +182,9 @@ public class QuicStreamContextTests : TestApplicationErrorLoggerLoggedTest
         readResult = await serverStream.Transport.Input.ReadAsync().DefaultTimeout();
         Assert.True(readResult.IsCompleted);
 
+        Logger.LogInformation("Client starting to read.");
+        var readingTask = clientStream.ReadUntilEndAsync();
+
         Logger.LogInformation("Server sending data.");
         await serverStream.Transport.Output.WriteAsync(testData).DefaultTimeout();
 
@@ -186,7 +193,7 @@ public class QuicStreamContextTests : TestApplicationErrorLoggerLoggedTest
         await serverStream.Transport.Output.CompleteAsync().DefaultTimeout();
 
         Logger.LogInformation("Client reading until end of stream.");
-        var data = await clientStream.ReadUntilEndAsync().DefaultTimeout();
+        var data = await readingTask.DefaultTimeout();
         Assert.Equal(testData.Length, data.Length);
         Assert.Equal(testData, data);
 
-- 
GitLab