Merge pull request #767 from aspnet/pakrym/perf

Improve performance
This commit is contained in:
Pavel Krymets 2018-04-10 12:07:26 -07:00 committed by GitHub
commit 6629236a0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 266 additions and 173 deletions

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>ASP.NET Core components for working with the IIS AspNetCoreModule.</Description>

View File

@ -89,7 +89,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
[DllImport(AspNetCoreModuleDll)]
private static extern int http_get_server_variable(
IntPtr pInProcessHandler,
[MarshalAs(UnmanagedType.AnsiBStr)] string variableName,
[MarshalAs(UnmanagedType.LPStr)] string variableName,
[MarshalAs(UnmanagedType.BStr)] out string value);
[DllImport(AspNetCoreModuleDll)]

View File

@ -20,9 +20,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
IHttpRequestFeature,
IHttpResponseFeature,
IHttpUpgradeFeature,
IHttpConnectionFeature,
IHttpRequestLifetimeFeature,
IHttpRequestIdentifierFeature,
IHttpAuthenticationFeature,
IServerVariablesFeature
{
@ -183,49 +181,12 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
bool IHttpResponseFeature.HasStarted => HasResponseStarted;
// The UpgradeAvailable Feature is set on the first request to the server.
bool IHttpUpgradeFeature.IsUpgradableRequest => _websocketAvailability == WebsocketAvailabilityStatus.Available;
bool IHttpUpgradeFeature.IsUpgradableRequest => _server.IsWebSocketAvailible(_pInProcessHandler);
bool IFeatureCollection.IsReadOnly => false;
int IFeatureCollection.Revision => _featureRevision;
IPAddress IHttpConnectionFeature.RemoteIpAddress
{
get => RemoteIpAddress;
set => RemoteIpAddress = value;
}
IPAddress IHttpConnectionFeature.LocalIpAddress
{
get => LocalIpAddress;
set => LocalIpAddress = value;
}
int IHttpConnectionFeature.RemotePort
{
get => RemotePort;
set => RemotePort = value;
}
int IHttpConnectionFeature.LocalPort
{
get => LocalPort;
set => LocalPort = value;
}
string IHttpConnectionFeature.ConnectionId
{
get => RequestConnectionId;
set => RequestConnectionId = value;
}
string IHttpRequestIdentifierFeature.TraceIdentifier
{
get => TraceIdentifier;
set => TraceIdentifier = value;
}
ClaimsPrincipal IHttpAuthenticationFeature.User
{
get => User;

View File

@ -0,0 +1,100 @@
// 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.Globalization;
using System.Net;
using Microsoft.AspNetCore.Http.Features;
namespace Microsoft.AspNetCore.Server.IISIntegration
{
internal partial class IISHttpContext : IHttpConnectionFeature
{
IPAddress IHttpConnectionFeature.RemoteIpAddress
{
get
{
if (RemoteIpAddress == null)
{
InitializeRemoteEndpoint();
}
return RemoteIpAddress;
}
set => RemoteIpAddress = value;
}
IPAddress IHttpConnectionFeature.LocalIpAddress
{
get
{
if (LocalIpAddress == null)
{
InitializeLocalEndpoint();
}
return LocalIpAddress;
}
set => LocalIpAddress = value;
}
int IHttpConnectionFeature.RemotePort
{
get
{
if (RemoteIpAddress == null)
{
InitializeRemoteEndpoint();
}
return RemotePort;
}
set => RemotePort = value;
}
int IHttpConnectionFeature.LocalPort
{
get
{
if (LocalIpAddress == null)
{
InitializeLocalEndpoint();
}
return LocalPort;
}
set => LocalPort = value;
}
string IHttpConnectionFeature.ConnectionId
{
get
{
if (RequestConnectionId == null)
{
InitializeConnectionId();
}
return RequestConnectionId;
}
set => RequestConnectionId = value;
}
private void InitializeLocalEndpoint()
{
var localEndPoint = GetLocalEndPoint();
LocalIpAddress = localEndPoint.GetIPAddress();
LocalPort = localEndPoint.GetPort();
}
private void InitializeRemoteEndpoint()
{
var remoteEndPoint = GetRemoteEndPoint();
RemoteIpAddress = remoteEndPoint.GetIPAddress();
RemotePort = remoteEndPoint.GetPort();
}
private void InitializeConnectionId()
{
RequestConnectionId = ConnectionId.ToString(CultureInfo.InvariantCulture);
}
}
}

View File

@ -0,0 +1,38 @@
// 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 Microsoft.AspNetCore.Http.Features;
namespace Microsoft.AspNetCore.Server.IISIntegration
{
internal partial class IISHttpContext : IHttpRequestIdentifierFeature
{
string IHttpRequestIdentifierFeature.TraceIdentifier
{
get
{
if (TraceIdentifier == null)
{
InitializeHttpRequestIdentifierFeature();
}
return TraceIdentifier;
}
set => TraceIdentifier = value;
}
private unsafe void InitializeHttpRequestIdentifierFeature()
{
// Copied from WebListener
// This is the base GUID used by HTTP.SYS for generating the activity ID.
// HTTP.SYS overwrites the first 8 bytes of the base GUID with RequestId to generate ETW activity ID.
// The requestId should be set by the NativeRequestContext
var guid = new Guid(0xffcb4c93, 0xa57f, 0x453c, 0xb6, 0x3f, 0x84, 0x71, 0xc, 0x79, 0x67, 0xbb);
*((ulong*)&guid) = RequestId;
// TODO: Also make this not slow
TraceIdentifier = guid.ToString();
}
}
}

View File

@ -28,11 +28,11 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
private const int PauseWriterThreshold = 65536;
private const int ResumeWriterTheshold = PauseWriterThreshold / 2;
// TODO make this static again.
private static WebsocketAvailabilityStatus _websocketAvailability = WebsocketAvailabilityStatus.Uninitialized;
protected readonly IntPtr _pInProcessHandler;
private readonly IISOptions _options;
private bool _reading; // To know whether we are currently in a read operation.
private volatile bool _hasResponseStarted;
@ -60,116 +60,14 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
private const string NtlmString = "NTLM";
private const string NegotiateString = "Negotiate";
private const string BasicString = "Basic";
private const string WebSocketVersionString = "WEBSOCKET_VERSION";
internal unsafe IISHttpContext(MemoryPool<byte> memoryPool, IntPtr pInProcessHandler, IISOptions options, IISHttpServer server)
: base((HttpApiTypes.HTTP_REQUEST*)NativeMethods.HttpGetRawRequest(pInProcessHandler))
{
_thisHandle = GCHandle.Alloc(this);
_memoryPool = memoryPool;
_pInProcessHandler = pInProcessHandler;
_options = options;
_server = server;
NativeMethods.HttpSetManagedContext(pInProcessHandler, (IntPtr)_thisHandle);
unsafe
{
Method = GetVerb();
RawTarget = GetRawUrl();
// TODO version is slow.
HttpVersion = GetVersion();
Scheme = SslStatus != SslStatus.Insecure ? Constants.HttpsScheme : Constants.HttpScheme;
KnownMethod = VerbId;
var originalPath = RequestUriBuilder.DecodeAndUnescapePath(GetRawUrlInBytes());
if (KnownMethod == HttpApiTypes.HTTP_VERB.HttpVerbOPTIONS && string.Equals(RawTarget, "*", StringComparison.Ordinal))
{
PathBase = string.Empty;
Path = string.Empty;
}
else
{
// Path and pathbase are unescaped by RequestUriBuilder
// The UsePathBase middleware will modify the pathbase and path correctly
PathBase = string.Empty;
Path = originalPath;
}
var cookedUrl = GetCookedUrl();
QueryString = cookedUrl.GetQueryString() ?? string.Empty;
// TODO: Avoid using long.ToString, it's pretty slow
RequestConnectionId = ConnectionId.ToString(CultureInfo.InvariantCulture);
// Copied from WebListener
// This is the base GUID used by HTTP.SYS for generating the activity ID.
// HTTP.SYS overwrites the first 8 bytes of the base GUID with RequestId to generate ETW activity ID.
// The requestId should be set by the NativeRequestContext
var guid = new Guid(0xffcb4c93, 0xa57f, 0x453c, 0xb6, 0x3f, 0x84, 0x71, 0xc, 0x79, 0x67, 0xbb);
*((ulong*)&guid) = RequestId;
// TODO: Also make this not slow
TraceIdentifier = guid.ToString();
var localEndPoint = GetLocalEndPoint();
LocalIpAddress = localEndPoint.GetIPAddress();
LocalPort = localEndPoint.GetPort();
var remoteEndPoint = GetRemoteEndPoint();
RemoteIpAddress = remoteEndPoint.GetIPAddress();
RemotePort = remoteEndPoint.GetPort();
StatusCode = 200;
RequestHeaders = new RequestHeaders(this);
HttpResponseHeaders = new HeaderCollection();
ResponseHeaders = HttpResponseHeaders;
if (options.ForwardWindowsAuthentication)
{
WindowsUser = GetWindowsPrincipal();
if (options.AutomaticAuthentication)
{
User = WindowsUser;
}
}
ResetFeatureCollection();
// Check if the Http upgrade feature is available in IIS.
// To check this, we can look at the server variable WEBSOCKET_VERSION
// And see if there is a version. Same check that Katana did:
// https://github.com/aspnet/AspNetKatana/blob/9f6e09af6bf203744feb5347121fe25f6eec06d8/src/Microsoft.Owin.Host.SystemWeb/OwinAppContext.cs#L125
// Actively not locking here as acquiring a lock on every request will hurt perf more than checking the
// server variables a few extra times if a bunch of requests hit the server at the same time.
if (_websocketAvailability == WebsocketAvailabilityStatus.Uninitialized)
{
var webSocketsAvailable = NativeMethods.HttpTryGetServerVariable(pInProcessHandler, WebSocketVersionString, out var webSocketsSupported)
&& !string.IsNullOrEmpty(webSocketsSupported);
_websocketAvailability = webSocketsAvailable ?
WebsocketAvailabilityStatus.Available :
WebsocketAvailabilityStatus.NotAvailable;
}
if (_websocketAvailability == WebsocketAvailabilityStatus.NotAvailable)
{
_currentIHttpUpgradeFeature = null;
}
}
RequestBody = new IISHttpRequestBody(this);
ResponseBody = new IISHttpResponseBody(this);
Input = new Pipe(new PipeOptions(_memoryPool, readerScheduler: PipeScheduler.ThreadPool, minimumSegmentSize: MinAllocBufferSize));
var pipe = new Pipe(new PipeOptions(
_memoryPool,
readerScheduler: PipeScheduler.ThreadPool,
pauseWriterThreshold: PauseWriterThreshold,
resumeWriterThreshold: ResumeWriterTheshold,
minimumSegmentSize: MinAllocBufferSize));
Output = new OutputProducer(pipe);
}
public Version HttpVersion { get; set; }
@ -197,7 +95,74 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
public IHeaderDictionary RequestHeaders { get; set; }
public IHeaderDictionary ResponseHeaders { get; set; }
private HeaderCollection HttpResponseHeaders { get; set; }
internal HttpApiTypes.HTTP_VERB KnownMethod { get; }
internal HttpApiTypes.HTTP_VERB KnownMethod { get; private set; }
protected void InitializeContext()
{
_thisHandle = GCHandle.Alloc(this);
NativeMethods.HttpSetManagedContext(_pInProcessHandler, (IntPtr)_thisHandle);
Method = GetVerb();
RawTarget = GetRawUrl();
// TODO version is slow.
HttpVersion = GetVersion();
Scheme = SslStatus != SslStatus.Insecure ? Constants.HttpsScheme : Constants.HttpScheme;
KnownMethod = VerbId;
StatusCode = 200;
var originalPath = RequestUriBuilder.DecodeAndUnescapePath(GetRawUrlInBytes());
if (KnownMethod == HttpApiTypes.HTTP_VERB.HttpVerbOPTIONS && string.Equals(RawTarget, "*", StringComparison.Ordinal))
{
PathBase = string.Empty;
Path = string.Empty;
}
else
{
// Path and pathbase are unescaped by RequestUriBuilder
// The UsePathBase middleware will modify the pathbase and path correctly
PathBase = string.Empty;
Path = originalPath;
}
var cookedUrl = GetCookedUrl();
QueryString = cookedUrl.GetQueryString() ?? string.Empty;
RequestHeaders = new RequestHeaders(this);
HttpResponseHeaders = new HeaderCollection();
ResponseHeaders = HttpResponseHeaders;
if (_options.ForwardWindowsAuthentication)
{
WindowsUser = GetWindowsPrincipal();
if (_options.AutomaticAuthentication)
{
User = WindowsUser;
}
}
ResetFeatureCollection();
if (!_server.IsWebSocketAvailible(_pInProcessHandler))
{
_currentIHttpUpgradeFeature = null;
}
RequestBody = new IISHttpRequestBody(this);
ResponseBody = new IISHttpResponseBody(this);
Input = new Pipe(new PipeOptions(_memoryPool, readerScheduler: PipeScheduler.ThreadPool, minimumSegmentSize: MinAllocBufferSize));
var pipe = new Pipe(
new PipeOptions(
_memoryPool,
readerScheduler: PipeScheduler.ThreadPool,
pauseWriterThreshold: PauseWriterThreshold,
resumeWriterThreshold: ResumeWriterTheshold,
minimumSegmentSize: MinAllocBufferSize));
Output = new OutputProducer(pipe);
}
public int StatusCode
{
@ -545,12 +510,5 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
}
return null;
}
private enum WebsocketAvailabilityStatus
{
Uninitialized = 1,
Available,
NotAvailable
}
}
}

