Read server response in Http2ConnectionBenchmark (#19694)

This commit is contained in:
James Newton-King 2020-03-10 15:50:16 +13:00 committed by GitHub
parent cd6e6ae0bc
commit 92e98b7ede
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 158 additions and 28 deletions

View File

@ -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<byte> _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();
}
}

View File

@ -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<DefaultHttpContext> _cache = new Queue<DefaultHttpContext>();
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);
}
}
}
}

View File

@ -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<byte> headerData, bool endStream)
public static void WriteStartStream(this PipeWriter writer, int streamId, Span<byte> 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<byte> data, bool endStream)
public static void WriteData(this PipeWriter writer, int streamId, Memory<byte> 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);
}
}
}

View File

@ -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);
}

View File

@ -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;