Skip to content

Commit 5823bba

Browse files
committed
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
1 parent 6ea0926 commit 5823bba

File tree

6 files changed

+115
-124
lines changed

6 files changed

+115
-124
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: 90 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2,121 +2,132 @@
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;
8-
using Microsoft.AspNetCore.Http;
7+
using System.Threading.Tasks.Sources;
98
using Microsoft.AspNetCore.HttpSys.Internal;
109

1110
namespace Microsoft.AspNetCore.Server.HttpSys
1211
{
13-
internal unsafe class AsyncAcceptContext : TaskCompletionSource<RequestContext>, IDisposable
12+
internal unsafe class AsyncAcceptContext : IValueTaskSource<RequestContext>, IDisposable
1413
{
15-
internal static readonly IOCompletionCallback IOCallback = new IOCompletionCallback(IOWaitCallback);
14+
private static readonly IOCompletionCallback IOCallback = IOWaitCallback;
15+
private readonly PreAllocatedOverlapped _preallocatedOverlapped;
16+
17+
// mutable struct; do not make this readonly
18+
private ManualResetValueTaskSourceCore<RequestContext> _tcs = new()
19+
{
20+
// We want to run continuations on the IO threads
21+
RunContinuationsAsynchronously = false
22+
};
1623

1724
private NativeRequestContext _nativeRequestContext;
1825

1926
internal AsyncAcceptContext(HttpSysListener server)
2027
{
2128
Server = server;
22-
AllocateNativeRequest();
29+
_preallocatedOverlapped = new(IOCallback, state: this, pinData: null);
2330
}
2431

2532
internal HttpSysListener Server { get; }
2633

27-
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Redirecting to callback")]
28-
[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Disposed by callback")]
34+
internal ValueTask<RequestContext> AcceptAsync()
35+
{
36+
_tcs.Reset();
37+
38+
AllocateNativeRequest();
39+
40+
uint statusCode = QueueBeginGetContext();
41+
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS &&
42+
statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
43+
{
44+
// some other bad error, possible(?) return values are:
45+
// ERROR_INVALID_HANDLE, ERROR_INSUFFICIENT_BUFFER, ERROR_OPERATION_ABORTED
46+
return ValueTask.FromException<RequestContext>(new HttpSysException((int)statusCode));
47+
}
48+
49+
return new ValueTask<RequestContext>(this, _tcs.Version);
50+
}
51+
2952
private static void IOCompleted(AsyncAcceptContext asyncContext, uint errorCode, uint numBytes)
3053
{
3154
bool complete = false;
55+
3256
try
3357
{
3458
if (errorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS &&
3559
errorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_MORE_DATA)
3660
{
37-
asyncContext.TrySetException(new HttpSysException((int)errorCode));
38-
complete = true;
61+
asyncContext._tcs.SetException(new HttpSysException((int)errorCode));
62+
return;
3963
}
40-
else
64+
65+
HttpSysListener server = asyncContext.Server;
66+
if (errorCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
4167
{
42-
HttpSysListener server = asyncContext.Server;
43-
if (errorCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS)
68+
// at this point we have received an unmanaged HTTP_REQUEST and memoryBlob
69+
// points to it we need to hook up our authentication handling code here.
70+
try
4471
{
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
72+
var nativeContext = asyncContext._nativeRequestContext;
73+
74+
if (server.ValidateRequest(nativeContext) && server.ValidateAuth(nativeContext))
6275
{
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-
}
76+
// It's important that we clear the native request context before we set the result
77+
// we want to reuse this object for future accepts.
78+
asyncContext._nativeRequestContext = null;
79+
80+
var requestContext = new RequestContext(server, nativeContext);
81+
asyncContext._tcs.SetResult(requestContext);
82+
83+
complete = true;
7284
}
7385
}
74-
else
86+
catch (Exception ex)
7587
{
76-
// (uint)backingBuffer.Length - AlignmentPadding
77-
asyncContext.AllocateNativeRequest(numBytes, asyncContext._nativeRequestContext.RequestId);
88+
asyncContext._tcs.SetException(ex);
7889
}
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)
90+
finally
8291
{
83-
uint statusCode = asyncContext.QueueBeginGetContext();
84-
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS &&
85-
statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
92+
if (!complete)
8693
{
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;
94+
asyncContext.AllocateNativeRequest(size: asyncContext._nativeRequestContext.Size);
9195
}
9296
}
93-
if (!complete)
94-
{
95-
return;
96-
}
97+
}
98+
else
99+
{
100+
// (uint)backingBuffer.Length - AlignmentPadding
101+
asyncContext.AllocateNativeRequest(numBytes, asyncContext._nativeRequestContext.RequestId);
97102
}
98103

99-
if (complete)
104+
// We need to issue a new request, either because auth failed, or because our buffer was too small the first time.
105+
if (!complete)
100106
{
101-
asyncContext.Dispose();
107+
uint statusCode = asyncContext.QueueBeginGetContext();
108+
109+
if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS &&
110+
statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING)
111+
{
112+
// someother bad error, possible(?) return values are:
113+
// ERROR_INVALID_HANDLE, ERROR_INSUFFICIENT_BUFFER, ERROR_OPERATION_ABORTED
114+
asyncContext._tcs.SetException(new HttpSysException((int)statusCode));
115+
}
102116
}
103117
}
104118
catch (Exception exception)
105119
{
106-
// Logged by caller
107-
asyncContext.TrySetException(exception);
108-
asyncContext.Dispose();
120+
asyncContext._tcs.SetException(exception);
109121
}
110122
}
111123

112124
private static unsafe void IOWaitCallback(uint errorCode, uint numBytes, NativeOverlapped* nativeOverlapped)
113125
{
114-
// take the ListenerAsyncResult object from the state
115126
var asyncResult = (AsyncAcceptContext)ThreadPoolBoundHandle.GetNativeOverlappedState(nativeOverlapped);
116127
IOCompleted(asyncResult, errorCode, numBytes);
117128
}
118129

119-
internal uint QueueBeginGetContext()
130+
private uint QueueBeginGetContext()
120131
{
121132
uint statusCode = UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS;
122133
bool retry;
@@ -162,17 +173,15 @@ internal uint QueueBeginGetContext()
162173
return statusCode;
163174
}
164175

165-
internal void AllocateNativeRequest(uint? size = null, ulong requestId = 0)
176+
private void AllocateNativeRequest(uint? size = null, ulong requestId = 0)
166177
{
167178
_nativeRequestContext?.ReleasePins();
168179
_nativeRequestContext?.Dispose();
169180

170-
// We can't reuse overlapped objects
171181
var boundHandle = Server.RequestQueue.BoundHandle;
172182
var nativeOverlapped = new SafeNativeOverlapped(boundHandle,
173-
boundHandle.AllocateNativeOverlapped(IOCallback, this, pinData: null));
183+
boundHandle.AllocateNativeOverlapped(_preallocatedOverlapped));
174184

175-
// nativeRequest
176185
_nativeRequestContext = new NativeRequestContext(nativeOverlapped, Server.MemoryPool, size, requestId);
177186
}
178187

