diff options
author | Brennan Conroy <brecon@microsoft.com> | 2022-11-12 03:49:06 +0300 |
---|---|---|
committer | Brennan Conroy <brecon@microsoft.com> | 2022-11-12 03:49:06 +0300 |
commit | 9c27c9af2db8c02d905398b1f4ff55998f752aa5 (patch) | |
tree | 95c047c21db4bcd79201928bb65764f2cefd1bca | |
parent | 04bb460447cc6b84ec88080a777b298ba70ce7d7 (diff) |
Remove array allocation from multi-span header parsingbrecon/multispan
3 files changed, 101 insertions, 39 deletions
diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs index 6bc2b7c168..6fb8c2c1e7 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpParser.cs @@ -288,6 +288,11 @@ public class HttpParser<TRequestHandler> : IHttpParser<TRequestHandler> where TR // This was a multi-line header. Advance the reader. reader.Advance(length); + if (reader.End) + { + return true; + } + continue; } @@ -384,36 +389,51 @@ public class HttpParser<TRequestHandler> : IHttpParser<TRequestHandler> where TR header = currentSlice.Slice(reader.Position, lineEnd); } - var headerSpan = header.ToSpan(); + byte[]? array = null; + int length = (int)header.Length; + Span<byte> span = length <= 256 ? stackalloc byte[length] : array = ArrayPool<byte>.Shared.Rent(length); - // 'a:b\n' or 'a:b\r\n' - var minHeaderSpan = _disableHttp1LineFeedTerminators ? 5 : 4; - if (headerSpan.Length < minHeaderSpan) + try { - RejectRequestHeader(headerSpan); - } + header.CopyTo(span); + span = span.Slice(0, length); - var terminatorSize = -1; + // 'a:b\n' or 'a:b\r\n' + var minHeaderSpan = _disableHttp1LineFeedTerminators ? 5 : 4; + if (span.Length < minHeaderSpan) + { + RejectRequestHeader(span); + } - if (headerSpan[^1] == ByteLF) - { - if (headerSpan[^2] == ByteCR) + var terminatorSize = -1; + + if (span[^1] == ByteLF) { - terminatorSize = 2; + if (span[^2] == ByteCR) + { + terminatorSize = 2; + } + else if (!_disableHttp1LineFeedTerminators) + { + terminatorSize = 1; + } } - else if (!_disableHttp1LineFeedTerminators) + + // Last chance to bail if the terminator size is not valid or the header doesn't parse. + if (terminatorSize == -1 || !TryTakeSingleHeader(handler, span.Slice(0, span.Length - terminatorSize))) { - terminatorSize = 1; + RejectRequestHeader(span); } - } - // Last chance to bail if the terminator size is not valid or the header doesn't parse. - if (terminatorSize == -1 || !TryTakeSingleHeader(handler, headerSpan.Slice(0, headerSpan.Length - terminatorSize))) + return span.Length; + } + finally { - RejectRequestHeader(headerSpan); + if (array is not null) + { + ArrayPool<byte>.Shared.Return(array); + } } - - return headerSpan.Length; } private static bool TryTakeSingleHeader(TRequestHandler handler, ReadOnlySpan<byte> headerLine) diff --git a/src/Servers/Kestrel/Core/test/HttpParserTests.cs b/src/Servers/Kestrel/Core/test/HttpParserTests.cs index 553cdbdfc8..9b69d0cc98 100644 --- a/src/Servers/Kestrel/Core/test/HttpParserTests.cs +++ b/src/Servers/Kestrel/Core/test/HttpParserTests.cs @@ -679,6 +679,22 @@ public class HttpParserTests : LoggedTest Assert.True(result); } + [Fact] + public void ParseLargeHeaderLineWithGratuitouslySplitBuffers() + { + // Must be greater than the stackalloc we use (256) to test the array pool path + var stringLength = 300; + var headers = $"Host: {new string('a', stringLength)}\r\nConnection: keep-alive\r\n"; + var parser = CreateParser(_nullTrace, false); + var buffer = BytePerSegmentTestSequenceFactory.Instance.CreateWithContent(headers); + + var requestHandler = new RequestHandler(); + var reader = new SequenceReader<byte>(buffer); + var result = parser.ParseHeaders(requestHandler, ref reader); + + Assert.True(result); + } + private bool ParseRequestLine(IHttpParser<RequestHandler> parser, RequestHandler requestHandler, ReadOnlySequence<byte> readableBuffer, out SequencePosition consumed, out SequencePosition examined) { var reader = new SequenceReader<byte>(readableBuffer); diff --git a/src/Servers/Kestrel/perf/Microbenchmarks/HttpParserBenchmark.cs b/src/Servers/Kestrel/perf/Microbenchmarks/HttpParserBenchmark.cs index 672aa710db..ee1d9b7eb1 100644 --- a/src/Servers/Kestrel/perf/Microbenchmarks/HttpParserBenchmark.cs +++ b/src/Servers/Kestrel/perf/Microbenchmarks/HttpParserBenchmark.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.IO.Pipelines; using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; @@ -12,43 +13,68 @@ public class HttpParserBenchmark : IHttpRequestLineHandler, IHttpHeadersHandler private readonly HttpParser<Adapter> _parser = new HttpParser<Adapter>(); private ReadOnlySequence<byte> _buffer; + private ReadOnlySequence<byte> _multispanHeader; - [Benchmark(Baseline = true, OperationsPerInvoke = RequestParsingData.InnerLoopCount)] - public void PlaintextTechEmpower() + [GlobalSetup] + public void Setup() { - for (var i = 0; i < RequestParsingData.InnerLoopCount; i++) - { - InsertData(RequestParsingData.PlaintextTechEmpowerRequest); - ParseData(); - } + var segment = new BufferSegment(); + var split = RequestParsingData.UnicodeRequest.Length / 2; + segment.SetOwnedMemory(RequestParsingData.UnicodeRequest.AsSpan(0, split).ToArray()); + segment.End = split; + var next = new BufferSegment(); + next.SetOwnedMemory(RequestParsingData.UnicodeRequest.AsSpan(split).ToArray()); + next.End = split; + segment.SetNext(next); + _multispanHeader = new ReadOnlySequence<byte>(segment, 0, next, next.Memory.Length); } - [Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount)] - public void JsonTechEmpower() - { - for (var i = 0; i < RequestParsingData.InnerLoopCount; i++) - { - InsertData(RequestParsingData.JsonTechEmpowerRequest); - ParseData(); - } - } + //[Benchmark(Baseline = true, OperationsPerInvoke = RequestParsingData.InnerLoopCount)] + //public void PlaintextTechEmpower() + //{ + // for (var i = 0; i < RequestParsingData.InnerLoopCount; i++) + // { + // InsertData(RequestParsingData.PlaintextTechEmpowerRequest); + // ParseData(); + // } + //} + + //[Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount)] + //public void JsonTechEmpower() + //{ + // for (var i = 0; i < RequestParsingData.InnerLoopCount; i++) + // { + // InsertData(RequestParsingData.JsonTechEmpowerRequest); + // ParseData(); + // } + //} + + //[Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount)] + //public void LiveAspNet() + //{ + // for (var i = 0; i < RequestParsingData.InnerLoopCount; i++) + // { + // InsertData(RequestParsingData.LiveaspnetRequest); + // ParseData(); + // } + //} [Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount)] - public void LiveAspNet() + public void Unicode() { for (var i = 0; i < RequestParsingData.InnerLoopCount; i++) { - InsertData(RequestParsingData.LiveaspnetRequest); + InsertData(RequestParsingData.UnicodeRequest); ParseData(); } } [Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount)] - public void Unicode() + public void MultispanUnicodeHeader() { for (var i = 0; i < RequestParsingData.InnerLoopCount; i++) { - InsertData(RequestParsingData.UnicodeRequest); + _buffer = _multispanHeader; ParseData(); } } |