Make the HTTP.sys accept loop cheaper (#28345)

* Make the HTTP.sys accept loop cheaper
- Allocate a single AsyncAcceptContext per loop instead of one per request.
- The AsyncAcceptContext now implements IValueTaskSource instead of
Task and doesn't allocate a Task per request.
- Use a preallocated overlapped since we don't have overlapping calls to accept for a single AsyncAcceptContext.
- Remove some dead code from SafeNativeOverlapped

* Clear the nativeRequestContext on dispose

* PR feedback
- Move NativeOverlapped* to the AsyncAcceptContext. This removes the SafeNativeOverlapped allocation as the lifetime is managed by the AsyncAcceptContext
- Rename tcs

* Send the error response before settings the result
This commit is contained in:
David Fowler 2020-12-04 17:45:38 -04:00 коммит произвёл GitHub
Родитель ec2e8a0444
Коммит 5d4dac2f9f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
8 изменённых файлов: 140 добавлений и 140 удалений

Просмотреть файл

@ -1,29 +1,29 @@
{
{
"solution": {
"path": "..\\..\\..\\AspNetCore.sln",
"projects" : [
"src\\Servers\\HttpSys\\samples\\TestClient\\TestClient.csproj",
"src\\Servers\\HttpSys\\samples\\SelfHostServer\\SelfHostServer.csproj",
"src\\Servers\\HttpSys\\samples\\HotAddSample\\HotAddSample.csproj",
"src\\Servers\\HttpSys\\test\\FunctionalTests\\Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj",
"src\\Servers\\HttpSys\\test\\Tests\\Microsoft.AspNetCore.Server.HttpSys.Tests.csproj",
"src\\Servers\\HttpSys\\src\\Microsoft.AspNetCore.Server.HttpSys.csproj",
"projects": [
"src\\DefaultBuilder\\src\\Microsoft.AspNetCore.csproj",
"src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj",
"src\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj",
"src\\Hosting\\Server.Abstractions\\src\\Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj",
"src\\Http\\Authentication.Abstractions\\src\\Microsoft.AspNetCore.Authentication.Abstractions.csproj",
"src\\Http\\Authentication.Core\\src\\Microsoft.AspNetCore.Authentication.Core.csproj",
"src\\Http\\Headers\\src\\Microsoft.Net.Http.Headers.csproj",
"src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj",
"src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj",
"src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj",
"src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj",
"src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj",
"src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj",
"src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj",
"src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj",
"src\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj",
"src\\Servers\\Connections.Abstractions\\src\\Microsoft.AspNetCore.Connections.Abstractions.csproj",
"src\\Servers\\HttpSys\\samples\\HotAddSample\\HotAddSample.csproj",
"src\\Servers\\HttpSys\\samples\\QueueSharing\\QueueSharing.csproj",
"src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj",
"src\\DefaultBuilder\\src\\Microsoft.AspNetCore.csproj"
"src\\Servers\\HttpSys\\samples\\SelfHostServer\\SelfHostServer.csproj",
"src\\Servers\\HttpSys\\samples\\TestClient\\TestClient.csproj",
"src\\Servers\\HttpSys\\src\\Microsoft.AspNetCore.Server.HttpSys.csproj",
"src\\Servers\\HttpSys\\test\\FunctionalTests\\Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj",
"src\\Servers\\HttpSys\\test\\Tests\\Microsoft.AspNetCore.Server.HttpSys.Tests.csproj",
"src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj"
]
}
}
}

Просмотреть файл

@ -2,121 +2,135 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Sources;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpSys.Internal;
namespace Microsoft.AspNetCore.Server.HttpSys
{
internal unsafe class AsyncAcceptContext : TaskCompletionSource<RequestContext>, IDisposable
internal unsafe class AsyncAcceptContext : IValueTaskSource<RequestContext>, IDisposable
{
internal static readonly IOCompletionCallback IOCallback = new IOCompletionCallback(IOWaitCallback);
private static readonly IOCompletionCallback IOCallback = IOWaitCallback;
private readonly PreAllocatedOverlapped _preallocatedOverlapped;
private NativeOverlapped* _overlapped;
// mutable struct; do not make this readonly
private ManualResetValueTaskSourceCore<RequestContext> _mrvts = new()
{
// We want to run continuations on the IO threads
RunContinuationsAsynchronously = false
};
private NativeRequestContext _nativeRequestContext;
internal AsyncAcceptContext(HttpSysListener server)
{
Server = server;
AllocateNativeRequest();
_preallocatedOverlapped = new(IOCallback, state: this, pinData: null);
}
internal HttpSysListener Server { get; }
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Redirecting to callback")]
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Disposed by callback")]
internal ValueTask<RequestContext> AcceptAsync()
{
_mrvts.Reset();
AllocateNativeRequest();
uint statusCode = QueueBeginGetContext();
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS &&
statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
{
// some other bad error, possible(?) return values are:
// ERROR_INVALID_HANDLE, ERROR_INSUFFICIENT_BUFFER, ERROR_OPERATION_ABORTED
return ValueTask.FromException<RequestContext>(new HttpSysException((int)statusCode));
}
return new ValueTask<RequestContext>(this, _mrvts.Version);
}
private static void IOCompleted(AsyncAcceptContext asyncContext, uint errorCode, uint numBytes)
{
bool complete = false;
try
{
if (errorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS &&
errorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_MORE_DATA)
{
asyncContext.TrySetException(new HttpSysException((int)errorCode));
complete = true;
asyncContext._mrvts.SetException(new HttpSysException((int)errorCode));
return;
}
else
{
HttpSysListener server = asyncContext.Server;
if (errorCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
{
// at this point we have received an unmanaged HTTP_REQUEST and memoryBlob
// points to it we need to hook up our authentication handling code here.
try
{
if (server.ValidateRequest(asyncContext._nativeRequestContext) && server.ValidateAuth(asyncContext._nativeRequestContext))
{
RequestContext requestContext = new RequestContext(server, asyncContext._nativeRequestContext);
asyncContext.TrySetResult(requestContext);
complete = true;
}
}
catch (Exception)
{
server.SendError(asyncContext._nativeRequestContext.RequestId, StatusCodes.Status400BadRequest);
throw;
}
finally
{
// The request has been handed to the user, which means this code can't reuse the blob. Reset it here.
if (complete)
{
asyncContext._nativeRequestContext = null;
}
else
{
asyncContext.AllocateNativeRequest(size: asyncContext._nativeRequestContext.Size);
}
}
}
else
{
// (uint)backingBuffer.Length - AlignmentPadding
asyncContext.AllocateNativeRequest(numBytes, asyncContext._nativeRequestContext.RequestId);
}
// We need to issue a new request, either because auth failed, or because our buffer was too small the first time.
if (!complete)
HttpSysListener server = asyncContext.Server;
if (errorCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
{
// at this point we have received an unmanaged HTTP_REQUEST and memoryBlob
// points to it we need to hook up our authentication handling code here.
try
{
uint statusCode = asyncContext.QueueBeginGetContext();
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS &&
statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
var nativeContext = asyncContext._nativeRequestContext;
if (server.ValidateRequest(nativeContext) && server.ValidateAuth(nativeContext))
{
// someother bad error, possible(?) return values are:
// ERROR_INVALID_HANDLE, ERROR_INSUFFICIENT_BUFFER, ERROR_OPERATION_ABORTED
asyncContext.TrySetException(new HttpSysException((int)statusCode));
// It's important that we clear the native request context before we set the result
// we want to reuse this object for future accepts.
asyncContext._nativeRequestContext = null;
var requestContext = new RequestContext(server, nativeContext);
asyncContext._mrvts.SetResult(requestContext);
complete = true;
}
}
if (!complete)
catch (Exception ex)
{
return;
server.SendError(asyncContext._nativeRequestContext.RequestId, StatusCodes.Status400BadRequest);
asyncContext._mrvts.SetException(ex);
}
finally
{
if (!complete)
{
asyncContext.AllocateNativeRequest(size: asyncContext._nativeRequestContext.Size);
}
}
}
if (complete)
else
{
asyncContext.Dispose();
// (uint)backingBuffer.Length - AlignmentPadding
asyncContext.AllocateNativeRequest(numBytes, asyncContext._nativeRequestContext.RequestId);
}
// We need to issue a new request, either because auth failed, or because our buffer was too small the first time.
if (!complete)
{
uint statusCode = asyncContext.QueueBeginGetContext();
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS &&
statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
{
// someother bad error, possible(?) return values are:
// ERROR_INVALID_HANDLE, ERROR_INSUFFICIENT_BUFFER, ERROR_OPERATION_ABORTED
asyncContext._mrvts.SetException(new HttpSysException((int)statusCode));
}
}
}
catch (Exception exception)
{
// Logged by caller
asyncContext.TrySetException(exception);
asyncContext.Dispose();
asyncContext._mrvts.SetException(exception);
}
}
private static unsafe void IOWaitCallback(uint errorCode, uint numBytes, NativeOverlapped* nativeOverlapped)
{
// take the ListenerAsyncResult object from the state
var asyncResult = (AsyncAcceptContext)ThreadPoolBoundHandle.GetNativeOverlappedState(nativeOverlapped);
IOCompleted(asyncResult, errorCode, numBytes);
}
internal uint QueueBeginGetContext()
private uint QueueBeginGetContext()
{
uint statusCode = UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS;
bool retry;
@ -133,7 +147,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
_nativeRequestContext.NativeRequest,
_nativeRequestContext.Size,
&bytesTransferred,
_nativeRequestContext.NativeOverlapped);
_overlapped);
if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_INVALID_PARAMETER && _nativeRequestContext.RequestId != 0)
{
@ -162,18 +176,20 @@ namespace Microsoft.AspNetCore.Server.HttpSys
return statusCode;
}
internal void AllocateNativeRequest(uint? size = null, ulong requestId = 0)
private void AllocateNativeRequest(uint? size = null, ulong requestId = 0)
{
_nativeRequestContext?.ReleasePins();
_nativeRequestContext?.Dispose();
// We can't reuse overlapped objects
var boundHandle = Server.RequestQueue.BoundHandle;
var nativeOverlapped = new SafeNativeOverlapped(boundHandle,
boundHandle.AllocateNativeOverlapped(IOCallback, this, pinData: null));
// nativeRequest
_nativeRequestContext = new NativeRequestContext(nativeOverlapped, Server.MemoryPool, size, requestId);
if (_overlapped != null)
{
boundHandle.FreeNativeOverlapped(_overlapped);
}
_nativeRequestContext = new NativeRequestContext(Server.MemoryPool, size, requestId);
_overlapped = boundHandle.AllocateNativeOverlapped(_preallocatedOverlapped);
}
public void Dispose()
@ -189,8 +205,32 @@ namespace Microsoft.AspNetCore.Server.HttpSys
{
_nativeRequestContext.ReleasePins();
_nativeRequestContext.Dispose();
_nativeRequestContext = null;
var boundHandle = Server.RequestQueue.BoundHandle;
if (_overlapped != null)
{
boundHandle.FreeNativeOverlapped(_overlapped);
_overlapped = null;
}
}
}
}
public RequestContext GetResult(short token)
{
return _mrvts.GetResult(token);
}
public ValueTaskSourceStatus GetStatus(short token)
{
return _mrvts.GetStatus(token);
}
public void OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags)
{
_mrvts.OnCompleted(continuation, state, token, flags);
}
}
}

