From dca31cc6f65b114eb8e2d32286d7b46a3166b3db Mon Sep 17 00:00:00 2001 From: Pavel Krymets Date: Mon, 9 Apr 2018 10:34:44 -0700 Subject: [PATCH] Perf --- .../NativeIISSample/PlaintextMiddleware.cs | 51 +++++++ samples/NativeIISSample/Startup.cs | 2 + src/IISLib/macros.h | 7 + .../IISHttpContext.FeatureCollection.cs | 41 +----- .../IISHttpContext.IHttpConnectionFeature.cs | 100 +++++++++++++ ...tpContext.IHttpRequestIdentifierFeature.cs | 38 +++++ .../Server/IISHttpContext.cs | 137 ++++++------------ .../Server/IISHttpServer.cs | 23 +++ 8 files changed, 266 insertions(+), 133 deletions(-) create mode 100644 samples/NativeIISSample/PlaintextMiddleware.cs create mode 100644 src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.IHttpConnectionFeature.cs create mode 100644 src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.IHttpRequestIdentifierFeature.cs diff --git a/samples/NativeIISSample/PlaintextMiddleware.cs b/samples/NativeIISSample/PlaintextMiddleware.cs new file mode 100644 index 0000000000..c7915a968b --- /dev/null +++ b/samples/NativeIISSample/PlaintextMiddleware.cs @@ -0,0 +1,51 @@ +// 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; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace Benchmarks.Middleware +{ + public class PlaintextMiddleware + { + private static readonly PathString _path = new PathString("/plaintext"); + private static readonly byte[] _helloWorldPayload = Encoding.UTF8.GetBytes("Hello, World!"); + + private readonly RequestDelegate _next; + + public PlaintextMiddleware(RequestDelegate next) + { + _next = next; + } + + public Task Invoke(HttpContext httpContext) + { + if (httpContext.Request.Path.StartsWithSegments(_path, StringComparison.Ordinal)) + { + return WriteResponse(httpContext.Response); + } + + return _next(httpContext); + } + + public static Task WriteResponse(HttpResponse response) + { + var payloadLength = _helloWorldPayload.Length; + response.StatusCode = 200; + response.ContentType = "text/plain"; + response.ContentLength = payloadLength; + return response.Body.WriteAsync(_helloWorldPayload, 0, payloadLength); + } + } + + public static class PlaintextMiddlewareExtensions + { + public static IApplicationBuilder UsePlainText(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} diff --git a/samples/NativeIISSample/Startup.cs b/samples/NativeIISSample/Startup.cs index 1a41f5a848..59af05d747 100644 --- a/samples/NativeIISSample/Startup.cs +++ b/samples/NativeIISSample/Startup.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using Benchmarks.Middleware; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -18,6 +19,7 @@ namespace NativeIISSample // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env, IAuthenticationSchemeProvider authSchemeProvider) { + app.UsePlainText(); app.Run(async (context) => { context.Response.ContentType = "text/plain"; diff --git a/src/IISLib/macros.h b/src/IISLib/macros.h index 960f663a98..6aa5b1a821 100644 --- a/src/IISLib/macros.h +++ b/src/IISLib/macros.h @@ -60,4 +60,11 @@ SAFEIsDigit(UCHAR c) return isdigit( c ); } +#define __RETURN_GLE_FAIL(str) return HRESULT_FROM_WIN32(GetLastError()); +#define __RETURN_HR_FAIL(hr, str) do { HRESULT __hr = (hr); return __hr; } while (0, 0) + +#define RETURN_IF_FAILED(hr) do { HRESULT __hrRet = hr; if (FAILED(__hrRet)) { __RETURN_HR_FAIL(__hrRet, #hr); }} while (0, 0) +#define RETURN_LAST_ERROR_IF_NULL(ptr) do { if ((ptr) == nullptr) { __RETURN_GLE_FAIL(#ptr); }} while (0, 0) +#define RETURN_IF_HANDLE_INVALID(handle) do { HANDLE __hRet = (handle); if (__hRet == INVALID_HANDLE_VALUE) { __RETURN_GLE_FAIL(#handle); }} while (0, 0) + #endif // _MACROS_H diff --git a/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.FeatureCollection.cs b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.FeatureCollection.cs index f43c88a319..0acf621bb0 100644 --- a/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.FeatureCollection.cs +++ b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.FeatureCollection.cs @@ -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; diff --git a/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.IHttpConnectionFeature.cs b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.IHttpConnectionFeature.cs new file mode 100644 index 0000000000..74defed9b5 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.IHttpConnectionFeature.cs @@ -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) + { + InitializeRemoteEndpoint(); + } + 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) + { + InitializeRemoteEndpoint(); + } + + 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); + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.IHttpRequestIdentifierFeature.cs b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.IHttpRequestIdentifierFeature.cs new file mode 100644 index 0000000000..68112f607c --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.IHttpRequestIdentifierFeature.cs @@ -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(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.cs b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.cs index 8ca003ed7f..0b62412c61 100644 --- a/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.cs +++ b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpContext.cs @@ -28,8 +28,6 @@ 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; @@ -60,7 +58,6 @@ 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 memoryPool, IntPtr pInProcessHandler, IISOptions options, IISHttpServer server) : base((HttpApiTypes.HTTP_REQUEST*)NativeMethods.HttpGetRawRequest(pInProcessHandler)) @@ -72,103 +69,64 @@ namespace Microsoft.AspNetCore.Server.IISIntegration _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; + StatusCode = 200; + + var originalPath = RequestUriBuilder.DecodeAndUnescapePath(GetRawUrlInBytes()); + + if (KnownMethod == HttpApiTypes.HTTP_VERB.HttpVerbOPTIONS && string.Equals(RawTarget, "*", StringComparison.Ordinal)) { - Method = GetVerb(); + 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; + } - RawTarget = GetRawUrl(); - // TODO version is slow. - HttpVersion = GetVersion(); - Scheme = SslStatus != SslStatus.Insecure ? Constants.HttpsScheme : Constants.HttpScheme; - KnownMethod = VerbId; + var cookedUrl = GetCookedUrl(); + QueryString = cookedUrl.GetQueryString() ?? string.Empty; - var originalPath = RequestUriBuilder.DecodeAndUnescapePath(GetRawUrlInBytes()); + RequestHeaders = new RequestHeaders(this); + HttpResponseHeaders = new HeaderCollection(); + ResponseHeaders = HttpResponseHeaders; - if (KnownMethod == HttpApiTypes.HTTP_VERB.HttpVerbOPTIONS && string.Equals(RawTarget, "*", StringComparison.Ordinal)) + if (options.ForwardWindowsAuthentication) + { + WindowsUser = GetWindowsPrincipal(); + if (options.AutomaticAuthentication) { - 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; + User = WindowsUser; } + } - var cookedUrl = GetCookedUrl(); - QueryString = cookedUrl.GetQueryString() ?? string.Empty; + ResetFeatureCollection(); - // 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; - } + 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)); + var pipe = new Pipe( + new PipeOptions( + _memoryPool, + readerScheduler: PipeScheduler.ThreadPool, + pauseWriterThreshold: PauseWriterThreshold, + resumeWriterThreshold: ResumeWriterTheshold, + minimumSegmentSize: MinAllocBufferSize)); Output = new OutputProducer(pipe); } @@ -545,12 +503,5 @@ namespace Microsoft.AspNetCore.Server.IISIntegration } return null; } - - private enum WebsocketAvailabilityStatus - { - Uninitialized = 1, - Available, - NotAvailable - } } } diff --git a/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpServer.cs b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpServer.cs index 046d8b9c4f..ecfc361bd5 100644 --- a/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpServer.cs +++ b/src/Microsoft.AspNetCore.Server.IISIntegration/Server/IISHttpServer.cs @@ -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 _shutdownSignal = new TaskCompletionSource(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 options) { _applicationLifetime = applicationLifetime; @@ -122,6 +144,7 @@ namespace Microsoft.AspNetCore.Server.IISIntegration var server = (IISHttpServer)GCHandle.FromIntPtr(pvRequestContext).Target; Interlocked.Increment(ref server._outstandingRequests); + // TODO: Add try/catch and logging here var context = server._iisContextFactory.CreateHttpContext(pInProcessHandler); var task = Task.Run(() => context.ProcessRequestAsync());