Skip to content

Commit 5d4dac2

Browse files
authored
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
1 parent ec2e8a0 commit 5d4dac2

File tree

8 files changed

+136
-136
lines changed

8 files changed

+136
-136
lines changed
Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,29 @@
1-
{
1+
{
22
"solution": {
33
"path": "..\\..\\..\\AspNetCore.sln",
4-
"projects" : [
5-
"src\\Servers\\HttpSys\\samples\\TestClient\\TestClient.csproj",
6-
"src\\Servers\\HttpSys\\samples\\SelfHostServer\\SelfHostServer.csproj",
7-
"src\\Servers\\HttpSys\\samples\\HotAddSample\\HotAddSample.csproj",
8-
"src\\Servers\\HttpSys\\test\\FunctionalTests\\Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj",
9-
"src\\Servers\\HttpSys\\test\\Tests\\Microsoft.AspNetCore.Server.HttpSys.Tests.csproj",
10-
"src\\Servers\\HttpSys\\src\\Microsoft.AspNetCore.Server.HttpSys.csproj",
4+
"projects": [
5+
"src\\DefaultBuilder\\src\\Microsoft.AspNetCore.csproj",
6+
"src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj",
7+
"src\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj",
118
"src\\Hosting\\Server.Abstractions\\src\\Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj",
129
"src\\Http\\Authentication.Abstractions\\src\\Microsoft.AspNetCore.Authentication.Abstractions.csproj",
1310
"src\\Http\\Authentication.Core\\src\\Microsoft.AspNetCore.Authentication.Core.csproj",
1411
"src\\Http\\Headers\\src\\Microsoft.Net.Http.Headers.csproj",
15-
"src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj",
1612
"src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj",
1713
"src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj",
1814
"src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj",
15+
"src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj",
1916
"src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj",
2017
"src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj",
21-
"src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj",
22-
"src\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj",
2318
"src\\Servers\\Connections.Abstractions\\src\\Microsoft.AspNetCore.Connections.Abstractions.csproj",
19+
"src\\Servers\\HttpSys\\samples\\HotAddSample\\HotAddSample.csproj",
2420
"src\\Servers\\HttpSys\\samples\\QueueSharing\\QueueSharing.csproj",
25-
"src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj",
26-
"src\\DefaultBuilder\\src\\Microsoft.AspNetCore.csproj"
21+
"src\\Servers\\HttpSys\\samples\\SelfHostServer\\SelfHostServer.csproj",
22+
"src\\Servers\\HttpSys\\samples\\TestClient\\TestClient.csproj",
23+
"src\\Servers\\HttpSys\\src\\Microsoft.AspNetCore.Server.HttpSys.csproj",
24+
"src\\Servers\\HttpSys\\test\\FunctionalTests\\Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj",
25+
"src\\Servers\\HttpSys\\test\\Tests\\Microsoft.AspNetCore.Server.HttpSys.Tests.csproj",
26+
"src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj"
2727
]
2828
}
29-
}
29+
}

src/Servers/HttpSys/src/AsyncAcceptContext.cs

Lines changed: 108 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -2,121 +2,135 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5-
using System.Diagnostics.CodeAnalysis;
65
using System.Threading;
76
using System.Threading.Tasks;
7+
using System.Threading.Tasks.Sources;
88
using Microsoft.AspNetCore.Http;
99
using Microsoft.AspNetCore.HttpSys.Internal;
1010

1111
namespace Microsoft.AspNetCore.Server.HttpSys
1212
{
13-
internal unsafe class AsyncAcceptContext : TaskCompletionSource<RequestContext>, IDisposable
13+
internal unsafe class AsyncAcceptContext : IValueTaskSource<RequestContext>, IDisposable
1414
{
15-
internal static readonly IOCompletionCallback IOCallback = new IOCompletionCallback(IOWaitCallback);
15+
private static readonly IOCompletionCallback IOCallback = IOWaitCallback;
16+
private readonly PreAllocatedOverlapped _preallocatedOverlapped;
17+
private NativeOverlapped* _overlapped;
18+
19+
// mutable struct; do not make this readonly
20+
private ManualResetValueTaskSourceCore<RequestContext> _mrvts = new()
21+
{
22+
// We want to run continuations on the IO threads
23+
RunContinuationsAsynchronously = false
24+
};
1625

1726
private NativeRequestContext _nativeRequestContext;
1827

1928
internal AsyncAcceptContext(HttpSysListener server)
2029
{
2130
Server = server;
22-
AllocateNativeRequest();
31+
_preallocatedOverlapped = new(IOCallback, state: this, pinData: null);
2332
}
2433

2534
internal HttpSysListener Server { get; }
2635

27-
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Redirecting to callback")]
28-
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Disposed by callback")]
36+
internal ValueTask<RequestContext> AcceptAsync()
37+
{
38+
_mrvts.Reset();
39+
40+
AllocateNativeRequest();
41+
42+
uint statusCode = QueueBeginGetContext();
43+
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS &&
44+
statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
45+
{
46+
// some other bad error, possible(?) return values are:
47+
// ERROR_INVALID_HANDLE, ERROR_INSUFFICIENT_BUFFER, ERROR_OPERATION_ABORTED
48+
return ValueTask.FromException<RequestContext>(new HttpSysException((int)statusCode));
49+
}
50+
51+
return new ValueTask<RequestContext>(this, _mrvts.Version);
52+
}
53+
2954
private static void IOCompleted(AsyncAcceptContext asyncContext, uint errorCode, uint numBytes)
3055
{
3156
bool complete = false;
57+
3258
try
3359
{
3460
if (errorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS &&
3561
errorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_MORE_DATA)
3662
{
37-
asyncContext.TrySetException(new HttpSysException((int)errorCode));
38-
complete = true;
63+
asyncContext._mrvts.SetException(new HttpSysException((int)errorCode));
64+
return;
3965
}
40-
else
66+
67+
HttpSysListener server = asyncContext.Server;
68+
if (errorCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
4169
{
42-
HttpSysListener server = asyncContext.Server;
43-
if (errorCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
70+
// at this point we have received an unmanaged HTTP_REQUEST and memoryBlob
71+
// points to it we need to hook up our authentication handling code here.
72+
try
4473
{
45-
// at this point we have received an unmanaged HTTP_REQUEST and memoryBlob
46-
// points to it we need to hook up our authentication handling code here.
47-
try
48-
{
49-
if (server.ValidateRequest(asyncContext._nativeRequestContext) && server.ValidateAuth(asyncContext._nativeRequestContext))
50-
{
51-
RequestContext requestContext = new RequestContext(server, asyncContext._nativeRequestContext);
52-
asyncContext.TrySetResult(requestContext);
53-
complete = true;
54-
}
55-
}
56-
catch (Exception)
57-
{
58-
server.SendError(asyncContext._nativeRequestContext.RequestId, StatusCodes.Status400BadRequest);
59-
throw;
60-
}
61-
finally
74+
var nativeContext = asyncContext._nativeRequestContext;
75+
76+
if (server.ValidateRequest(nativeContext) && server.ValidateAuth(nativeContext))
6277
{
63-
// The request has been handed to the user, which means this code can't reuse the blob. Reset it here.
64-
if (complete)
65-
{
66-
asyncContext._nativeRequestContext = null;
67-
}
68-
else
69-
{
70-
asyncContext.AllocateNativeRequest(size: asyncContext._nativeRequestContext.Size);
71-
}
78+
// It's important that we clear the native request context before we set the result
79+
// we want to reuse this object for future accepts.
80+
asyncContext._nativeRequestContext = null;
81+
82+
var requestContext = new RequestContext(server, nativeContext);
83+
asyncContext._mrvts.SetResult(requestContext);
84+
85+
complete = true;
7286
}
7387
}
74-
else
88+
catch (Exception ex)
7589
{
76-
// (uint)backingBuffer.Length - AlignmentPadding
77-
asyncContext.AllocateNativeRequest(numBytes, asyncContext._nativeRequestContext.RequestId);
90+
server.SendError(asyncContext._nativeRequestContext.RequestId, StatusCodes.Status400BadRequest);
91+
asyncContext._mrvts.SetException(ex);
7892
}
79-
80-
// We need to issue a new request, either because auth failed, or because our buffer was too small the first time.
81-
if (!complete)
93+
finally
8294
{
83-
uint statusCode = asyncContext.QueueBeginGetContext();
84-
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS &&
85-
statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
95+
if (!complete)
8696
{
87-
// someother bad error, possible(?) return values are:
88-
// ERROR_INVALID_HANDLE, ERROR_INSUFFICIENT_BUFFER, ERROR_OPERATION_ABORTED
89-
asyncContext.TrySetException(new HttpSysException((int)statusCode));
90-
complete = true;
97+
asyncContext.AllocateNativeRequest(size: asyncContext._nativeRequestContext.Size);
9198
}
9299
}
93-
if (!complete)
94-
{
95-
return;
96-
}
100+
}
101+
else
102+
{
103+
// (uint)backingBuffer.Length - AlignmentPadding
104+
asyncContext.AllocateNativeRequest(numBytes, asyncContext._nativeRequestContext.RequestId);
97105
}
98106

99-
if (complete)
107+
// We need to issue a new request, either because auth failed, or because our buffer was too small the first time.
108+
if (!complete)
100109
{
101-
asyncContext.Dispose();
110+
uint statusCode = asyncContext.QueueBeginGetContext();
111+
112+
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS &&
113+
statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
114+
{
115+
// someother bad error, possible(?) return values are:
116+
// ERROR_INVALID_HANDLE, ERROR_INSUFFICIENT_BUFFER, ERROR_OPERATION_ABORTED
117+
asyncContext._mrvts.SetException(new HttpSysException((int)statusCode));
118+
}
102119
}
103120
}
104121
catch (Exception exception)
105122
{
106-
// Logged by caller
107-
asyncContext.TrySetException(exception);
108-
asyncContext.Dispose();
123+
asyncContext._mrvts.SetException(exception);
109124
}
110125
}
111126

112127
private static unsafe void IOWaitCallback(uint errorCode, uint numBytes, NativeOverlapped* nativeOverlapped)
113128
{
114-
// take the ListenerAsyncResult object from the state
115129
var asyncResult = (AsyncAcceptContext)ThreadPoolBoundHandle.GetNativeOverlappedState(nativeOverlapped);
116130
IOCompleted(asyncResult, errorCode, numBytes);
117131
}
118132

119-
internal uint QueueBeginGetContext()
133+
private uint QueueBeginGetContext()
120134
{
121135
uint statusCode = UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS;
122136
bool retry;
@@ -133,7 +147,7 @@ internal uint QueueBeginGetContext()
133147
_nativeRequestContext.NativeRequest,
134148
_nativeRequestContext.Size,
135149
&bytesTransferred,
136-
_nativeRequestContext.NativeOverlapped);
150+
_overlapped);
137151

138152
if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_INVALID_PARAMETER && _nativeRequestContext.RequestId != 0)
139153
{
@@ -162,18 +176,20 @@ internal uint QueueBeginGetContext()
162176
return statusCode;
163177
}
164178

165-
internal void AllocateNativeRequest(uint? size = null, ulong requestId = 0)
179+
private void AllocateNativeRequest(uint? size = null, ulong requestId = 0)
166180
{
167181
_nativeRequestContext?.ReleasePins();
168182
_nativeRequestContext?.Dispose();
169183

170-
// We can't reuse overlapped objects
171184
var boundHandle = Server.RequestQueue.BoundHandle;
172-
var nativeOverlapped = new SafeNativeOverlapped(boundHandle,
173-
boundHandle.AllocateNativeOverlapped(IOCallback, this, pinData: null));
174185

175-
// nativeRequest
176-
_nativeRequestContext = new NativeRequestContext(nativeOverlapped, Server.MemoryPool, size, requestId);
186+
if (_overlapped != null)
187+
{
188+
boundHandle.FreeNativeOverlapped(_overlapped);
189+
}
190+
191+
_nativeRequestContext = new NativeRequestContext(Server.MemoryPool, size, requestId);
192+
_overlapped = boundHandle.AllocateNativeOverlapped(_preallocatedOverlapped);
177193
}
178194

179195
public void Dispose()
@@ -189,8 +205,32 @@ protected virtual void Dispose(bool disposing)
189205
{
190206
_nativeRequestContext.ReleasePins();
191207
_nativeRequestContext.Dispose();
208+
_nativeRequestContext = null;
209+
210+
var boundHandle = Server.RequestQueue.BoundHandle;
211+
212+
if (_overlapped != null)
213+
{
214+
boundHandle.FreeNativeOverlapped(_overlapped);
215+
_overlapped = null;
216+
}
192217
}
193218
}
194219
}
220+
221+
public RequestContext GetResult(short token)
222+
{
223+
return _mrvts.GetResult(token);
224+
}
225+
226+
public ValueTaskSourceStatus GetStatus(short token)
227+
{
228+
return _mrvts.GetStatus(token);
229+
}
230+
231+
public void OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags)
232+
{
233+
_mrvts.OnCompleted(continuation, state, token, flags);
234+
}
195235
}
196236
}

