From 0404bcc58c6de8740b16e393de325bf982eefaa4 Mon Sep 17 00:00:00 2001 From: Cesar Blum Silveira Date: Wed, 1 Mar 2017 17:29:13 -0800 Subject: [PATCH] Add more microbenchmarks. --- .../FrameParsingOverhead.cs | 135 +++++++++++++++++ .../KestrelHttpParser.cs | 77 ++++++++++ .../Program.cs | 12 +- .../RequestParsing.cs | 142 ++++++------------ .../RequestParsingData.cs | 62 ++++++++ 5 files changed, 334 insertions(+), 94 deletions(-) create mode 100644 test/Microsoft.AspNetCore.Server.Kestrel.Performance/FrameParsingOverhead.cs create mode 100644 test/Microsoft.AspNetCore.Server.Kestrel.Performance/KestrelHttpParser.cs create mode 100644 test/Microsoft.AspNetCore.Server.Kestrel.Performance/RequestParsingData.cs diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/FrameParsingOverhead.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/FrameParsingOverhead.cs new file mode 100644 index 0000000000..acb39238bc --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/FrameParsingOverhead.cs @@ -0,0 +1,135 @@ +// 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.IO.Pipelines; +using System.Text; +using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Server.Kestrel.Internal.Http; +using Microsoft.AspNetCore.Testing; + +namespace Microsoft.AspNetCore.Server.Kestrel.Performance +{ + [Config(typeof(CoreConfig))] + public class FrameParsingOverhead + { + private const int InnerLoopCount = 512; + + public ReadableBuffer _buffer; + public Frame _frame; + + [Setup] + public void Setup() + { + var connectionContext = new MockConnection(new KestrelServerOptions()); + connectionContext.ListenerContext.ServiceContext.HttpParserFactory = frame => NullParser.Instance; + + _frame = new Frame(application: null, context: connectionContext); + } + + [Benchmark(Baseline = true, OperationsPerInvoke = InnerLoopCount)] + public void FrameOverheadTotal() + { + for (var i = 0; i < InnerLoopCount; i++) + { + ParseRequest(); + } + } + + [Benchmark(OperationsPerInvoke = InnerLoopCount)] + public void FrameOverheadRequestLine() + { + for (var i = 0; i < InnerLoopCount; i++) + { + ParseRequestLine(); + } + } + + [Benchmark(OperationsPerInvoke = InnerLoopCount)] + public void FrameOverheadRequestHeaders() + { + for (var i = 0; i < InnerLoopCount; i++) + { + ParseRequestHeaders(); + } + } + + private void ParseRequest() + { + _frame.Reset(); + + if (!_frame.TakeStartLine(_buffer, out var consumed, out var examined)) + { + RequestParsing.ThrowInvalidRequestLine(); + } + + _frame.InitializeHeaders(); + + if (!_frame.TakeMessageHeaders(_buffer, out consumed, out examined)) + { + RequestParsing.ThrowInvalidRequestHeaders(); + } + } + + private void ParseRequestLine() + { + _frame.Reset(); + + if (!_frame.TakeStartLine(_buffer, out var consumed, out var examined)) + { + RequestParsing.ThrowInvalidRequestLine(); + } + } + + private void ParseRequestHeaders() + { + _frame.Reset(); + _frame.InitializeHeaders(); + + if (!_frame.TakeMessageHeaders(_buffer, out var consumed, out var examined)) + { + RequestParsing.ThrowInvalidRequestHeaders(); + } + } + + private class NullParser : IHttpParser + { + private readonly byte[] _target = Encoding.ASCII.GetBytes("/plaintext"); + private readonly byte[] _hostHeaderName = Encoding.ASCII.GetBytes("Host"); + private readonly byte[] _hostHeaderValue = Encoding.ASCII.GetBytes("www.example.com"); + private readonly byte[] _acceptHeaderName = Encoding.ASCII.GetBytes("Accept"); + private readonly byte[] _acceptHeaderValue = Encoding.ASCII.GetBytes("text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7\r\n\r\n"); + private readonly byte[] _connectionHeaderName = Encoding.ASCII.GetBytes("Connection"); + private readonly byte[] _connectionHeaderValue = Encoding.ASCII.GetBytes("keep-alive"); + + public static readonly NullParser Instance = new NullParser(); + + public bool ParseHeaders(T handler, ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined, out int consumedBytes) where T : IHttpHeadersHandler + { + handler.OnHeader(new Span(_hostHeaderName), new Span(_hostHeaderValue)); + handler.OnHeader(new Span(_acceptHeaderName), new Span(_acceptHeaderValue)); + handler.OnHeader(new Span(_connectionHeaderName), new Span(_connectionHeaderValue)); + + consumedBytes = 0; + consumed = buffer.Start; + examined = buffer.End; + + return true; + } + + public bool ParseRequestLine(T handler, ReadableBuffer buffer, out ReadCursor consumed, out ReadCursor examined) where T : IHttpRequestLineHandler + { + handler.OnStartLine(HttpMethod.Get, HttpVersion.Http11, new Span(_target), new Span(_target), Span.Empty, Span.Empty); + + consumed = buffer.Start; + examined = buffer.End; + + return true; + } + + public void Reset() + { + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/KestrelHttpParser.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/KestrelHttpParser.cs new file mode 100644 index 0000000000..f77346ee8b --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/KestrelHttpParser.cs @@ -0,0 +1,77 @@ +// 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.IO.Pipelines; +using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Server.Kestrel.Internal.Http; + +namespace Microsoft.AspNetCore.Server.Kestrel.Performance +{ + [Config(typeof(CoreConfig))] + + public class KestrelHttpParser : IHttpRequestLineHandler, IHttpHeadersHandler + { + private readonly Internal.Http.KestrelHttpParser _parser = new Internal.Http.KestrelHttpParser(log: null); + + private ReadableBuffer _buffer; + + [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 LiveAspNet() + { + for (var i = 0; i < RequestParsingData.InnerLoopCount; i++) + { + InsertData(RequestParsingData.LiveaspnetRequest); + ParseData(); + } + } + + [Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount)] + public void Unicode() + { + for (var i = 0; i < RequestParsingData.InnerLoopCount; i++) + { + InsertData(RequestParsingData.UnicodeRequest); + ParseData(); + } + } + + private void InsertData(byte[] data) + { + _buffer = ReadableBuffer.Create(data); + } + + private void ParseData() + { + if (!_parser.ParseRequestLine(this, _buffer, out var consumed, out var examined)) + { + RequestParsing.ThrowInvalidRequestHeaders(); + } + + _buffer = _buffer.Slice(consumed, _buffer.End); + + if (!_parser.ParseHeaders(this, _buffer, out consumed, out examined, out var consumedBytes)) + { + RequestParsing.ThrowInvalidRequestHeaders(); + } + } + + public void OnStartLine(HttpMethod method, HttpVersion version, Span target, Span path, Span query, Span customMethod) + { + } + + public void OnHeader(Span name, Span value) + { + } + } +} diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/Program.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/Program.cs index e6bea9e92c..95ca5909db 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/Program.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/Program.cs @@ -40,10 +40,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance { BenchmarkRunner.Run(); } - if (type.HasFlag(BenchmarkType.KnownStrings)) + if (type.HasFlag(BenchmarkType.KnownStrings)) { BenchmarkRunner.Run(); } + if (type.HasFlag(BenchmarkType.KestrelHttpParser)) + { + BenchmarkRunner.Run(); + } + if (type.HasFlag(BenchmarkType.FrameParsingOverhead)) + { + BenchmarkRunner.Run(); + } } } @@ -54,6 +62,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance Writing = 2, Throughput = 4, KnownStrings = 8, + KestrelHttpParser = 16, + FrameParsingOverhead = 32, // add new ones in powers of two - e.g. 2,4,8,16... All = uint.MaxValue diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/RequestParsing.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/RequestParsing.cs index 444e4022b6..90bd7a16c5 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/RequestParsing.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/RequestParsing.cs @@ -3,8 +3,6 @@ using System; using System.IO.Pipelines; -using System.Linq; -using System.Text; using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Server.Kestrel.Internal.Http; using Microsoft.AspNetCore.Testing; @@ -14,107 +12,82 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance [Config(typeof(CoreConfig))] public class RequestParsing { - private const int InnerLoopCount = 512; - private const int Pipelining = 16; - - private const string plaintextTechEmpower = "GET /plaintext HTTP/1.1\r\n" + - "Host: localhost\r\n" + - "Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7\r\n" + - "Connection: keep-alive\r\n\r\n"; - - private const string liveaspnetRequest = "GET https://live.asp.net/ HTTP/1.1\r\n" + - "Host: live.asp.net\r\n" + - "Connection: keep-alive\r\n" + - "Upgrade-Insecure-Requests: 1\r\n" + - "User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36\r\n" + - "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n" + - "DNT: 1\r\n" + - "Accept-Encoding: gzip, deflate, sdch, br\r\n" + - "Accept-Language: en-US,en;q=0.8\r\n" + - "Cookie: __unam=7a67379-1s65dc575c4-6d778abe-1; omniID=9519gfde_3347_4762_8762_df51458c8ec2\r\n\r\n"; - - private const string unicodeRequest = - "GET http://stackoverflow.com/questions/40148683/why-is-%e0%a5%a7%e0%a5%a8%e0%a5%a9-numeric HTTP/1.1\r\n" + - "Accept: text/html, application/xhtml+xml, image/jxr, */*\r\n" + - "Accept-Language: en-US,en-GB;q=0.7,en;q=0.3\r\n" + - "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36 Edge/15.14965\r\n" + - "Accept-Encoding: gzip, deflate\r\n" + - "Host: stackoverflow.com\r\n" + - "Connection: Keep-Alive\r\n" + - "Cache-Control: max-age=0\r\n" + - "Upgrade-Insecure-Requests: 1\r\n" + - "DNT: 1\r\n" + - "Referer: http://stackoverflow.com/?tab=month\r\n" + - "Pragma: no-cache\r\n" + - "Cookie: prov=20629ccd-8b0f-e8ef-2935-cd26609fc0bc; __qca=P0-1591065732-1479167353442; _ga=GA1.2.1298898376.1479167354; _gat=1; sgt=id=9519gfde_3347_4762_8762_df51458c8ec2; acct=t=why-is-%e0%a5%a7%e0%a5%a8%e0%a5%a9-numeric&s=why-is-%e0%a5%a7%e0%a5%a8%e0%a5%a9-numeric\r\n\r\n"; - - private static readonly byte[] _plaintextTechEmpowerPipelinedRequests = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat(plaintextTechEmpower, Pipelining))); - private static readonly byte[] _plaintextTechEmpower = Encoding.ASCII.GetBytes(plaintextTechEmpower); - - private static readonly byte[] _liveaspnentPipelinedRequests = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat(liveaspnetRequest, Pipelining))); - private static readonly byte[] _liveaspnentRequest = Encoding.ASCII.GetBytes(liveaspnetRequest); - - private static readonly byte[] _unicodePipelinedRequests = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat(unicodeRequest, Pipelining))); - private static readonly byte[] _unicodeRequest = Encoding.ASCII.GetBytes(unicodeRequest); - - [Params(typeof(KestrelHttpParser))] + [Params(typeof(Internal.Http.KestrelHttpParser))] public Type ParserType { get; set; } - [Benchmark(Baseline = true, OperationsPerInvoke = InnerLoopCount)] - public void ParsePlaintextTechEmpower() + public IPipe Pipe { get; set; } + + public Frame Frame { get; set; } + + public PipeFactory PipelineFactory { get; set; } + + [Setup] + public void Setup() { - for (var i = 0; i < InnerLoopCount; i++) { - InsertData(_plaintextTechEmpower); + var connectionContext = new MockConnection(new KestrelServerOptions()); + connectionContext.ListenerContext.ServiceContext.HttpParserFactory = frame => (IHttpParser)Activator.CreateInstance(ParserType, frame.ConnectionContext.ListenerContext.ServiceContext.Log); + + Frame = new Frame(application: null, context: connectionContext); + PipelineFactory = new PipeFactory(); + Pipe = PipelineFactory.Create(); + } + + [Benchmark(Baseline = true, OperationsPerInvoke = RequestParsingData.InnerLoopCount)] + public void PlaintextTechEmpower() + { + for (var i = 0; i < RequestParsingData.InnerLoopCount; i++) + { + InsertData(RequestParsingData.PlaintextTechEmpowerRequest); ParseData(); } } - [Benchmark(OperationsPerInvoke = InnerLoopCount * Pipelining)] - public void ParsePipelinedPlaintextTechEmpower() + [Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount * RequestParsingData.Pipelining)] + public void PipelinedPlaintextTechEmpower() { - for (var i = 0; i < InnerLoopCount; i++) + for (var i = 0; i < RequestParsingData.InnerLoopCount; i++) { - InsertData(_plaintextTechEmpowerPipelinedRequests); + InsertData(RequestParsingData.PlaintextTechEmpowerPipelinedRequests); ParseData(); } } - [Benchmark(OperationsPerInvoke = InnerLoopCount)] - public void ParseLiveAspNet() + [Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount)] + public void LiveAspNet() { - for (var i = 0; i < InnerLoopCount; i++) + for (var i = 0; i < RequestParsingData.InnerLoopCount; i++) { - InsertData(_liveaspnentRequest); + InsertData(RequestParsingData.LiveaspnetRequest); ParseData(); } } - [Benchmark(OperationsPerInvoke = InnerLoopCount * Pipelining)] - public void ParsePipelinedLiveAspNet() + [Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount * RequestParsingData.Pipelining)] + public void PipelinedLiveAspNet() { - for (var i = 0; i < InnerLoopCount; i++) + for (var i = 0; i < RequestParsingData.InnerLoopCount; i++) { - InsertData(_liveaspnentPipelinedRequests); + InsertData(RequestParsingData.LiveaspnetPipelinedRequests); ParseData(); } } - [Benchmark(OperationsPerInvoke = InnerLoopCount)] - public void ParseUnicode() + [Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount)] + public void Unicode() { - for (var i = 0; i < InnerLoopCount; i++) + for (var i = 0; i < RequestParsingData.InnerLoopCount; i++) { - InsertData(_unicodeRequest); + InsertData(RequestParsingData.UnicodeRequest); ParseData(); } } - [Benchmark(OperationsPerInvoke = InnerLoopCount * Pipelining)] - public void ParseUnicodePipelined() + [Benchmark(OperationsPerInvoke = RequestParsingData.InnerLoopCount * RequestParsingData.Pipelining)] + public void UnicodePipelined() { - for (var i = 0; i < InnerLoopCount; i++) + for (var i = 0; i < RequestParsingData.InnerLoopCount; i++) { - InsertData(_unicodePipelinedRequests); + InsertData(RequestParsingData.UnicodePipelinedRequests); ParseData(); } } @@ -145,7 +118,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance if (!Frame.TakeStartLine(readableBuffer, out var consumed, out var examined)) { - ThrowInvalidStartLine(); + ThrowInvalidRequestLine(); } Pipe.Reader.Advance(consumed, examined); @@ -156,38 +129,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance if (!Frame.TakeMessageHeaders(readableBuffer, out consumed, out examined)) { - ThrowInvalidMessageHeaders(); + ThrowInvalidRequestHeaders(); } Pipe.Reader.Advance(consumed, examined); } while (true); } - private void ThrowInvalidStartLine() + public static void ThrowInvalidRequestLine() { - throw new InvalidOperationException("Invalid StartLine"); + throw new InvalidOperationException("Invalid request line"); } - private void ThrowInvalidMessageHeaders() + public static void ThrowInvalidRequestHeaders() { - throw new InvalidOperationException("Invalid MessageHeaders"); + throw new InvalidOperationException("Invalid request headers"); } - - [Setup] - public void Setup() - { - var connectionContext = new MockConnection(new KestrelServerOptions()); - connectionContext.ListenerContext.ServiceContext.HttpParserFactory = frame => (IHttpParser)Activator.CreateInstance(ParserType, frame.ConnectionContext.ListenerContext.ServiceContext.Log); - - Frame = new Frame(application: null, context: connectionContext); - PipelineFactory = new PipeFactory(); - Pipe = PipelineFactory.Create(); - } - - public IPipe Pipe { get; set; } - - public Frame Frame { get; set; } - - public PipeFactory PipelineFactory { get; set; } } } diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/RequestParsingData.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/RequestParsingData.cs new file mode 100644 index 0000000000..bddc1a7a97 --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/RequestParsingData.cs @@ -0,0 +1,62 @@ +// 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.Linq; +using System.Text; +using BenchmarkDotNet.Attributes; + +namespace Microsoft.AspNetCore.Server.Kestrel.Performance +{ + [Config(typeof(CoreConfig))] + public class RequestParsingData + { + public const int InnerLoopCount = 512; + + public const int Pipelining = 16; + + private const string _plaintextTechEmpowerRequest = + "GET /plaintext HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7\r\n" + + "Connection: keep-alive\r\n" + + "\r\n"; + + private const string _liveaspnetRequest = + "GET https://live.asp.net/ HTTP/1.1\r\n" + + "Host: live.asp.net\r\n" + + "Connection: keep-alive\r\n" + + "Upgrade-Insecure-Requests: 1\r\n" + + "User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.99 Safari/537.36\r\n" + + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n" + + "DNT: 1\r\n" + + "Accept-Encoding: gzip, deflate, sdch, br\r\n" + + "Accept-Language: en-US,en;q=0.8\r\n" + + "Cookie: __unam=7a67379-1s65dc575c4-6d778abe-1; omniID=9519gfde_3347_4762_8762_df51458c8ec2\r\n" + + "\r\n"; + + private const string _unicodeRequest = + "GET http://stackoverflow.com/questions/40148683/why-is-%e0%a5%a7%e0%a5%a8%e0%a5%a9-numeric HTTP/1.1\r\n" + + "Accept: text/html, application/xhtml+xml, image/jxr, */*\r\n" + + "Accept-Language: en-US,en-GB;q=0.7,en;q=0.3\r\n" + + "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36 Edge/15.14965\r\n" + + "Accept-Encoding: gzip, deflate\r\n" + + "Host: stackoverflow.com\r\n" + + "Connection: Keep-Alive\r\n" + + "Cache-Control: max-age=0\r\n" + + "Upgrade-Insecure-Requests: 1\r\n" + + "DNT: 1\r\n" + + "Referer: http://stackoverflow.com/?tab=month\r\n" + + "Pragma: no-cache\r\n" + + "Cookie: prov=20629ccd-8b0f-e8ef-2935-cd26609fc0bc; __qca=P0-1591065732-1479167353442; _ga=GA1.2.1298898376.1479167354; _gat=1; sgt=id=9519gfde_3347_4762_8762_df51458c8ec2; acct=t=why-is-%e0%a5%a7%e0%a5%a8%e0%a5%a9-numeric&s=why-is-%e0%a5%a7%e0%a5%a8%e0%a5%a9-numeric\r\n" + + "\r\n"; + + public static readonly byte[] PlaintextTechEmpowerPipelinedRequests = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat(_plaintextTechEmpowerRequest, Pipelining))); + public static readonly byte[] PlaintextTechEmpowerRequest = Encoding.ASCII.GetBytes(_plaintextTechEmpowerRequest); + + public static readonly byte[] LiveaspnetPipelinedRequests = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat(_liveaspnetRequest, Pipelining))); + public static readonly byte[] LiveaspnetRequest = Encoding.ASCII.GetBytes(_liveaspnetRequest); + + public static readonly byte[] UnicodePipelinedRequests = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat(_unicodeRequest, Pipelining))); + public static readonly byte[] UnicodeRequest = Encoding.ASCII.GetBytes(_unicodeRequest); + } +}