@@ -192,5 +201,20 @@ protected virtual void Dispose(bool disposing)
192201
}
193202
}
194203
}
204+
205+
public RequestContext GetResult(short token)
206+
{
207+
return _tcs.GetResult(token);
208+
}
209+
210+
public ValueTaskSourceStatus GetStatus(short token)
211+
{
212+
return _tcs.GetStatus(token);
213+
}
214+
215+
public void OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags)
216+
{
217+
_tcs.OnCompleted(continuation, state, token, flags);
218+
}
195219
}
196220
}

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
@@ -176,14 +176,17 @@ internal void SetShutdownSignal()
176176
// The awaits will manage stack depth for us.
177177
private async void ProcessRequestsWorker()
178178
{
179+
// Allocate and accept context per loop and reuse it for all accepts
180+
using var acceptContext = new AsyncAcceptContext(Listener);
181+
179182
int workerIndex = Interlocked.Increment(ref _acceptorCounts);
180183
while (!Stopping && workerIndex <= _maxAccepts)
181184
{
182185
// Receive a request
183186
RequestContext requestContext;
184187
try
185188
{
186-
requestContext = await Listener.AcceptAsync().SupressContext();
189+
requestContext = await Listener.AcceptAsync(acceptContext);
187190
// Assign the message pump to this request context
188191
requestContext.MessagePump = this;
189192
}

src/Servers/HttpSys/test/FunctionalTests/Listener/Utilities.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ internal static HttpSysListener CreateServerOnExistingQueue(AuthenticationScheme
113113
/// </summary>
114114
internal static async Task<RequestContext> AcceptAsync(this HttpSysListener server, TimeSpan timeout)
115115
{
116-
var acceptTask = server.AcceptAsync();
116+
var acceptContext = new AsyncAcceptContext(server);
117+
var acceptTask = server.AcceptAsync(acceptContext).AsTask();
117118
var completedTask = await Task.WhenAny(acceptTask, Task.Delay(timeout));
118119

119120
if (completedTask == acceptTask)

src/Shared/HttpSys/NativeInterop/SafeNativeOverlapped.cs

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,6 @@ public override bool IsInvalid
2929
get { return handle == IntPtr.Zero; }
3030
}
3131

32-
public void ReinitializeNativeOverlapped()
33-
{
34-
IntPtr handleSnapshot = handle;
35-
36-
if (handleSnapshot != IntPtr.Zero)
37-
{
38-
unsafe
39-
{
40-
((NativeOverlapped*)handleSnapshot)->InternalHigh = IntPtr.Zero;
41-
((NativeOverlapped*)handleSnapshot)->InternalLow = IntPtr.Zero;
42-
((NativeOverlapped*)handleSnapshot)->EventHandle = IntPtr.Zero;
43-
}
44-
}
45-
}
46-
4732
protected override bool ReleaseHandle()
4833
{
4934
IntPtr oldHandle = Interlocked.Exchange(ref handle, IntPtr.Zero);

0 commit comments

Comments
 (0)