diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/MultiThreadedRequestParsing.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/MultiThreadedRequestParsing.cs new file mode 100644 index 000000000..53b8ea02d --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/MultiThreadedRequestParsing.cs @@ -0,0 +1,192 @@ +// 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.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Server.Kestrel.Internal.Http; +using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; +using Microsoft.AspNetCore.Testing; +using RequestLineStatus = Microsoft.AspNetCore.Server.Kestrel.Internal.Http.Frame.RequestLineStatus; + +namespace Microsoft.AspNetCore.Server.Kestrel.Performance +{ + [Config(typeof(CoreConfig))] + public class MultiThreadedRequestParsing + { + // Is divided by processor count so will be slightly off as the + // LCM of Intel's core counts (22, 20, 18, 16, 14, 12, 10, 8, 6, 4, 2) is 55440 + // which would be far too many iterations + private const int InnerLoopCount = 4096; + + private static readonly int ThreadCount = Environment.ProcessorCount; + private static readonly int LoopCount = InnerLoopCount / ThreadCount; + + private static MemoryPool[] MemoryPool; + private static SocketInput[] Input; + private static Frame[] Frame; + + [Benchmark(Baseline = true, OperationsPerInvoke = InnerLoopCount)] + public void ParsePlaintext() + { + Parallel.For(0, ThreadCount, new ParallelOptions() {MaxDegreeOfParallelism = ThreadCount}, (index) => + { + var socketInput = Input[index]; + var frame = Frame[index]; + + for (var i = 0; i < LoopCount; i++) + { + InsertData(socketInput, Requests.PlaintextRequest); + + ParseData(socketInput, frame); + } + }); + } + + [Benchmark(OperationsPerInvoke = InnerLoopCount * Requests.Pipelining)] + public void ParsePipelinedPlaintext() + { + Parallel.For(0, ThreadCount, new ParallelOptions() { MaxDegreeOfParallelism = ThreadCount }, (index) => + { + var socketInput = Input[index]; + var frame = Frame[index]; + + for (var i = 0; i < LoopCount; i++) + { + InsertData(socketInput, Requests.PlaintextPipelinedRequests); + + ParseData(socketInput, frame); + } + }); + } + + [Benchmark(OperationsPerInvoke = InnerLoopCount)] + public void ParseLiveAspNet() + { + Parallel.For(0, ThreadCount, new ParallelOptions() { MaxDegreeOfParallelism = ThreadCount }, (index) => + { + var socketInput = Input[index]; + var frame = Frame[index]; + + for (var i = 0; i < LoopCount; i++) + { + InsertData(socketInput, Requests.LiveAspNetRequest); + + ParseData(socketInput, frame); + } + }); + } + + [Benchmark(OperationsPerInvoke = InnerLoopCount * Requests.Pipelining)] + public void ParsePipelinedLiveAspNet() + { + Parallel.For(0, ThreadCount, new ParallelOptions() { MaxDegreeOfParallelism = ThreadCount }, (index) => + { + var socketInput = Input[index]; + var frame = Frame[index]; + + for (var i = 0; i < LoopCount; i++) + { + InsertData(socketInput, Requests.LiveAspNetPipelinedRequests); + + ParseData(socketInput, frame); + } + }); + } + + [Benchmark(OperationsPerInvoke = InnerLoopCount)] + public void ParseUnicode() + { + Parallel.For(0, ThreadCount, new ParallelOptions() { MaxDegreeOfParallelism = ThreadCount }, (index) => + { + var socketInput = Input[index]; + var frame = Frame[index]; + + for (var i = 0; i < LoopCount; i++) + { + InsertData(socketInput, Requests.UnicodeRequest); + + ParseData(socketInput, frame); + } + }); + } + + [Benchmark(OperationsPerInvoke = InnerLoopCount * Requests.Pipelining)] + public void ParseUnicodePipelined() + { + Parallel.For(0, ThreadCount, new ParallelOptions() { MaxDegreeOfParallelism = ThreadCount }, (index) => + { + var socketInput = Input[index]; + var frame = Frame[index]; + + for (var i = 0; i < LoopCount; i++) + { + InsertData(socketInput, Requests.UnicodePipelinedRequests); + + ParseData(socketInput, frame); + } + }); + } + + private static void InsertData(SocketInput socketInput, byte[] dataBytes) + { + socketInput.IncomingData(dataBytes, 0, dataBytes.Length); + } + + private static void ParseData(SocketInput socketInput, Frame frame) + { + while (socketInput.GetAwaiter().IsCompleted) + { + frame.Reset(); + + if (frame.TakeStartLine(socketInput) != RequestLineStatus.Done) + { + ThrowInvalidStartLine(); + } + + frame.InitializeHeaders(); + + if (!frame.TakeMessageHeaders(socketInput, (FrameRequestHeaders)frame.RequestHeaders)) + { + ThrowInvalidMessageHeaders(); + } + } + } + + private static void ThrowInvalidStartLine() + { + throw new InvalidOperationException("Invalid StartLine"); + } + + private static void ThrowInvalidMessageHeaders() + { + throw new InvalidOperationException("Invalid MessageHeaders"); + } + + [Setup] + public void Setup() + { + var threadCount = ThreadCount; + + MemoryPool = new MemoryPool[threadCount]; + Input = new SocketInput[threadCount]; + Frame = new Frame[threadCount]; + + for (var i = 0; i < threadCount; i++) + { + Requests.SetupFrameObjects(out MemoryPool[i], out Input[i], out Frame[i]); + } + + } + + [Cleanup] + public void Cleanup() + { + var threadCount = ThreadCount; + for (var i = 0; i < threadCount; i++) + { + Requests.CleanUpFrameObjects(ref MemoryPool[i], ref Input[i], ref Frame[i]); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/Program.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/Program.cs index b627755d7..df74f6e91 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/Program.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/Program.cs @@ -2,11 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using BenchmarkDotNet.Environments; -using BenchmarkDotNet.Jobs; -using BenchmarkDotNet.Properties; using BenchmarkDotNet.Running; -using BenchmarkDotNet.Toolchains; namespace Microsoft.AspNetCore.Server.Kestrel.Performance { @@ -14,6 +10,7 @@ public class Program { public static void Main(string[] args) { + Console.WriteLine($"Kestrel.Performance Tests - ProcessorCount: {Environment.ProcessorCount.ToString()}"); var options = (uint[])Enum.GetValues(typeof(BenchmarkType)); BenchmarkType type; if (args.Length != 1 || !Enum.TryParse(args[0], out type)) @@ -36,6 +33,10 @@ private static void RunSelectedBenchmarks(BenchmarkType type) { BenchmarkRunner.Run(); } + if (type.HasFlag(BenchmarkType.MultiThreadedRequestParsing)) + { + BenchmarkRunner.Run(); + } } } @@ -43,6 +44,7 @@ private static void RunSelectedBenchmarks(BenchmarkType type) public enum BenchmarkType : uint { RequestParsing = 1, + MultiThreadedRequestParsing = 2, // 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/Readme.md b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/Readme.md index b98f36ff5..1ea3365cd 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/Readme.md +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/Readme.md @@ -3,6 +3,7 @@ To run a specific benchmark add it as parameter ``` dotnet run RequestParsing +dotnet run MultiThreadedRequestParsing ``` To run all use `All` as parameter ``` diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/RequestParsing.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/RequestParsing.cs index facb7bcef..36af5f782 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/RequestParsing.cs +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/RequestParsing.cs @@ -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.Linq; -using System.Text; using BenchmarkDotNet.Attributes; -using Microsoft.AspNetCore.Server.Kestrel.Internal; using Microsoft.AspNetCore.Server.Kestrel.Internal.Http; using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; using Microsoft.AspNetCore.Testing; @@ -17,49 +14,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Performance public class RequestParsing { private const int InnerLoopCount = 512; - private const int Pipelining = 16; - - private const string plaintextRequest = "GET /plaintext HTTP/1.1\r\nHost: www.example.com\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[] _plaintextPipelinedRequests = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat(plaintextRequest, Pipelining))); - private static readonly byte[] _plaintextRequest = Encoding.ASCII.GetBytes(plaintextRequest); - - 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); - - private KestrelTrace Trace; - private LoggingThreadPool ThreadPool; + private MemoryPool MemoryPool; - private SocketInput SocketInput; + private SocketInput Input; private Frame Frame; [Benchmark(Baseline = true, OperationsPerInvoke = InnerLoopCount)] @@ -67,18 +24,18 @@ public void ParsePlaintext() { for (var i = 0; i < InnerLoopCount; i++) { - InsertData(_plaintextRequest); + InsertData(Requests.PlaintextRequest); ParseData(); } } - [Benchmark(OperationsPerInvoke = InnerLoopCount * Pipelining)] + [Benchmark(OperationsPerInvoke = InnerLoopCount * Requests.Pipelining)] public void ParsePipelinedPlaintext() { for (var i = 0; i < InnerLoopCount; i++) { - InsertData(_plaintextPipelinedRequests); + InsertData(Requests.PlaintextPipelinedRequests); ParseData(); } @@ -89,18 +46,18 @@ public void ParseLiveAspNet() { for (var i = 0; i < InnerLoopCount; i++) { - InsertData(_liveaspnentRequest); + InsertData(Requests.LiveAspNetRequest); ParseData(); } } - [Benchmark(OperationsPerInvoke = InnerLoopCount * Pipelining)] + [Benchmark(OperationsPerInvoke = InnerLoopCount * Requests.Pipelining)] public void ParsePipelinedLiveAspNet() { for (var i = 0; i < InnerLoopCount; i++) { - InsertData(_liveaspnentPipelinedRequests); + InsertData(Requests.LiveAspNetPipelinedRequests); ParseData(); } @@ -111,18 +68,18 @@ public void ParseUnicode() { for (var i = 0; i < InnerLoopCount; i++) { - InsertData(_unicodeRequest); + InsertData(Requests.UnicodeRequest); ParseData(); } } - [Benchmark(OperationsPerInvoke = InnerLoopCount * Pipelining)] + [Benchmark(OperationsPerInvoke = InnerLoopCount * Requests.Pipelining)] public void ParseUnicodePipelined() { for (var i = 0; i < InnerLoopCount; i++) { - InsertData(_unicodePipelinedRequests); + InsertData(Requests.UnicodePipelinedRequests); ParseData(); } @@ -130,23 +87,23 @@ public void ParseUnicodePipelined() private void InsertData(byte[] dataBytes) { - SocketInput.IncomingData(dataBytes, 0, dataBytes.Length); + Input.IncomingData(dataBytes, 0, dataBytes.Length); } private void ParseData() { - while (SocketInput.GetAwaiter().IsCompleted) + while (Input.GetAwaiter().IsCompleted) { Frame.Reset(); - if (Frame.TakeStartLine(SocketInput) != RequestLineStatus.Done) + if (Frame.TakeStartLine(Input) != RequestLineStatus.Done) { ThrowInvalidStartLine(); } Frame.InitializeHeaders(); - if (!Frame.TakeMessageHeaders(SocketInput, (FrameRequestHeaders) Frame.RequestHeaders)) + if (!Frame.TakeMessageHeaders(Input, (FrameRequestHeaders) Frame.RequestHeaders)) { ThrowInvalidMessageHeaders(); } @@ -166,23 +123,13 @@ private void ThrowInvalidMessageHeaders() [Setup] public void Setup() { - Trace = new KestrelTrace(new TestKestrelTrace()); - ThreadPool = new LoggingThreadPool(Trace); - MemoryPool = new MemoryPool(); - SocketInput = new SocketInput(MemoryPool, ThreadPool); - - var connectionContext = new MockConnection(new KestrelServerOptions()); - connectionContext.Input = SocketInput; - - Frame = new Frame(application: null, context: connectionContext); + Requests.SetupFrameObjects(out MemoryPool, out Input, out Frame); } [Cleanup] public void Cleanup() { - SocketInput.IncomingFin(); - SocketInput.Dispose(); - MemoryPool.Dispose(); + Requests.CleanUpFrameObjects(ref MemoryPool, ref Input, ref Frame); } } } diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/Requests.cs b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/Requests.cs new file mode 100644 index 000000000..7f9fe93c9 --- /dev/null +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/Requests.cs @@ -0,0 +1,75 @@ +// 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 Microsoft.AspNetCore.Server.Kestrel.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Internal.Http; +using Microsoft.AspNetCore.Server.Kestrel.Internal.Infrastructure; +using Microsoft.AspNetCore.Testing; + +namespace Microsoft.AspNetCore.Server.Kestrel.Performance +{ + public class Requests + { + public const int Pipelining = 16; + + private const string plaintextRequest = "GET /plaintext HTTP/1.1\r\nHost: www.example.com\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[] PlaintextPipelinedRequests = Encoding.ASCII.GetBytes(string.Concat(Enumerable.Repeat(plaintextRequest, Pipelining))); + public static readonly byte[] PlaintextRequest = Encoding.ASCII.GetBytes(plaintextRequest); + + 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); + + public static void SetupFrameObjects(out MemoryPool memoryPool, out SocketInput input, out Frame frame) + { + memoryPool = new MemoryPool(); + + var trace = new KestrelTrace(new TestKestrelTrace()); + var threadPool = new LoggingThreadPool(trace); + input = new SocketInput(memoryPool, threadPool); + + var connectionContext = new MockConnection(new KestrelServerOptions()); + connectionContext.Input = input; + + frame = new Frame(application: null, context: connectionContext); + } + + public static void CleanUpFrameObjects(ref MemoryPool memoryPool, ref SocketInput input, ref Frame frame) + { + input.IncomingFin(); + input.Dispose(); + memoryPool.Dispose(); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/project.json b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/project.json index a74be5fef..ca1cf9230 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.Performance/project.json +++ b/test/Microsoft.AspNetCore.Server.Kestrel.Performance/project.json @@ -1,14 +1,14 @@ { "version": "1.0.0-*", "dependencies": { - "BenchmarkDotNet": "0.10.0", + "BenchmarkDotNet": "0.10.1", "Microsoft.AspNetCore.Server.Kestrel": "1.2.0-*" }, "frameworks": { - "netcoreapp1.0": { + "netcoreapp1.1": { "dependencies": { "Microsoft.NETCore.App": { - "version": "1.0.1-*", + "version": "1.1.0-*", "type": "platform" } }