src/Servers/HttpSys/src/HttpSysListener.cs

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ private void Stop()
195195
return;
196196
}
197197

198-
Logger.LogTrace(LoggerEventIds.ListenerStopping,"Stopping the listener.");
198+
Logger.LogTrace(LoggerEventIds.ListenerStopping, "Stopping the listener.");
199199

200200
// If this instance created the queue then remove the URL prefixes before shutting down.
201201
if (_requestQueue.Created)
@@ -277,34 +277,12 @@ private void DisposeInternal()
277277
/// <summary>
278278
/// Accept a request from the incoming request queue.
279279
/// </summary>
280-
public Task<RequestContext> AcceptAsync()
280+
internal ValueTask<RequestContext> AcceptAsync(AsyncAcceptContext acceptContext)
281281
{
282-
AsyncAcceptContext acceptContext = null;
283-
try
284-
{
285-
CheckDisposed();
286-
Debug.Assert(_state != State.Stopped, "Listener has been stopped.");
287-
// prepare the ListenerAsyncResult object (this will have it's own
288-
// event that the user can wait on for IO completion - which means we
289-
// need to signal it when IO completes)
290-
acceptContext = new AsyncAcceptContext(this);
291-
uint statusCode = acceptContext.QueueBeginGetContext();
292-
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS &&
293-
statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
294-
{
295-
// some other bad error, possible(?) return values are:
296-
// ERROR_INVALID_HANDLE, ERROR_INSUFFICIENT_BUFFER, ERROR_OPERATION_ABORTED
297-
acceptContext.Dispose();
298-
throw new HttpSysException((int)statusCode);
299-
}
300-
}
301-
catch (Exception exception)
302-
{
303-
Logger.LogError(LoggerEventIds.AcceptError, exception, "AcceptAsync");
304-
throw;
305-
}
282+
CheckDisposed();
283+
Debug.Assert(_state != State.Stopped, "Listener has been stopped.");
306284

307-
return acceptContext.Task;
285+
return acceptContext.AcceptAsync();
308286
}
309287