View File

@ -22,6 +22,8 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
public override async Task<bool> ProcessRequestAsync()
{
InitializeContext();
var context = default(TContext);
var success = true;

View File

@ -17,6 +17,8 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
{
internal class IISHttpServer : IServer
{
private const string WebSocketVersionString = "WEBSOCKET_VERSION";
private static NativeMethods.PFN_REQUEST_HANDLER _requestHandler = HandleRequest;
private static NativeMethods.PFN_SHUTDOWN_HANDLER _shutdownHandler = HandleShutdown;
private static NativeMethods.PFN_ASYNC_COMPLETION _onAsyncCompletion = OnAsyncCompletion;
@ -32,8 +34,28 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
private bool Stopping => _stopping == 1;
private int _outstandingRequests;
private readonly TaskCompletionSource<object> _shutdownSignal = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
private bool? _websocketAvailable;
public IFeatureCollection Features { get; } = new FeatureCollection();
// TODO: Remove pInProcessHandler argument
public bool IsWebSocketAvailible(IntPtr pInProcessHandler)
{
// Check if the Http upgrade feature is available in IIS.
// To check this, we can look at the server variable WEBSOCKET_VERSION
// And see if there is a version. Same check that Katana did:
// https://github.com/aspnet/AspNetKatana/blob/9f6e09af6bf203744feb5347121fe25f6eec06d8/src/Microsoft.Owin.Host.SystemWeb/OwinAppContext.cs#L125
// Actively not locking here as acquiring a lock on every request will hurt perf more than checking the
// server variables a few extra times if a bunch of requests hit the server at the same time.
if (!_websocketAvailable.HasValue)
{
_websocketAvailable = NativeMethods.HttpTryGetServerVariable(pInProcessHandler, WebSocketVersionString, out var webSocketsSupported)
&& !string.IsNullOrEmpty(webSocketsSupported);
}
return _websocketAvailable.Value;
}
public IISHttpServer(IApplicationLifetime applicationLifetime, IAuthenticationSchemeProvider authentication, IOptions<IISOptions> options)
{
_applicationLifetime = applicationLifetime;
@ -124,13 +146,17 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
var context = server._iisContextFactory.CreateHttpContext(pInProcessHandler);
var task = Task.Run(() => context.ProcessRequestAsync());
task.ContinueWith((t, state) => CompleteRequest((IISHttpContext)state, t), context);
ThreadPool.QueueUserWorkItem(state => _ = HandleRequest((IISHttpContext)state), context);
return NativeMethods.REQUEST_NOTIFICATION_STATUS.RQ_NOTIFICATION_PENDING;
}
private static async Task HandleRequest(IISHttpContext context)
{
var result = await context.ProcessRequestAsync();
CompleteRequest(context, result);
}
private static bool HandleShutdown(IntPtr pvRequestContext)
{
var server = (IISHttpServer)GCHandle.FromIntPtr(pvRequestContext).Target;
@ -145,10 +171,10 @@ namespace Microsoft.AspNetCore.Server.IISIntegration
return NativeMethods.REQUEST_NOTIFICATION_STATUS.RQ_NOTIFICATION_PENDING;
}
private static void CompleteRequest(IISHttpContext context, Task<bool> completedTask)
private static void CompleteRequest(IISHttpContext context, bool result)
{
// Post completion after completing the request to resume the state machine
context.PostCompletion(ConvertRequestCompletionResults(completedTask.Result));
context.PostCompletion(ConvertRequestCompletionResults(result));
if (Interlocked.Decrement(ref context.Server._outstandingRequests) == 0 && context.Server.Stopping)
{

View File

@ -194,7 +194,7 @@ http_read_request_bytes(
_Out_ BOOL* pfCompletionPending
)
{
HRESULT hr;
HRESULT hr = S_OK;
if (pInProcessHandler == NULL)
{
@ -206,19 +206,27 @@ http_read_request_bytes(
}
IHttpRequest *pHttpRequest = (IHttpRequest*)pInProcessHandler->QueryHttpContext()->GetRequest();
BOOL fAsync = TRUE;
hr = pHttpRequest->ReadEntityBody(
pvBuffer,
dwCbBuffer,
fAsync,
pdwBytesReceived,
pfCompletionPending);
if (hr == HRESULT_FROM_WIN32(ERROR_HANDLE_EOF))
// Check if there is anything to read
if (pHttpRequest->GetRemainingEntityBytes() > 0)
{
// We reached the end of the data
hr = S_OK;
BOOL fAsync = TRUE;
hr = pHttpRequest->ReadEntityBody(
pvBuffer,
dwCbBuffer,
fAsync,
pdwBytesReceived,
pfCompletionPending);
if (hr == HRESULT_FROM_WIN32(ERROR_HANDLE_EOF))
{
// We reached the end of the data
hr = S_OK;
}
}
else
{
*pdwBytesReceived = 0;
*pfCompletionPending = FALSE;
}
return hr;