Просмотреть файл

@ -195,7 +195,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
return;
}
Logger.LogTrace(LoggerEventIds.ListenerStopping,"Stopping the listener.");
Logger.LogTrace(LoggerEventIds.ListenerStopping, "Stopping the listener.");
// If this instance created the queue then remove the URL prefixes before shutting down.
if (_requestQueue.Created)
@ -277,34 +277,12 @@ namespace Microsoft.AspNetCore.Server.HttpSys
/// <summary>
/// Accept a request from the incoming request queue.
/// </summary>
public Task<RequestContext> AcceptAsync()
internal ValueTask<RequestContext> AcceptAsync(AsyncAcceptContext acceptContext)
{
AsyncAcceptContext acceptContext = null;
try
{
CheckDisposed();
Debug.Assert(_state != State.Stopped, "Listener has been stopped.");
// prepare the ListenerAsyncResult object (this will have it's own
// event that the user can wait on for IO completion - which means we
// need to signal it when IO completes)
acceptContext = new AsyncAcceptContext(this);
uint statusCode = acceptContext.QueueBeginGetContext();
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS &&
statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
{
// some other bad error, possible(?) return values are:
// ERROR_INVALID_HANDLE, ERROR_INSUFFICIENT_BUFFER, ERROR_OPERATION_ABORTED
acceptContext.Dispose();
throw new HttpSysException((int)statusCode);
}
}
catch (Exception exception)
{
Logger.LogError(LoggerEventIds.AcceptError, exception, "AcceptAsync");
throw;
}
CheckDisposed();
Debug.Assert(_state != State.Stopped, "Listener has been stopped.");
return acceptContext.Task;
return acceptContext.AcceptAsync();
}
internal unsafe bool ValidateRequest(NativeRequestContext requestMemory)