310288
internal unsafe bool ValidateRequest(NativeRequestContext requestMemory)

src/Servers/HttpSys/src/MessagePump.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,14 +178,17 @@ internal void SetShutdownSignal()
178178
// The awaits will manage stack depth for us.
179179
private async Task ProcessRequestsWorker()
180180
{
181+
// Allocate and accept context per loop and reuse it for all accepts
182+
using var acceptContext = new AsyncAcceptContext(Listener);
183+
181184
int workerIndex = Interlocked.Increment(ref _acceptorCounts);
182185
while (!Stopping && workerIndex <= _maxAccepts)
183186
{
184187
// Receive a request
185188
RequestContext requestContext;
186189
try
187190
{
188-
requestContext = await Listener.AcceptAsync();
191+
requestContext = await Listener.AcceptAsync(acceptContext);
189192
// Assign the message pump to this request context
190193
requestContext.MessagePump = this;
191194
}

src/Servers/HttpSys/src/NativeInterop/HttpApi.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Runtime.InteropServices;
6+
using System.Threading;
67
using Microsoft.AspNetCore.HttpSys.Internal;
78
using static Microsoft.AspNetCore.HttpSys.Internal.HttpApiTypes;
89

@@ -25,7 +26,7 @@ internal static unsafe class HttpApi
2526
internal static extern uint HttpReceiveClientCertificate(SafeHandle requestQueueHandle, ulong connectionId, uint flags, byte* pSslClientCertInfo, uint sslClientCertInfoSize, uint* pBytesReceived, SafeNativeOverlapped pOverlapped);
2627

2728
[DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
28-
internal static extern uint HttpReceiveHttpRequest(SafeHandle requestQueueHandle, ulong requestId, uint flags, HTTP_REQUEST* pRequestBuffer, uint requestBufferLength, uint* pBytesReturned, SafeNativeOverlapped pOverlapped);
29+
internal static extern uint HttpReceiveHttpRequest(SafeHandle requestQueueHandle, ulong requestId, uint flags, HTTP_REQUEST* pRequestBuffer, uint requestBufferLength, uint* pBytesReturned, NativeOverlapped* pOverlapped);
2930

3031
[DllImport(HTTPAPI, ExactSpelling = true, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
3132
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);

0 commit comments

Comments
 (0)