aspnetcore/benchmarks/Kestrel.Performance/InMemoryTransportBenchmark.cs

243 lines
9.1 KiB
C#

// 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 BenchmarkDotNet.Attributes;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO.Pipelines;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Server.Kestrel.Performance
{
public class InMemoryTransportBenchmark
{
private const string _plaintextExpectedResponse =
"HTTP/1.1 200 OK\r\n" +
"Date: Fri, 02 Mar 2018 18:37:05 GMT\r\n" +
"Content-Type: text/plain\r\n" +
"Server: Kestrel\r\n" +
"Content-Length: 13\r\n" +
"\r\n" +
"Hello, World!";
private static readonly string _plaintextPipelinedExpectedResponse =
string.Concat(Enumerable.Repeat(_plaintextExpectedResponse, RequestParsingData.Pipelining));
private IWebHost _host;
private InMemoryConnection _connection;
[GlobalSetup(Target = nameof(Plaintext) + "," + nameof(PlaintextPipelined))]
public void GlobalSetupPlaintext()
{
var transportFactory = new InMemoryTransportFactory(connectionsPerEndPoint: 1);
_host = new WebHostBuilder()
// Prevent VS from attaching to hosting startup which could impact results
.UseSetting("preventHostingStartup", "true")
.UseKestrel()
// Bind to a single non-HTTPS endpoint
.UseUrls("http://127.0.0.1:5000")
.ConfigureServices(services => services.AddSingleton<ITransportFactory>(transportFactory))
.Configure(app => app.UseMiddleware<PlaintextMiddleware>())
.Build();
_host.Start();
// Ensure there is a single endpoint and single connection
_connection = transportFactory.Connections.Values.Single().Single();
ValidateResponseAsync(RequestParsingData.PlaintextTechEmpowerRequest, _plaintextExpectedResponse).Wait();
ValidateResponseAsync(RequestParsingData.PlaintextTechEmpowerPipelinedRequests, _plaintextPipelinedExpectedResponse).Wait();
}
private async Task ValidateResponseAsync(byte[] request, string expectedResponse)
{
await _connection.SendRequestAsync(request);
var response = Encoding.ASCII.GetString(await _connection.GetResponseAsync(expectedResponse.Length));
// Exclude date header since the value changes on every request
var expectedResponseLines = expectedResponse.Split("\r\n").Where(s => !s.StartsWith("Date:"));
var responseLines = response.Split("\r\n").Where(s => !s.StartsWith("Date:"));
if (!Enumerable.SequenceEqual(expectedResponseLines, responseLines))
{
throw new InvalidOperationException(string.Join(Environment.NewLine,
"Invalid response", "Expected:", expectedResponse, "Actual:", response));
}
}
[GlobalCleanup]
public void GlobalCleanup()
{
_host.Dispose();
}
[Benchmark]
public async Task Plaintext()
{
await _connection.SendRequestAsync(RequestParsingData.PlaintextTechEmpowerRequest);
await _connection.ReadResponseAsync(_plaintextExpectedResponse.Length);
}
[Benchmark(OperationsPerInvoke = RequestParsingData.Pipelining)]
public async Task PlaintextPipelined()
{
await _connection.SendRequestAsync(RequestParsingData.PlaintextTechEmpowerPipelinedRequests);
await _connection.ReadResponseAsync(_plaintextPipelinedExpectedResponse.Length);
}
public class InMemoryTransportFactory : ITransportFactory
{
private readonly int _connectionsPerEndPoint;
private readonly Dictionary<IEndPointInformation, IReadOnlyList<InMemoryConnection>> _connections =
new Dictionary<IEndPointInformation, IReadOnlyList<InMemoryConnection>>();
public IReadOnlyDictionary<IEndPointInformation, IReadOnlyList<InMemoryConnection>> Connections => _connections;
public InMemoryTransportFactory(int connectionsPerEndPoint)
{
_connectionsPerEndPoint = connectionsPerEndPoint;
}
public ITransport Create(IEndPointInformation endPointInformation, IConnectionHandler handler)
{
var connections = new InMemoryConnection[_connectionsPerEndPoint];
for (var i = 0; i < _connectionsPerEndPoint; i++)
{
connections[i] = new InMemoryConnection();
}
_connections.Add(endPointInformation, connections);
return new InMemoryTransport(handler, connections);
}
}
public class InMemoryTransport : ITransport
{
private readonly IConnectionHandler _handler;
private readonly IReadOnlyList<InMemoryConnection> _connections;
public InMemoryTransport(IConnectionHandler handler, IReadOnlyList<InMemoryConnection> connections)
{
_handler = handler;
_connections = connections;
}
public Task BindAsync()
{
foreach (var connection in _connections)
{
_handler.OnConnection(connection);
}
return Task.CompletedTask;
}
public Task StopAsync()
{
return Task.CompletedTask;
}
public Task UnbindAsync()
{
return Task.CompletedTask;
}
}
public class InMemoryConnection : TransportConnection
{
public ValueTask<FlushResult> SendRequestAsync(byte[] request)
{
return Input.WriteAsync(request);
}
// Reads response as efficiently as possible (similar to LibuvTransport), but doesn't return anything
public async Task ReadResponseAsync(int length)
{
while (length > 0)
{
var result = await Output.ReadAsync();
var buffer = result.Buffer;
length -= (int)buffer.Length;
Output.AdvanceTo(buffer.End);
}
if (length < 0)
{
throw new InvalidOperationException($"Invalid response, length={length}");
}
}
// Returns response so it can be validated, but is slower and allocates more than ReadResponseAsync()
public async Task<byte[]> GetResponseAsync(int length)
{
while (true)
{
var result = await Output.ReadAsync();
var buffer = result.Buffer;
var consumed = buffer.Start;
var examined = buffer.End;
try
{
if (buffer.Length >= length)
{
var response = buffer.Slice(0, length);
consumed = response.End;
examined = response.End;
return response.ToArray();
}
}
finally
{
Output.AdvanceTo(consumed, examined);
}
}
}
}
// Copied from https://github.com/aspnet/benchmarks/blob/dev/src/Benchmarks/Middleware/PlaintextMiddleware.cs
public class PlaintextMiddleware
{
private static readonly PathString _path = new PathString("/plaintext");
private static readonly byte[] _helloWorldPayload = Encoding.UTF8.GetBytes("Hello, World!");
private readonly RequestDelegate _next;
public PlaintextMiddleware(RequestDelegate next)
{
_next = next;
}
public Task Invoke(HttpContext httpContext)
{
if (httpContext.Request.Path.StartsWithSegments(_path, StringComparison.Ordinal))
{
return WriteResponse(httpContext.Response);
}
return _next(httpContext);
}
public static Task WriteResponse(HttpResponse response)
{
var payloadLength = _helloWorldPayload.Length;
response.StatusCode = 200;
response.ContentType = "text/plain";
response.ContentLength = payloadLength;
return response.Body.WriteAsync(_helloWorldPayload, 0, payloadLength);
}
}
}
}