From ca23b1a325a9c65ff9c375f15c03a18af08ca0bd Mon Sep 17 00:00:00 2001 From: Chris Ross Date: Fri, 22 Nov 2019 15:35:47 -0800 Subject: [PATCH] Pool HttpSys request buffers (#17314) --- .../HttpSys/samples/TestClient/App.config | 6 -- .../HttpSys/samples/TestClient/Program.cs | 51 +++++++++----- .../TestClient/Properties/AssemblyInfo.cs | 53 -------------- .../samples/TestClient/TestClient.csproj | 67 +++--------------- src/Servers/HttpSys/src/AsyncAcceptContext.cs | 25 +------ src/Servers/HttpSys/src/HttpSysListener.cs | 3 + ...Microsoft.AspNetCore.Server.HttpSys.csproj | 6 +- .../RequestProcessing/HeaderEncoding.cs | 12 +--- .../RequestProcessing/NativeRequestContext.cs | 70 +++++++++++++------ 9 files changed, 102 insertions(+), 191 deletions(-) delete mode 100644 src/Servers/HttpSys/samples/TestClient/App.config delete mode 100644 src/Servers/HttpSys/samples/TestClient/Properties/AssemblyInfo.cs diff --git a/src/Servers/HttpSys/samples/TestClient/App.config b/src/Servers/HttpSys/samples/TestClient/App.config deleted file mode 100644 index 2d2a12d81b..0000000000 --- a/src/Servers/HttpSys/samples/TestClient/App.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/Servers/HttpSys/samples/TestClient/Program.cs b/src/Servers/HttpSys/samples/TestClient/Program.cs index f57945de42..f81dc83833 100644 --- a/src/Servers/HttpSys/samples/TestClient/Program.cs +++ b/src/Servers/HttpSys/samples/TestClient/Program.cs @@ -11,20 +11,45 @@ namespace TestClient public class Program { private const string Address = - // "http://localhost:5000/public/1kb.txt"; - "https://localhost:9090/public/1kb.txt"; + "http://localhost:5000/public/1kb.txt"; + // "https://localhost:9090/public/1kb.txt"; public static void Main(string[] args) { - WebRequestHandler handler = new WebRequestHandler(); - handler.ServerCertificateValidationCallback = (_, __, ___, ____) => true; + Console.WriteLine("Ready"); + Console.ReadKey(); + + var handler = new HttpClientHandler(); + handler.MaxConnectionsPerServer = 500; + handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; // handler.UseDefaultCredentials = true; - handler.Credentials = new NetworkCredential(@"redmond\chrross", "passwird"); HttpClient client = new HttpClient(handler); - /* + RunParallelRequests(client); + + // RunManualRequests(client); + + // RunWebSocketClient().Wait(); + + Console.WriteLine("Done"); + // Console.ReadKey(); + } + + private static void RunManualRequests(HttpClient client) + { + while (true) + { + Console.WriteLine("Press any key to send request"); + Console.ReadKey(); + var result = client.GetAsync(Address).Result; + Console.WriteLine(result); + } + } + + private static void RunParallelRequests(HttpClient client) + { int completionCount = 0; - int iterations = 30000; + int iterations = 100000; for (int i = 0; i < iterations; i++) { client.GetAsync(Address) @@ -34,19 +59,7 @@ namespace TestClient while (completionCount < iterations) { Thread.Sleep(10); - }*/ - - while (true) - { - Console.WriteLine("Press any key to send request"); - Console.ReadKey(); - var result = client.GetAsync(Address).Result; - Console.WriteLine(result); } - - // RunWebSocketClient().Wait(); - // Console.WriteLine("Done"); - // Console.ReadKey(); } public static async Task RunWebSocketClient() diff --git a/src/Servers/HttpSys/samples/TestClient/Properties/AssemblyInfo.cs b/src/Servers/HttpSys/samples/TestClient/Properties/AssemblyInfo.cs deleted file mode 100644 index 249372ae2d..0000000000 --- a/src/Servers/HttpSys/samples/TestClient/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. -// All Rights Reserved -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR -// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING -// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF -// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR -// NON-INFRINGEMENT. -// See the Apache 2 License for the specific language governing -// permissions and limitations under the License. - -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("TestClient")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("TestClient")] -[assembly: AssemblyCopyright("Copyright © 2012")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("8db62eb3-48c0-4049-b33e-271c738140a0")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("0.5")] -[assembly: AssemblyVersion("0.5")] -[assembly: AssemblyFileVersion("0.5.40117.0")] diff --git a/src/Servers/HttpSys/samples/TestClient/TestClient.csproj b/src/Servers/HttpSys/samples/TestClient/TestClient.csproj index c92be92bc8..6d5bb6e338 100644 --- a/src/Servers/HttpSys/samples/TestClient/TestClient.csproj +++ b/src/Servers/HttpSys/samples/TestClient/TestClient.csproj @@ -1,64 +1,13 @@ - - - + + - Debug - AnyCPU - {8B828433-B333-4C19-96AE-00BFFF9D8841} + $(DefaultNetCoreTargetFramework) Exe - Properties - TestClient - TestClient - v4.6 - 512 - ..\..\ - true - - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 + TestClient.Program + - - - - - - - - - + - - - - - - - - - - - \ No newline at end of file + + diff --git a/src/Servers/HttpSys/src/AsyncAcceptContext.cs b/src/Servers/HttpSys/src/AsyncAcceptContext.cs index 4c0cd86756..5908322598 100644 --- a/src/Servers/HttpSys/src/AsyncAcceptContext.cs +++ b/src/Servers/HttpSys/src/AsyncAcceptContext.cs @@ -3,7 +3,6 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -18,8 +17,6 @@ namespace Microsoft.AspNetCore.Server.HttpSys private TaskCompletionSource _tcs; private HttpSysListener _server; private NativeRequestContext _nativeRequestContext; - private const int DefaultBufferSize = 4096; - private const int AlignmentPadding = 8; internal AsyncAcceptContext(HttpSysListener server) { @@ -192,32 +189,14 @@ namespace Microsoft.AspNetCore.Server.HttpSys { _nativeRequestContext?.ReleasePins(); _nativeRequestContext?.Dispose(); - //Debug.Assert(size != 0, "unexpected size"); // We can't reuse overlapped objects - uint newSize = size.HasValue ? size.Value : DefaultBufferSize; - var backingBuffer = new byte[newSize + AlignmentPadding]; - var boundHandle = Server.RequestQueue.BoundHandle; var nativeOverlapped = new SafeNativeOverlapped(boundHandle, - boundHandle.AllocateNativeOverlapped(IOCallback, this, backingBuffer)); + boundHandle.AllocateNativeOverlapped(IOCallback, this, pinData: null)); - var requestAddress = Marshal.UnsafeAddrOfPinnedArrayElement(backingBuffer, 0); - - // TODO: - // Apparently the HttpReceiveHttpRequest memory alignment requirements for non - ARM processors - // are different than for ARM processors. We have seen 4 - byte - aligned buffers allocated on - // virtual x64/x86 machines which were accepted by HttpReceiveHttpRequest without errors. In - // these cases the buffer alignment may cause reading values at invalid offset. Setting buffer - // alignment to 0 for now. - // - // _bufferAlignment = (int)(requestAddress.ToInt64() & 0x07); - - var bufferAlignment = 0; - - var nativeRequest = (HttpApiTypes.HTTP_REQUEST*)(requestAddress + bufferAlignment); // nativeRequest - _nativeRequestContext = new NativeRequestContext(nativeOverlapped, bufferAlignment, nativeRequest, backingBuffer, requestId); + _nativeRequestContext = new NativeRequestContext(nativeOverlapped, Server.MemoryPool, size, requestId); } public object AsyncState diff --git a/src/Servers/HttpSys/src/HttpSysListener.cs b/src/Servers/HttpSys/src/HttpSysListener.cs index 24f6c61955..bbb4c9d088 100644 --- a/src/Servers/HttpSys/src/HttpSysListener.cs +++ b/src/Servers/HttpSys/src/HttpSysListener.cs @@ -2,6 +2,7 @@ // 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.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; @@ -32,6 +33,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys // 0.5 seconds per request. Respond with a 400 Bad Request. private const int UnknownHeaderLimit = 1000; + internal MemoryPool MemoryPool { get; } = SlabMemoryPoolFactory.Create(); + private volatile State _state; // m_State is set only within lock blocks, but often read outside locks. private ServerSession _serverSession; diff --git a/src/Servers/HttpSys/src/Microsoft.AspNetCore.Server.HttpSys.csproj b/src/Servers/HttpSys/src/Microsoft.AspNetCore.Server.HttpSys.csproj index c41c4af7a1..2e57c61ce8 100644 --- a/src/Servers/HttpSys/src/Microsoft.AspNetCore.Server.HttpSys.csproj +++ b/src/Servers/HttpSys/src/Microsoft.AspNetCore.Server.HttpSys.csproj @@ -1,4 +1,4 @@ - + ASP.NET Core HTTP server that uses the Windows HTTP Server API. @@ -13,6 +13,10 @@ + + + + diff --git a/src/Shared/HttpSys/RequestProcessing/HeaderEncoding.cs b/src/Shared/HttpSys/RequestProcessing/HeaderEncoding.cs index 991571904b..a5294c5eb7 100644 --- a/src/Shared/HttpSys/RequestProcessing/HeaderEncoding.cs +++ b/src/Shared/HttpSys/RequestProcessing/HeaderEncoding.cs @@ -1,6 +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; using System.Text; namespace Microsoft.AspNetCore.HttpSys.Internal @@ -14,16 +15,7 @@ namespace Microsoft.AspNetCore.HttpSys.Internal internal static unsafe string GetString(byte* pBytes, int byteCount) { - // net451: return new string(pBytes, 0, byteCount, Encoding); - - var charCount = Encoding.GetCharCount(pBytes, byteCount); - var chars = new char[charCount]; - fixed (char* pChars = chars) - { - var count = Encoding.GetChars(pBytes, byteCount, pChars, charCount); - System.Diagnostics.Debug.Assert(count == charCount); - } - return new string(chars); + return Encoding.GetString(new ReadOnlySpan(pBytes, byteCount)); } internal static byte[] GetBytes(string myString) diff --git a/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs b/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs index 4108d901e2..790b858b3b 100644 --- a/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs +++ b/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs @@ -2,6 +2,7 @@ // 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.Collections.Generic; using System.Collections.Immutable; using System.Collections.ObjectModel; @@ -17,24 +18,45 @@ namespace Microsoft.AspNetCore.HttpSys.Internal internal unsafe class NativeRequestContext : IDisposable { private const int AlignmentPadding = 8; + private const int DefaultBufferSize = 4096 - AlignmentPadding; private IntPtr _originalBufferAddress; private HttpApiTypes.HTTP_REQUEST* _nativeRequest; - private byte[] _backingBuffer; + private IMemoryOwner _backingBuffer; + private MemoryHandle _memoryHandle; private int _bufferAlignment; private SafeNativeOverlapped _nativeOverlapped; private bool _permanentlyPinned; + private bool _disposed; // To be used by HttpSys - internal NativeRequestContext(SafeNativeOverlapped nativeOverlapped, - int bufferAlignment, - HttpApiTypes.HTTP_REQUEST* nativeRequest, - byte[] backingBuffer, - ulong requestId) + internal NativeRequestContext(SafeNativeOverlapped nativeOverlapped, MemoryPool memoryPool, uint? bufferSize, ulong requestId) { _nativeOverlapped = nativeOverlapped; - _bufferAlignment = bufferAlignment; - _nativeRequest = nativeRequest; - _backingBuffer = backingBuffer; + + // TODO: + // Apparently the HttpReceiveHttpRequest memory alignment requirements for non - ARM processors + // are different than for ARM processors. We have seen 4 - byte - aligned buffers allocated on + // virtual x64/x86 machines which were accepted by HttpReceiveHttpRequest without errors. In + // these cases the buffer alignment may cause reading values at invalid offset. Setting buffer + // alignment to 0 for now. + // + // _bufferAlignment = (int)(requestAddress.ToInt64() & 0x07); + _bufferAlignment = 0; + + var newSize = (int)(bufferSize ?? DefaultBufferSize) + AlignmentPadding; + if (newSize <= memoryPool.MaxBufferSize) + { + _backingBuffer = memoryPool.Rent(newSize); + } + else + { + // No size limit + _backingBuffer = MemoryPool.Shared.Rent(newSize); + } + _backingBuffer.Memory.Span.Fill(0);// Zero the buffer + _memoryHandle = _backingBuffer.Memory.Pin(); + _nativeRequest = (HttpApiTypes.HTTP_REQUEST*)((long)_memoryHandle.Pointer + _bufferAlignment); + RequestId = requestId; } @@ -94,15 +116,17 @@ namespace Microsoft.AspNetCore.HttpSys.Internal internal uint Size { - get { return (uint)_backingBuffer.Length - AlignmentPadding; } + get { return (uint)_backingBuffer.Memory.Length - AlignmentPadding; } } // ReleasePins() should be called exactly once. It must be called before Dispose() is called, which means it must be called // before an object (Request) which closes the RequestContext on demand is returned to the application. internal void ReleasePins() { - Debug.Assert(_nativeRequest != null || _backingBuffer == null, "RequestContextBase::ReleasePins()|ReleasePins() called twice."); + Debug.Assert(_nativeRequest != null, "RequestContextBase::ReleasePins()|ReleasePins() called twice."); _originalBufferAddress = (IntPtr)_nativeRequest; + _memoryHandle.Dispose(); + _memoryHandle = default; _nativeRequest = null; _nativeOverlapped?.Dispose(); _nativeOverlapped = null; @@ -110,8 +134,14 @@ namespace Microsoft.AspNetCore.HttpSys.Internal public virtual void Dispose() { - Debug.Assert(_nativeRequest == null, "RequestContextBase::Dispose()|Dispose() called before ReleasePins()."); - _nativeOverlapped?.Dispose(); + if (!_disposed) + { + _disposed = true; + Debug.Assert(_nativeRequest == null, "RequestContextBase::Dispose()|Dispose() called before ReleasePins()."); + _nativeOverlapped?.Dispose(); + _memoryHandle.Dispose(); + _backingBuffer.Dispose(); + } } // These methods require the HTTP_REQUEST to still be pinned in its original location. @@ -272,7 +302,7 @@ namespace Microsoft.AspNetCore.HttpSys.Internal } else { - fixed (byte* pMemoryBlob = _backingBuffer) + fixed (byte* pMemoryBlob = _backingBuffer.Memory.Span) { var request = (HttpApiTypes.HTTP_REQUEST*)(pMemoryBlob + _bufferAlignment); long fixup = pMemoryBlob - (byte*)_originalBufferAddress; @@ -306,7 +336,7 @@ namespace Microsoft.AspNetCore.HttpSys.Internal else { // Return value. - fixed (byte* pMemoryBlob = _backingBuffer) + fixed (byte* pMemoryBlob = _backingBuffer.Memory.Span) { var request = (HttpApiTypes.HTTP_REQUEST*)(pMemoryBlob + _bufferAlignment); long fixup = pMemoryBlob - (byte*)_originalBufferAddress; @@ -366,7 +396,7 @@ namespace Microsoft.AspNetCore.HttpSys.Internal } else { - fixed (byte* pMemoryBlob = _backingBuffer) + fixed (byte* pMemoryBlob = _backingBuffer.Memory.Span) { var request = (HttpApiTypes.HTTP_REQUEST*)(pMemoryBlob + _bufferAlignment); return GetEndPointHelper(localEndpoint, request, pMemoryBlob); @@ -426,7 +456,7 @@ namespace Microsoft.AspNetCore.HttpSys.Internal } else { - fixed (byte* pMemoryBlob = _backingBuffer) + fixed (byte* pMemoryBlob = _backingBuffer.Memory.Span) { var request = (HttpApiTypes.HTTP_REQUEST*)(pMemoryBlob + _bufferAlignment); long fixup = pMemoryBlob - (byte*)_originalBufferAddress; @@ -490,7 +520,7 @@ namespace Microsoft.AspNetCore.HttpSys.Internal } else { - fixed (byte* pMemoryBlob = _backingBuffer) + fixed (byte* pMemoryBlob = _backingBuffer.Memory.Span) { var request = (HttpApiTypes.HTTP_REQUEST_V2*)(pMemoryBlob + _bufferAlignment); return GetRequestInfo(_originalBufferAddress, request); @@ -514,7 +544,7 @@ namespace Microsoft.AspNetCore.HttpSys.Internal var offset = (long)requestInfo.pInfo - (long)baseAddress; info.Add( (int)requestInfo.InfoType, - new ReadOnlyMemory(_backingBuffer, (int)offset, (int)requestInfo.InfoLength)); + _backingBuffer.Memory.Slice((int)offset, (int)requestInfo.InfoLength)); } return new ReadOnlyDictionary>(info); @@ -528,7 +558,7 @@ namespace Microsoft.AspNetCore.HttpSys.Internal } else { - fixed (byte* pMemoryBlob = _backingBuffer) + fixed (byte* pMemoryBlob = _backingBuffer.Memory.Span) { var request = (HttpApiTypes.HTTP_REQUEST_V2*)(pMemoryBlob + _bufferAlignment); return GetClientCertificate(_originalBufferAddress, request);