[Platform] Speedups (#2569)

- Move ProcessRequest into ParseHttpRequest (out of state machine)
- Use struct adapter for parsing
- Non-virtual calls
- RequestType to enum
- Readonly statics and early init statics
- Early init path statics
- Faster Json fall through
- CountingBufferWriter -> Smaller BufferWriter
- Pre-init date static
- Smaller WriteNumeric
This commit is contained in:
Ben Adams 2018-05-14 03:26:30 +01:00 committed by David Fowler
parent 84ca10a45d
commit c52a3bf534
10 changed files with 443 additions and 186 deletions

View File

@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
namespace PlatformBenchmarks
{
public struct AsciiString : IEquatable<AsciiString>
public readonly struct AsciiString : IEquatable<AsciiString>
{
private readonly byte[] _data;

View File

@ -0,0 +1,171 @@
// 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;
using System.Buffers;
using System.IO.Pipelines;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
namespace PlatformBenchmarks
{
public partial class BenchmarkApplication : IHttpConnection
{
private State _state;
public PipeReader Reader { get; set; }
public PipeWriter Writer { get; set; }
private HttpParser<ParsingAdapter> Parser { get; } = new HttpParser<ParsingAdapter>();
public async Task ExecuteAsync()
{
try
{
await ProcessRequestsAsync();
Reader.Complete();
}
catch (Exception ex)
{
Reader.Complete(ex);
}
finally
{
Writer.Complete();
}
}
private async Task ProcessRequestsAsync()
{
while (true)
{
var task = Reader.ReadAsync();
if (!task.IsCompleted)
{
// No more data in the input
await OnReadCompletedAsync();
}
var result = await task;
if (!ParseHttpRequest(ref result))
{
break;
}
if (_state == State.Body)
{
await ProcessRequestAsync();
_state = State.StartLine;
}
}
}
// Should be `in` but ReadResult isn't readonly struct
private bool ParseHttpRequest(ref ReadResult result)
{
var buffer = result.Buffer;
var consumed = buffer.Start;
var examined = buffer.End;
var state = _state;
if (!buffer.IsEmpty)
{
var parsingStartLine = state == State.StartLine;
if (parsingStartLine)
{
if (Parser.ParseRequestLine(new ParsingAdapter(this), buffer, out consumed, out examined))
{
state = State.Headers;
}
}
if (state == State.Headers)
{
if (parsingStartLine)
{
buffer = buffer.Slice(consumed);
}
if (Parser.ParseHeaders(new ParsingAdapter(this), buffer, out consumed, out examined, out int consumedBytes))
{
state = State.Body;
}
}
if (state != State.Body && result.IsCompleted)
{
ThrowUnexpectedEndOfData();
}
}
else if (result.IsCompleted)
{
return false;
}
_state = state;
Reader.AdvanceTo(consumed, examined);
return true;
}
public void OnHeader(Span<byte> name, Span<byte> value)
{
}
public async ValueTask OnReadCompletedAsync()
{
await Writer.FlushAsync();
}
private static void ThrowUnexpectedEndOfData()
{
throw new InvalidOperationException("Unexpected end of data!");
}
private enum State
{
StartLine,
Headers,
Body
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static BufferWriter<WriterAdapter> GetWriter(PipeWriter pipeWriter)
=> new BufferWriter<WriterAdapter>(new WriterAdapter(pipeWriter));
private struct WriterAdapter : IBufferWriter<byte>
{
public PipeWriter Writer;
public WriterAdapter(PipeWriter writer)
=> Writer = writer;
public void Advance(int count)
=> Writer.Advance(count);
public Memory<byte> GetMemory(int sizeHint = 0)
=> Writer.GetMemory(sizeHint);
public Span<byte> GetSpan(int sizeHint = 0)
=> Writer.GetSpan(sizeHint);
}
private struct ParsingAdapter : IHttpRequestLineHandler, IHttpHeadersHandler
{
public BenchmarkApplication RequestHandler;
public ParsingAdapter(BenchmarkApplication requestHandler)
=> RequestHandler = requestHandler;
public void OnHeader(Span<byte> name, Span<byte> value)
=> RequestHandler.OnHeader(name, value);
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded)
=> RequestHandler.OnStartLine(method, version, target, path, query, customMethod, pathEncoded);
}
}
}

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Buffers;
using System.IO.Pipelines;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
@ -10,57 +9,55 @@ using Utf8Json;
namespace PlatformBenchmarks
{
public class BenchmarkApplication : HttpConnection
public partial class BenchmarkApplication
{
private static AsciiString _crlf = "\r\n";
private static AsciiString _eoh = "\r\n\r\n"; // End Of Headers
private static AsciiString _http11OK = "HTTP/1.1 200 OK\r\n";
private static AsciiString _headerServer = "Server: Custom";
private static AsciiString _headerContentLength = "Content-Length: ";
private static AsciiString _headerContentLengthZero = "Content-Length: 0\r\n";
private static AsciiString _headerContentTypeText = "Content-Type: text/plain\r\n";
private static AsciiString _headerContentTypeJson = "Content-Type: application/json\r\n";
private readonly static AsciiString _applicationName = "Kestrel Platform-Level Application";
public static AsciiString ApplicationName => _applicationName;
private readonly static AsciiString _crlf = "\r\n";
private readonly static AsciiString _eoh = "\r\n\r\n"; // End Of Headers
private readonly static AsciiString _http11OK = "HTTP/1.1 200 OK\r\n";
private readonly static AsciiString _headerServer = "Server: Custom";
private readonly static AsciiString _headerContentLength = "Content-Length: ";
private readonly static AsciiString _headerContentLengthZero = "Content-Length: 0\r\n";
private readonly static AsciiString _headerContentTypeText = "Content-Type: text/plain\r\n";
private readonly static AsciiString _headerContentTypeJson = "Content-Type: application/json\r\n";
private static AsciiString _plainTextBody = "Hello, World!";
private readonly static AsciiString _plainTextBody = "Hello, World!";
private static class Paths
public static class Paths
{
public static AsciiString Plaintext = "/plaintext";
public static AsciiString Json = "/json";
public readonly static AsciiString Plaintext = "/plaintext";
public readonly static AsciiString Json = "/json";
}
private bool _isPlainText;
private bool _isJson;
private RequestType _requestType;
public override void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded)
public void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded)
{
if (path.StartsWith(Paths.Plaintext) && method == HttpMethod.Get)
var requestType = RequestType.NotRecognized;
if (method == HttpMethod.Get)
{
_isPlainText = true;
}
else if (path.StartsWith(Paths.Json) && method == HttpMethod.Get)
{
_isJson = true;
}
else
{
_isPlainText = false;
_isJson = false;
if (Paths.Plaintext.Length <= path.Length && path.StartsWith(Paths.Plaintext))
{
requestType = RequestType.PlainText;
}
else if (Paths.Json.Length <= path.Length && path.StartsWith(Paths.Json))
{
requestType = RequestType.Json;
}
}
_requestType = requestType;
}
public override void OnHeader(Span<byte> name, Span<byte> value)
public ValueTask ProcessRequestAsync()
{
}
public override ValueTask ProcessRequestAsync()
{
if (_isPlainText)
if (_requestType == RequestType.PlainText)
{
PlainText(Writer);
}
else if (_isJson)
else if (_requestType == RequestType.Json)
{
Json(Writer);
}
@ -72,13 +69,10 @@ namespace PlatformBenchmarks
return default;
}
public override async ValueTask OnReadCompletedAsync()
{
await Writer.FlushAsync();
}
private static void PlainText(PipeWriter pipeWriter)
{
var writer = new CountingBufferWriter<PipeWriter>(pipeWriter);
var writer = GetWriter(pipeWriter);
// HTTP 1.1 OK
writer.Write(_http11OK);
@ -93,7 +87,7 @@ namespace PlatformBenchmarks
// Content-Length header
writer.Write(_headerContentLength);
writer.WriteNumeric((ulong)_plainTextBody.Length);
writer.WriteNumeric((uint)_plainTextBody.Length);
// End of headers
writer.Write(_eoh);
@ -105,7 +99,7 @@ namespace PlatformBenchmarks
private static void Json(PipeWriter pipeWriter)
{
var writer = new CountingBufferWriter<PipeWriter>(pipeWriter);
var writer = GetWriter(pipeWriter);
// HTTP 1.1 OK
writer.Write(_http11OK);
@ -122,7 +116,7 @@ namespace PlatformBenchmarks
// Content-Length header
writer.Write(_headerContentLength);
var jsonPayload = JsonSerializer.SerializeUnsafe(new { message = "Hello, World!" });
writer.WriteNumeric((ulong)jsonPayload.Count);
writer.WriteNumeric((uint)jsonPayload.Count);
// End of headers
writer.Write(_eoh);
@ -134,7 +128,7 @@ namespace PlatformBenchmarks
private static void Default(PipeWriter pipeWriter)
{
var writer = new CountingBufferWriter<PipeWriter>(pipeWriter);
var writer = GetWriter(pipeWriter);
// HTTP 1.1 OK
writer.Write(_http11OK);
@ -152,5 +146,12 @@ namespace PlatformBenchmarks
writer.Write(_crlf);
writer.Commit();
}
private enum RequestType
{
NotRecognized,
PlainText,
Json
}
}
}

View File

@ -0,0 +1,104 @@
// 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;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace PlatformBenchmarks
{
// Same as KestrelHttpServer\src\Kestrel.Core\Internal\Http\PipelineExtensions.cs
// However methods accept T : struct, IBufferWriter<byte> rather than PipeWriter.
// This allows a struct wrapper to turn CountingBufferWriter into a non-shared generic,
// while still offering the WriteNumeric extension.
public static class BufferExtensions
{
private const int _maxULongByteLength = 20;
[ThreadStatic]
private static byte[] _numericBytesScratch;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static unsafe void WriteNumeric<T>(ref this BufferWriter<T> buffer, uint number)
where T : struct, IBufferWriter<byte>
{
const byte AsciiDigitStart = (byte)'0';
var span = buffer.Span;
var bytesLeftInBlock = span.Length;
// Fast path, try copying to the available memory directly
var advanceBy = 0;
fixed (byte* output = &MemoryMarshal.GetReference(span))
{
var start = output;
if (number < 10 && bytesLeftInBlock >= 1)
{
start[0] = (byte)(number + AsciiDigitStart);
advanceBy = 1;
}
else if (number < 100 && bytesLeftInBlock >= 2)
{
var tens = (byte)((number * 205u) >> 11); // div10, valid to 1028
start[0] = (byte)(tens + AsciiDigitStart);
start[1] = (byte)(number - (tens * 10) + AsciiDigitStart);
advanceBy = 2;
}
else if (number < 1000 && bytesLeftInBlock >= 3)
{
var digit0 = (byte)((number * 41u) >> 12); // div100, valid to 1098
var digits01 = (byte)((number * 205u) >> 11); // div10, valid to 1028
start[0] = (byte)(digit0 + AsciiDigitStart);
start[1] = (byte)(digits01 - (digit0 * 10) + AsciiDigitStart);
start[2] = (byte)(number - (digits01 * 10) + AsciiDigitStart);
advanceBy = 3;
}
}
if (advanceBy > 0)
{
buffer.Advance(advanceBy);
}
else
{
WriteNumericMultiWrite(ref buffer, number);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void WriteNumericMultiWrite<T>(ref this BufferWriter<T> buffer, uint number)
where T : struct, IBufferWriter<byte>
{
const byte AsciiDigitStart = (byte)'0';
var value = number;
var position = _maxULongByteLength;
var byteBuffer = NumericBytesScratch;
do
{
// Consider using Math.DivRem() if available
var quotient = value / 10;
byteBuffer[--position] = (byte)(AsciiDigitStart + (value - quotient * 10)); // 0x30 = '0'
value = quotient;
}
while (value != 0);
var length = _maxULongByteLength - position;
buffer.Write(new ReadOnlySpan<byte>(byteBuffer, position, length));
}
private static byte[] NumericBytesScratch => _numericBytesScratch ?? CreateNumericBytesScratch();
[MethodImpl(MethodImplOptions.NoInlining)]
private static byte[] CreateNumericBytesScratch()
{
var bytes = new byte[_maxULongByteLength];
_numericBytesScratch = bytes;
return bytes;
}
}
}

View File

@ -0,0 +1,94 @@
// 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;
using System.Buffers;
using System.Runtime.CompilerServices;
namespace PlatformBenchmarks
{
public ref struct BufferWriter<T> where T : IBufferWriter<byte>
{
private T _output;
private Span<byte> _span;
private int _buffered;
public BufferWriter(T output)
{
_buffered = 0;
_output = output;
_span = output.GetSpan();
}
public Span<byte> Span => _span;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Commit()
{
var buffered = _buffered;
if (buffered > 0)
{
_buffered = 0;
_output.Advance(buffered);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Advance(int count)
{
_buffered += count;
_span = _span.Slice(count);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Write(ReadOnlySpan<byte> source)
{
if (_span.Length >= source.Length)
{
source.CopyTo(_span);
Advance(source.Length);
}
else
{
WriteMultiBuffer(source);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Ensure(int count = 1)
{
if (_span.Length < count)
{
EnsureMore(count);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void EnsureMore(int count = 0)
{
if (_buffered > 0)
{
Commit();
}
_output.GetMemory(count);
_span = _output.GetSpan();
}
private void WriteMultiBuffer(ReadOnlySpan<byte> source)
{
while (source.Length > 0)
{
if (_span.Length == 0)
{
EnsureMore();
}
var writable = Math.Min(source.Length, _span.Length);
source.Slice(0, writable).CopyTo(_span);
source = source.Slice(writable);
Advance(writable);
}
}
}
}

View File

@ -37,8 +37,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
s_headerBytesScratch[suffixIndex] = (byte)'\r';
s_headerBytesScratch[suffixIndex + 1] = (byte)'\n';
SetDateValues(DateTimeOffset.UtcNow);
SyncDateTimer();
}
public static void SyncDateTimer() => s_timer.Change(1000, 1000);
public static ReadOnlySpan<byte> HeaderBytes => s_headerBytesMaster;
private static void SetDateValues(DateTimeOffset value)

View File

@ -1,161 +1,29 @@
// 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;
using System.Buffers;
using System.IO.Pipelines;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
namespace PlatformBenchmarks
{
public static class HttpApplicationConnectionBuilderExtensions
{
public static IConnectionBuilder UseHttpApplication<TConnection>(this IConnectionBuilder builder) where TConnection : HttpConnection, new()
public static IConnectionBuilder UseHttpApplication<TConnection>(this IConnectionBuilder builder) where TConnection : IHttpConnection, new()
{
return builder.Use(next => new HttpApplication<TConnection>().ExecuteAsync);
}
}
public class HttpApplication<TConnection> where TConnection : HttpConnection, new()
public class HttpApplication<TConnection> where TConnection : IHttpConnection, new()
{
public Task ExecuteAsync(ConnectionContext connection)
{
var parser = new HttpParser<HttpConnection>();
var httpConnection = new TConnection
{
Parser = parser,
Reader = connection.Transport.Input,
Writer = connection.Transport.Output
};
return httpConnection.ExecuteAsync();
}
}
public class HttpConnection : IHttpHeadersHandler, IHttpRequestLineHandler
{
private State _state;
public PipeReader Reader { get; set; }
public PipeWriter Writer { get; set; }
internal HttpParser<HttpConnection> Parser { get; set; }
public virtual void OnHeader(Span<byte> name, Span<byte> value)
{
}
public virtual void OnStartLine(HttpMethod method, HttpVersion version, Span<byte> target, Span<byte> path, Span<byte> query, Span<byte> customMethod, bool pathEncoded)
{
}
public virtual ValueTask ProcessRequestAsync()
{
return default;
}
public virtual ValueTask OnReadCompletedAsync()
{
return default;
}
public async Task ExecuteAsync()
{
try
{
await ProcessRequestsAsync();
Reader.Complete();
}
catch (Exception ex)
{
Reader.Complete(ex);
}
finally
{
Writer.Complete();
}
}
private async Task ProcessRequestsAsync()
{
while (true)
{
var task = Reader.ReadAsync();
if (!task.IsCompleted)
{
// No more data in the input
await OnReadCompletedAsync();
}
var result = await task;
var buffer = result.Buffer;
var consumed = buffer.Start;
var examined = buffer.End;
if (!buffer.IsEmpty)
{
ParseHttpRequest(buffer, out consumed, out examined);
if (_state != State.Body && result.IsCompleted)
{
ThrowUnexpectedEndOfData();
}
}
else if (result.IsCompleted)
{
break;
}
Reader.AdvanceTo(consumed, examined);
if (_state == State.Body)
{
await ProcessRequestAsync();
_state = State.StartLine;
}
}
}
private void ParseHttpRequest(in ReadOnlySequence<byte> buffer, out SequencePosition consumed, out SequencePosition examined)
{
consumed = buffer.Start;
examined = buffer.End;
var parsingStartLine = _state == State.StartLine;
if (parsingStartLine)
{
if (Parser.ParseRequestLine(this, buffer, out consumed, out examined))
{
_state = State.Headers;
}
}
if (_state == State.Headers)
{
if (Parser.ParseHeaders(this, parsingStartLine ? buffer.Slice(consumed) : buffer, out consumed, out examined, out int consumedBytes))
{
_state = State.Body;
}
}
}
private static void ThrowUnexpectedEndOfData()
{
throw new InvalidOperationException("Unexpected end of data!");
}
private enum State
{
StartLine,
Headers,
Body
}
}
}

View File

@ -0,0 +1,17 @@
// 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.IO.Pipelines;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
namespace PlatformBenchmarks
{
public interface IHttpConnection : IHttpHeadersHandler, IHttpRequestLineHandler
{
PipeReader Reader { get; set; }
PipeWriter Writer { get; set; }
Task ExecuteAsync();
ValueTask OnReadCompletedAsync();
}
}

View File

@ -3,12 +3,9 @@
using System;
using System.Net;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace PlatformBenchmarks
{
@ -16,6 +13,11 @@ namespace PlatformBenchmarks
{
public static void Main(string[] args)
{
Console.WriteLine(BenchmarkApplication.ApplicationName);
Console.WriteLine(BenchmarkApplication.Paths.Plaintext);
Console.WriteLine(BenchmarkApplication.Paths.Json);
DateHeader.SyncDateTimer();
BuildWebHost(args).Run();
}

View File

@ -2,10 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Net.WebSockets;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
namespace PlatformBenchmarks
{