Просмотреть файл

@ -178,6 +178,9 @@ namespace Microsoft.AspNetCore.Server.HttpSys
// The awaits will manage stack depth for us.
private async Task ProcessRequestsWorker()
{
// Allocate and accept context per loop and reuse it for all accepts
using var acceptContext = new AsyncAcceptContext(Listener);
int workerIndex = Interlocked.Increment(ref _acceptorCounts);
while (!Stopping && workerIndex <= _maxAccepts)
{
@ -185,7 +188,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
RequestContext requestContext;
try
{
requestContext = await Listener.AcceptAsync();
requestContext = await Listener.AcceptAsync(acceptContext);
// Assign the message pump to this request context
requestContext.MessagePump = this;
}

Просмотреть файл

@ -3,6 +3,7 @@
using System;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.AspNetCore.HttpSys.Internal;
using static Microsoft.AspNetCore.HttpSys.Internal.HttpApiTypes;
@ -25,7 +26,7 @@ namespace Microsoft.AspNetCore.Server.HttpSys
internal static extern uint HttpReceiveClientCertificate(SafeHandle requestQueueHandle, ulong connectionId, uint flags, byte* pSslClientCertInfo, uint sslClientCertInfoSize, uint* pBytesReceived, SafeNativeOverlapped pOverlapped);
[DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
internal static extern uint HttpReceiveHttpRequest(SafeHandle requestQueueHandle, ulong requestId, uint flags, HTTP_REQUEST* pRequestBuffer, uint requestBufferLength, uint* pBytesReturned, SafeNativeOverlapped pOverlapped);
internal static extern uint HttpReceiveHttpRequest(SafeHandle requestQueueHandle, ulong requestId, uint flags, HTTP_REQUEST* pRequestBuffer, uint requestBufferLength, uint* pBytesReturned, NativeOverlapped* pOverlapped);
[DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
internal static extern uint HttpSendHttpResponse(SafeHandle requestQueueHandle, ulong requestId, uint flags, HTTP_RESPONSE_V2* pHttpResponse, HTTP_CACHE_POLICY* pCachePolicy, uint* pBytesSent, IntPtr pReserved1, uint Reserved2, SafeNativeOverlapped pOverlapped, IntPtr pLogData);

Просмотреть файл

@ -113,7 +113,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys.Listener
/// </summary>
internal static async Task<RequestContext> AcceptAsync(this HttpSysListener server, TimeSpan timeout)
{
var acceptTask = server.AcceptAsync();
var acceptContext = new AsyncAcceptContext(server);
var acceptTask = server.AcceptAsync(acceptContext).AsTask();
var completedTask = await Task.WhenAny(acceptTask, Task.Delay(timeout));
if (completedTask == acceptTask)

Просмотреть файл

@ -29,21 +29,6 @@ namespace Microsoft.AspNetCore.HttpSys.Internal
get { return handle == IntPtr.Zero; }
}
public void ReinitializeNativeOverlapped()
{
IntPtr handleSnapshot = handle;
if (handleSnapshot != IntPtr.Zero)
{
unsafe
{
((NativeOverlapped*)handleSnapshot)->InternalHigh = IntPtr.Zero;
((NativeOverlapped*)handleSnapshot)->InternalLow = IntPtr.Zero;
((NativeOverlapped*)handleSnapshot)->EventHandle = IntPtr.Zero;
}
}
}
protected override bool ReleaseHandle()
{
IntPtr oldHandle = Interlocked.Exchange(ref handle, IntPtr.Zero);

Просмотреть файл

@ -25,15 +25,12 @@ namespace Microsoft.AspNetCore.HttpSys.Internal
private IMemoryOwner<byte> _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, MemoryPool<Byte> memoryPool, uint? bufferSize, ulong requestId)
internal NativeRequestContext(MemoryPool<byte> memoryPool, uint? bufferSize, ulong requestId)
{
_nativeOverlapped = nativeOverlapped;
// 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
@ -70,8 +67,6 @@ namespace Microsoft.AspNetCore.HttpSys.Internal
_permanentlyPinned = true;
}
internal SafeNativeOverlapped NativeOverlapped => _nativeOverlapped;
internal HttpApiTypes.HTTP_REQUEST* NativeRequest
{
get
@ -130,8 +125,6 @@ namespace Microsoft.AspNetCore.HttpSys.Internal
_memoryHandle.Dispose();
_memoryHandle = default;
_nativeRequest = null;
_nativeOverlapped?.Dispose();
_nativeOverlapped = null;
}
public virtual void Dispose()
@ -140,7 +133,6 @@ namespace Microsoft.AspNetCore.HttpSys.Internal
{
_disposed = true;
Debug.Assert(_nativeRequest == null, "RequestContextBase::Dispose()|Dispose() called before ReleasePins().");
_nativeOverlapped?.Dispose();
_memoryHandle.Dispose();
_backingBuffer.Dispose();
}