diff --git a/src/Servers/IIS/test/Common.FunctionalTests/HttpsTests.cs b/src/Servers/IIS/test/Common.FunctionalTests/HttpsTests.cs index 658aaf7a0b..c3003aaf4f 100644 --- a/src/Servers/IIS/test/Common.FunctionalTests/HttpsTests.cs +++ b/src/Servers/IIS/test/Common.FunctionalTests/HttpsTests.cs @@ -44,11 +44,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests var deploymentResult = await DeployAsync(deploymentParameters); - var handler = new HttpClientHandler - { - ServerCertificateCustomValidationCallback = (a, b, c, d) => true - }; - var client = deploymentResult.CreateClient(handler); + var client = CreateNonValidatingClient(deploymentResult); var response = await client.GetAsync("HttpsHelloWorld"); var responseText = await response.Content.ReadAsStringAsync(); if (variant.HostingModel == HostingModel.OutOfProcess) @@ -94,7 +90,9 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests }); var deploymentResult = await DeployAsync(deploymentParameters); - Assert.Equal(deploymentParameters.ApplicationBaseUriHint + appName, await deploymentResult.HttpClient.GetStringAsync($"/{appName}/ServerAddresses")); + var client = CreateNonValidatingClient(deploymentResult); + + Assert.Equal(deploymentParameters.ApplicationBaseUriHint + appName, await client.GetStringAsync($"/{appName}/ServerAddresses")); } [ConditionalFact] @@ -115,7 +113,9 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests deploymentParameters.WebConfigBasedEnvironmentVariables["ASPNETCORE_HTTPS_PORT"] = "123"; var deploymentResult = await DeployAsync(deploymentParameters); - Assert.Equal("123", await deploymentResult.HttpClient.GetStringAsync("/HTTPS_PORT")); + var client = CreateNonValidatingClient(deploymentResult); + + Assert.Equal("123", await client.GetStringAsync("/HTTPS_PORT")); } [ConditionalFact] @@ -142,7 +142,18 @@ namespace Microsoft.AspNetCore.Server.IISIntegration.FunctionalTests }); var deploymentResult = await DeployAsync(deploymentParameters); - Assert.Equal("NOVALUE", await deploymentResult.HttpClient.GetStringAsync("/HTTPS_PORT")); + var client = CreateNonValidatingClient(deploymentResult); + + Assert.Equal("NOVALUE", await client.GetStringAsync("/HTTPS_PORT")); + } + + private static HttpClient CreateNonValidatingClient(IISDeploymentResult deploymentResult) + { + var handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (a, b, c, d) => true + }; + return deploymentResult.CreateClient(handler); } public static int GetNextSSLPort(int avoid = 0) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/ChunkWriter.cs b/src/Servers/Kestrel/Core/src/Internal/Http/ChunkWriter.cs index 3d8cc4566b..7bb2a3781f 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/ChunkWriter.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/ChunkWriter.cs @@ -5,59 +5,63 @@ using System; using System.Buffers; using System.IO.Pipelines; using System.Text; +using System.Runtime.CompilerServices; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { internal static class ChunkWriter { - private static readonly ArraySegment _endChunkBytes = CreateAsciiByteArraySegment("\r\n"); private static readonly byte[] _hex = Encoding.ASCII.GetBytes("0123456789abcdef"); - private static ArraySegment CreateAsciiByteArraySegment(string text) + public static int BeginChunkBytes(int dataCount, Span span) { - var bytes = Encoding.ASCII.GetBytes(text); - return new ArraySegment(bytes); - } - - public static ArraySegment BeginChunkBytes(int dataCount) - { - var bytes = new byte[10] - { - _hex[((dataCount >> 0x1c) & 0x0f)], - _hex[((dataCount >> 0x18) & 0x0f)], - _hex[((dataCount >> 0x14) & 0x0f)], - _hex[((dataCount >> 0x10) & 0x0f)], - _hex[((dataCount >> 0x0c) & 0x0f)], - _hex[((dataCount >> 0x08) & 0x0f)], - _hex[((dataCount >> 0x04) & 0x0f)], - _hex[((dataCount >> 0x00) & 0x0f)], - (byte)'\r', - (byte)'\n', - }; - // Determine the most-significant non-zero nibble int total, shift; - total = (dataCount > 0xffff) ? 0x10 : 0x00; - dataCount >>= total; - shift = (dataCount > 0x00ff) ? 0x08 : 0x00; - dataCount >>= shift; + var count = dataCount; + total = (count > 0xffff) ? 0x10 : 0x00; + count >>= total; + shift = (count > 0x00ff) ? 0x08 : 0x00; + count >>= shift; total |= shift; - total |= (dataCount > 0x000f) ? 0x04 : 0x00; + total |= (count > 0x000f) ? 0x04 : 0x00; - var offset = 7 - (total >> 2); - return new ArraySegment(bytes, offset, 10 - offset); + count = (total >> 2) + 3; + + var offset = 0; + ref var startHex = ref _hex[0]; + + for (shift = total; shift >= 0; shift -= 4) + { + // Using Unsafe.Add to elide the bounds check on _hex as the & 0x0f definately + // constrains it to the range 0x0 - 0xf, matching the bounds of the array + span[offset] = Unsafe.Add(ref startHex, ((dataCount >> shift) & 0x0f)); + offset++; + } + + span[count - 2] = (byte)'\r'; + span[count - 1] = (byte)'\n'; + + return count; } - internal static int WriteBeginChunkBytes(ref BufferWriter start, int dataCount) + internal static void WriteBeginChunkBytes(this ref BufferWriter start, int dataCount) { - var chunkSegment = BeginChunkBytes(dataCount); - start.Write(new ReadOnlySpan(chunkSegment.Array, chunkSegment.Offset, chunkSegment.Count)); - return chunkSegment.Count; + // 10 bytes is max length + \r\n + start.Ensure(10); + + var count = BeginChunkBytes(dataCount, start.Span); + start.Advance(count); } - internal static void WriteEndChunkBytes(ref BufferWriter start) + internal static void WriteEndChunkBytes(this ref BufferWriter start) { - start.Write(new ReadOnlySpan(_endChunkBytes.Array, _endChunkBytes.Offset, _endChunkBytes.Count)); + start.Ensure(2); + var span = start.Span; + + // CRLF done in reverse order so the 1st index will elide the bounds check for the 0th index + span[1] = (byte)'\n'; + span[0] = (byte)'\r'; + start.Advance(2); } } } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs index 15fb2dcb8f..eaed7c7be1 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs @@ -280,16 +280,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http return new ForContentLength(keepAlive, contentLength, context); } - // Avoid slowing down most common case - if (!object.ReferenceEquals(context.Method, HttpMethods.Get)) + // If we got here, request contains no Content-Length or Transfer-Encoding header. + // Reject with 411 Length Required. + if (context.Method == HttpMethod.Post || context.Method == HttpMethod.Put) { - // If we got here, request contains no Content-Length or Transfer-Encoding header. - // Reject with 411 Length Required. - if (context.Method == HttpMethod.Post || context.Method == HttpMethod.Put) - { - var requestRejectionReason = httpVersion == HttpVersion.Http11 ? RequestRejectionReason.LengthRequired : RequestRejectionReason.LengthRequiredHttp10; - BadHttpRequestException.Throw(requestRejectionReason, context.Method); - } + var requestRejectionReason = httpVersion == HttpVersion.Http11 ? RequestRejectionReason.LengthRequired : RequestRejectionReason.LengthRequiredHttp10; + BadHttpRequestException.Throw(requestRejectionReason, context.Method); } return keepAlive ? MessageBody.ZeroContentLengthKeepAlive : MessageBody.ZeroContentLengthClose; diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs index 72bedf1be6..773e5cc112 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs @@ -927,9 +927,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { var writer = new BufferWriter(writableBuffer); - ChunkWriter.WriteBeginChunkBytes(ref writer, buffer.Length); + writer.WriteBeginChunkBytes(buffer.Length); writer.Write(buffer.Span); - ChunkWriter.WriteEndChunkBytes(ref writer); + writer.WriteEndChunkBytes(); writer.Commit(); bytesWritten = writer.BytesCommitted; @@ -938,12 +938,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http return bytesWritten; } - private static ArraySegment CreateAsciiByteArraySegment(string text) - { - var bytes = Encoding.ASCII.GetBytes(text); - return new ArraySegment(bytes); - } - public void ProduceContinue() { if (HasResponseStarted) diff --git a/src/Servers/Kestrel/Core/test/ChunkWriterTests.cs b/src/Servers/Kestrel/Core/test/ChunkWriterTests.cs index 722c0281ab..2b6f3a3c65 100644 --- a/src/Servers/Kestrel/Core/test/ChunkWriterTests.cs +++ b/src/Servers/Kestrel/Core/test/ChunkWriterTests.cs @@ -1,7 +1,7 @@ // 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; using System.Text; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Xunit; @@ -11,28 +11,37 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests public class ChunkWriterTests { [Theory] - [InlineData(1, "1\r\n")] - [InlineData(10, "a\r\n")] + [InlineData(0x00, "0\r\n")] + [InlineData(0x01, "1\r\n")] [InlineData(0x08, "8\r\n")] - [InlineData(0x10, "10\r\n")] + [InlineData(0x0a, "a\r\n")] + [InlineData(0x0f, "f\r\n")] + [InlineData(0x010, "10\r\n")] [InlineData(0x080, "80\r\n")] - [InlineData(0x100, "100\r\n")] + [InlineData(0x0ff, "ff\r\n")] + [InlineData(0x0100, "100\r\n")] [InlineData(0x0800, "800\r\n")] - [InlineData(0x1000, "1000\r\n")] + [InlineData(0x0fff, "fff\r\n")] + [InlineData(0x01000, "1000\r\n")] [InlineData(0x08000, "8000\r\n")] - [InlineData(0x10000, "10000\r\n")] + [InlineData(0x0ffff, "ffff\r\n")] + [InlineData(0x010000, "10000\r\n")] [InlineData(0x080000, "80000\r\n")] - [InlineData(0x100000, "100000\r\n")] + [InlineData(0x0fffff, "fffff\r\n")] + [InlineData(0x0100000, "100000\r\n")] [InlineData(0x0800000, "800000\r\n")] - [InlineData(0x1000000, "1000000\r\n")] + [InlineData(0x0ffffff, "ffffff\r\n")] + [InlineData(0x01000000, "1000000\r\n")] [InlineData(0x08000000, "8000000\r\n")] - [InlineData(0x10000000, "10000000\r\n")] + [InlineData(0x0fffffff, "fffffff\r\n")] + [InlineData(0x010000000, "10000000\r\n")] [InlineData(0x7fffffffL, "7fffffff\r\n")] public void ChunkedPrefixMustBeHexCrLfWithoutLeadingZeros(int dataCount, string expected) { - var beginChunkBytes = ChunkWriter.BeginChunkBytes(dataCount); + Span span = new byte[10]; + var count = ChunkWriter.BeginChunkBytes(dataCount, span); - Assert.Equal(Encoding.ASCII.GetBytes(expected), beginChunkBytes.ToArray()); + Assert.Equal(Encoding.ASCII.GetBytes(expected), span.Slice(0, count).ToArray()); } } } diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Internal/IOQueue.cs b/src/Servers/Kestrel/Transport.Sockets/src/Internal/IOQueue.cs index 892e3927df..e0ae0f4dfd 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/Internal/IOQueue.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/Internal/IOQueue.cs @@ -8,10 +8,8 @@ using System.Threading; namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal { - public class IOQueue : PipeScheduler + public class IOQueue : PipeScheduler, IThreadPoolWorkItem { - private static readonly WaitCallback _doWorkCallback = s => ((IOQueue)s).DoWork(); - private readonly object _workSync = new object(); private readonly ConcurrentQueue _workItems = new ConcurrentQueue(); private bool _doingWork; @@ -30,13 +28,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal { if (!_doingWork) { - System.Threading.ThreadPool.UnsafeQueueUserWorkItem(_doWorkCallback, this); + System.Threading.ThreadPool.UnsafeQueueUserWorkItem(this, preferLocal: false); _doingWork = true; } } } - private void DoWork() + void IThreadPoolWorkItem.Execute() { while (true) { diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/ChunkWriterBenchmark.cs b/src/Servers/Kestrel/perf/Kestrel.Performance/ChunkWriterBenchmark.cs new file mode 100644 index 0000000000..3b24e49cab --- /dev/null +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/ChunkWriterBenchmark.cs @@ -0,0 +1,63 @@ +// 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.Buffers; +using System.IO.Pipelines; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; + +namespace Microsoft.AspNetCore.Server.Kestrel.Performance +{ + public class ChunkWriterBenchmark + { + private const int InnerLoopCount = 1024; + + private PipeReader _reader; + private PipeWriter _writer; + private MemoryPool _memoryPool; + + [GlobalSetup] + public void Setup() + { + _memoryPool = KestrelMemoryPool.Create(); + var pipe = new Pipe(new PipeOptions(_memoryPool)); + _reader = pipe.Reader; + _writer = pipe.Writer; + } + + [Params(0x0, 0x1, 0x10, 0x100, 0x1_000, 0x10_000, 0x100_000, 0x1_000_000)] + public int DataLength { get; set; } + + [Benchmark(OperationsPerInvoke = InnerLoopCount)] + public async Task WriteBeginChunkBytes() + { + WriteBeginChunkBytes_Write(); + + var flushResult = _writer.FlushAsync(); + + var result = await _reader.ReadAsync(); + _reader.AdvanceTo(result.Buffer.End, result.Buffer.End); + await flushResult; + } + + private void WriteBeginChunkBytes_Write() + { + var writer = new BufferWriter(_writer); + var dataLength = DataLength; + for (int i = 0; i < InnerLoopCount; i++) + { + ChunkWriter.WriteBeginChunkBytes(ref writer, dataLength); + } + + writer.Commit(); + } + + [GlobalCleanup] + public void Cleanup() + { + _memoryPool.Dispose(); + } + } +} diff --git a/src/Servers/Kestrel/perf/Kestrel.Performance/Microsoft.AspNetCore.Server.Kestrel.Performance.csproj b/src/Servers/Kestrel/perf/Kestrel.Performance/Microsoft.AspNetCore.Server.Kestrel.Performance.csproj index 09655f7ca8..0015869a26 100644 --- a/src/Servers/Kestrel/perf/Kestrel.Performance/Microsoft.AspNetCore.Server.Kestrel.Performance.csproj +++ b/src/Servers/Kestrel/perf/Kestrel.Performance/Microsoft.AspNetCore.Server.Kestrel.Performance.csproj @@ -6,6 +6,7 @@ true true false + false