diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmark.cs index 3018f4a346..b3e8f15403 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmark.cs +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Http2ConnectionBenchmark.cs @@ -3,9 +3,11 @@ using System; using System.Buffers; -using System.Collections.Generic; +using System.Buffers.Binary; +using System.Diagnostics; +using System.IO; using System.IO.Pipelines; -using System.Net.Http.HPack; +using System.Linq; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Http; @@ -25,20 +27,29 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance public class Http2ConnectionBenchmark { private MemoryPool _memoryPool; - private Pipe _pipe; private HttpRequestHeaders _httpRequestHeaders; private Http2Connection _connection; private Http2HeadersEnumerator _requestHeadersEnumerator; private int _currentStreamId; private byte[] _headersBuffer; + private DuplexPipe.DuplexPipePair _connectionPair; + private Http2Frame _httpFrame; + private string _responseData; + private int _dataWritten; + + [Params(0, 10, 1024 * 1024)] + public int ResponseDataLength { get; set; } [GlobalSetup] public void GlobalSetup() { _memoryPool = SlabMemoryPoolFactory.Create(); + _httpFrame = new Http2Frame(); + _responseData = new string('!', ResponseDataLength); var options = new PipeOptions(_memoryPool, readerScheduler: PipeScheduler.Inline, writerScheduler: PipeScheduler.Inline, useSynchronizationContext: false); - _pipe = new Pipe(options); + + _connectionPair = DuplexPipe.CreateConnectionPair(options, options); _httpRequestHeaders = new HttpRequestHeaders(); _httpRequestHeaders.Append(HeaderNames.Method, new StringValues("GET")); @@ -55,15 +66,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance Log = new KestrelTrace(NullLogger.Instance), SystemClock = new MockSystemClock() }; - serviceContext.ServerOptions.Limits.Http2.MaxStreamsPerConnection = int.MaxValue; serviceContext.DateHeaderValueManager.OnHeartbeat(default); _connection = new Http2Connection(new HttpConnectionContext { MemoryPool = _memoryPool, ConnectionId = "TestConnectionId", - Protocols = Core.HttpProtocols.Http2, - Transport = new MockDuplexPipe(_pipe.Reader, new NullPipeWriter()), + Protocols = HttpProtocols.Http2, + Transport = _connectionPair.Transport, ServiceContext = serviceContext, ConnectionFeatures = new FeatureCollection(), TimeoutControl = new MockTimeoutControl(), @@ -73,11 +83,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance _currentStreamId = 1; - _ = _connection.ProcessRequestsAsync(new DummyApplication()); + _ = _connection.ProcessRequestsAsync(new DummyApplication(c => ResponseDataLength == 0 ? Task.CompletedTask : c.Response.WriteAsync(_responseData), new MockHttpContextFactory())); - _pipe.Writer.Write(Http2Connection.ClientPreface); - _pipe.Writer.WriteSettings(new Http2PeerSettings()); - _pipe.Writer.FlushAsync().GetAwaiter().GetResult(); + _connectionPair.Application.Output.Write(Http2Connection.ClientPreface); + _connectionPair.Application.Output.WriteSettings(new Http2PeerSettings + { + InitialWindowSize = 2147483647 + }); + _connectionPair.Application.Output.FlushAsync().GetAwaiter().GetResult(); + + // Read past connection setup frames + ReceiveFrameAsync(_connectionPair.Application.Input, _httpFrame).GetAwaiter().GetResult(); + Debug.Assert(_httpFrame.Type == Http2FrameType.SETTINGS); + ReceiveFrameAsync(_connectionPair.Application.Input, _httpFrame).GetAwaiter().GetResult(); + Debug.Assert(_httpFrame.Type == Http2FrameType.WINDOW_UPDATE); + ReceiveFrameAsync(_connectionPair.Application.Input, _httpFrame).GetAwaiter().GetResult(); + Debug.Assert(_httpFrame.Type == Http2FrameType.SETTINGS); } [Benchmark] @@ -85,15 +106,77 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance { _requestHeadersEnumerator.Initialize(_httpRequestHeaders); _requestHeadersEnumerator.MoveNext(); - _pipe.Writer.WriteStartStream(streamId: _currentStreamId, _requestHeadersEnumerator, _headersBuffer, endStream: true); + _connectionPair.Application.Output.WriteStartStream(streamId: _currentStreamId, _requestHeadersEnumerator, _headersBuffer, endStream: true, frame: _httpFrame); + await _connectionPair.Application.Output.FlushAsync(); + + while (true) + { + await ReceiveFrameAsync(_connectionPair.Application.Input, _httpFrame); + + if (_httpFrame.StreamId != _currentStreamId && _httpFrame.StreamId != 0) + { + throw new Exception($"Unexpected stream ID: {_httpFrame.StreamId}"); + } + + if (_httpFrame.Type == Http2FrameType.DATA) + { + _dataWritten += _httpFrame.DataPayloadLength; + } + + if (_dataWritten > 1024 * 32) + { + _connectionPair.Application.Output.WriteWindowUpdateAsync(streamId: 0, _dataWritten, _httpFrame); + await _connectionPair.Application.Output.FlushAsync(); + + _dataWritten = 0; + } + + if ((_httpFrame.HeadersFlags & Http2HeadersFrameFlags.END_STREAM) == Http2HeadersFrameFlags.END_STREAM) + { + break; + } + } + _currentStreamId += 2; - await _pipe.Writer.FlushAsync(); + } + + internal async ValueTask ReceiveFrameAsync(PipeReader pipeReader, Http2Frame frame, uint maxFrameSize = Http2PeerSettings.DefaultMaxFrameSize) + { + while (true) + { + var result = await pipeReader.ReadAsync(); + var buffer = result.Buffer; + var consumed = buffer.Start; + var examined = buffer.Start; + + try + { + if (Http2FrameReader.TryReadFrame(ref buffer, frame, maxFrameSize, out var framePayload)) + { + consumed = examined = framePayload.End; + return; + } + else + { + examined = buffer.End; + } + + if (result.IsCompleted) + { + throw new IOException("The reader completed without returning a frame."); + } + } + finally + { + pipeReader.AdvanceTo(consumed, examined); + } + } } [GlobalCleanup] public void Dispose() { - _pipe.Writer.Complete(); + _connectionPair.Application.Output.Complete(); _memoryPool?.Dispose(); } } diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Mocks/MockHttpContextFactory.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/Mocks/MockHttpContextFactory.cs new file mode 100644 index 0000000000..e89076aba2 --- /dev/null +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Mocks/MockHttpContextFactory.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Server.Kestrel.Performance +{ + public class MockHttpContextFactory : IHttpContextFactory + { + private readonly object _lock = new object(); + private readonly Queue _cache = new Queue(); + + public HttpContext Create(IFeatureCollection featureCollection) + { + DefaultHttpContext httpContext; + + lock (_lock) + { + if (!_cache.TryDequeue(out httpContext)) + { + httpContext = new DefaultHttpContext(); + } + } + + httpContext.Initialize(featureCollection); + return httpContext; + } + + public void Dispose(HttpContext httpContext) + { + lock (_lock) + { + var defaultHttpContext = (DefaultHttpContext)httpContext; + + defaultHttpContext.Uninitialize(); + _cache.Enqueue(defaultHttpContext); + } + } + } +} diff --git a/src/Servers/Kestrel/shared/test/PipeWriterHttp2FrameExtensions.cs b/src/Servers/Kestrel/shared/test/PipeWriterHttp2FrameExtensions.cs index daf333e703..481278ae42 100644 --- a/src/Servers/Kestrel/shared/test/PipeWriterHttp2FrameExtensions.cs +++ b/src/Servers/Kestrel/shared/test/PipeWriterHttp2FrameExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Buffers; +using System.Buffers.Binary; using System.Collections.Generic; using System.IO.Pipelines; using System.Net.Http.HPack; @@ -24,9 +25,9 @@ namespace Microsoft.AspNetCore.Testing writer.Write(payload); } - public static void WriteStartStream(this PipeWriter writer, int streamId, Http2HeadersEnumerator headers, byte[] headerEncodingBuffer, bool endStream) + public static void WriteStartStream(this PipeWriter writer, int streamId, Http2HeadersEnumerator headers, byte[] headerEncodingBuffer, bool endStream, Http2Frame frame = null) { - var frame = new Http2Frame(); + frame ??= new Http2Frame(); frame.PrepareHeaders(Http2HeadersFrameFlags.NONE, streamId); var buffer = headerEncodingBuffer.AsSpan(); @@ -63,9 +64,9 @@ namespace Microsoft.AspNetCore.Testing } } - public static void WriteStartStream(this PipeWriter writer, int streamId, Span headerData, bool endStream) + public static void WriteStartStream(this PipeWriter writer, int streamId, Span headerData, bool endStream, Http2Frame frame = null) { - var frame = new Http2Frame(); + frame ??= new Http2Frame(); frame.PrepareHeaders(Http2HeadersFrameFlags.NONE, streamId); frame.PayloadLength = headerData.Length; frame.HeadersFlags = Http2HeadersFrameFlags.END_HEADERS; @@ -79,10 +80,9 @@ namespace Microsoft.AspNetCore.Testing writer.Write(headerData); } - public static void WriteData(this PipeWriter writer, int streamId, Memory data, bool endStream) + public static void WriteData(this PipeWriter writer, int streamId, Memory data, bool endStream, Http2Frame frame = null) { - var frame = new Http2Frame(); - + frame ??= new Http2Frame(); frame.PrepareData(streamId); frame.PayloadLength = data.Length; frame.DataFlags = endStream ? Http2DataFrameFlags.END_STREAM : Http2DataFrameFlags.NONE; @@ -90,5 +90,14 @@ namespace Microsoft.AspNetCore.Testing Http2FrameWriter.WriteHeader(frame, writer); writer.Write(data.Span); } + + public static void WriteWindowUpdateAsync(this PipeWriter writer, int streamId, int sizeIncrement, Http2Frame frame = null) + { + frame ??= new Http2Frame(); + frame.PrepareWindowUpdate(streamId, sizeIncrement); + Http2FrameWriter.WriteHeader(frame, writer); + BinaryPrimitives.WriteUInt32BigEndian(writer.GetSpan(4), (uint)sizeIncrement); + writer.Advance(4); + } } } diff --git a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs index 9277372a79..f80e5ad386 100644 --- a/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs +++ b/src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2TestBase.cs @@ -6,6 +6,7 @@ using System.Buffers; using System.Buffers.Binary; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Drawing; using System.IO; using System.IO.Pipelines; using System.Linq; @@ -1051,12 +1052,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests protected Task SendWindowUpdateAsync(int streamId, int sizeIncrement) { var outputWriter = _pair.Application.Output; - var frame = new Http2Frame(); - frame.PrepareWindowUpdate(streamId, sizeIncrement); - Http2FrameWriter.WriteHeader(frame, outputWriter); - var buffer = outputWriter.GetSpan(4); - BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)sizeIncrement); - outputWriter.Advance(4); + outputWriter.WriteWindowUpdateAsync(streamId, sizeIncrement); return FlushAsync(outputWriter); } diff --git a/src/Shared/BenchmarkRunner/Program.cs b/src/Shared/BenchmarkRunner/Program.cs index a1db1a50e8..87a01cf6c2 100644 --- a/src/Shared/BenchmarkRunner/Program.cs +++ b/src/Shared/BenchmarkRunner/Program.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System;