From edf25b7817d6b19557c1158e5f87cea4d0acbce2 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Wed, 19 Aug 2020 07:41:48 +1200 Subject: [PATCH] HTTP method matching: Jump table optimized for a single method (#24953) --- .../HttpMethodPolicyJumpTableBenchmark.cs | 54 ++++++++++++ .../HttpMethodDictionaryPolicyJumpTable.cs | 48 +++++++++++ .../src/Matching/HttpMethodMatcherPolicy.cs | 86 +++++++++---------- .../HttpMethodSingleEntryPolicyJumpTable.cs | 45 ++++++++++ ...pMethodMatcherPolicyIntegrationTestBase.cs | 20 +++-- 5 files changed, 198 insertions(+), 55 deletions(-) create mode 100644 src/Http/Routing/perf/Matching/HttpMethodPolicyJumpTableBenchmark.cs create mode 100644 src/Http/Routing/src/Matching/HttpMethodDictionaryPolicyJumpTable.cs create mode 100644 src/Http/Routing/src/Matching/HttpMethodSingleEntryPolicyJumpTable.cs diff --git a/src/Http/Routing/perf/Matching/HttpMethodPolicyJumpTableBenchmark.cs b/src/Http/Routing/perf/Matching/HttpMethodPolicyJumpTableBenchmark.cs new file mode 100644 index 0000000000..012d138e9a --- /dev/null +++ b/src/Http/Routing/perf/Matching/HttpMethodPolicyJumpTableBenchmark.cs @@ -0,0 +1,54 @@ +// 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.Collections.Generic; +using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Matching +{ + public class HttpMethodPolicyJumpTableBenchmark + { + private PolicyJumpTable _dictionaryJumptable; + private PolicyJumpTable _singleEntryJumptable; + private DefaultHttpContext _httpContext; + + [GlobalSetup] + public void Setup() + { + _dictionaryJumptable = new HttpMethodDictionaryPolicyJumpTable( + 0, + new Dictionary + { + [HttpMethods.Get] = 1 + }, + -1, + new Dictionary + { + [HttpMethods.Get] = 2 + }); + _singleEntryJumptable = new HttpMethodSingleEntryPolicyJumpTable( + 0, + HttpMethods.Get, + -1, + supportsCorsPreflight: true, + -1, + 2); + + _httpContext = new DefaultHttpContext(); + _httpContext.Request.Method = HttpMethods.Get; + } + + [Benchmark] + public int DictionaryPolicyJumpTable() + { + return _dictionaryJumptable.GetDestination(_httpContext); + } + + [Benchmark] + public int SingleEntryPolicyJumpTable() + { + return _singleEntryJumptable.GetDestination(_httpContext); + } + } +} diff --git a/src/Http/Routing/src/Matching/HttpMethodDictionaryPolicyJumpTable.cs b/src/Http/Routing/src/Matching/HttpMethodDictionaryPolicyJumpTable.cs new file mode 100644 index 0000000000..f77e65f37e --- /dev/null +++ b/src/Http/Routing/src/Matching/HttpMethodDictionaryPolicyJumpTable.cs @@ -0,0 +1,48 @@ +// 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.Collections.Generic; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Matching +{ + internal sealed class HttpMethodDictionaryPolicyJumpTable : PolicyJumpTable + { + private readonly int _exitDestination; + private readonly Dictionary? _destinations; + private readonly int _corsPreflightExitDestination; + private readonly Dictionary? _corsPreflightDestinations; + + private readonly bool _supportsCorsPreflight; + + public HttpMethodDictionaryPolicyJumpTable( + int exitDestination, + Dictionary? destinations, + int corsPreflightExitDestination, + Dictionary? corsPreflightDestinations) + { + _exitDestination = exitDestination; + _destinations = destinations; + _corsPreflightExitDestination = corsPreflightExitDestination; + _corsPreflightDestinations = corsPreflightDestinations; + + _supportsCorsPreflight = _corsPreflightDestinations != null && _corsPreflightDestinations.Count > 0; + } + + public override int GetDestination(HttpContext httpContext) + { + int destination; + + var httpMethod = httpContext.Request.Method; + if (_supportsCorsPreflight && HttpMethodMatcherPolicy.IsCorsPreflightRequest(httpContext, httpMethod, out var accessControlRequestMethod)) + { + return _corsPreflightDestinations!.TryGetValue(accessControlRequestMethod, out destination) + ? destination + : _corsPreflightExitDestination; + } + + return _destinations != null && + _destinations.TryGetValue(httpMethod, out destination) ? destination : _exitDestination; + } + } +} diff --git a/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs b/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs index 039d889fed..9ffa13fa91 100644 --- a/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs @@ -370,11 +370,38 @@ namespace Microsoft.AspNetCore.Routing.Matching destinations.Remove(AnyMethod); } - return new HttpMethodPolicyJumpTable( - exitDestination, - destinations, - corsPreflightExitDestination, - corsPreflightDestinations); + if (destinations?.Count == 1) + { + // If there is only a single valid HTTP method then use an optimized jump table. + // It avoids unnecessary dictionary lookups with the method name. + var httpMethodDestination = destinations.Single(); + var method = httpMethodDestination.Key; + var destination = httpMethodDestination.Value; + var supportsCorsPreflight = false; + var corsPreflightDestination = 0; + + if (corsPreflightDestinations?.Count > 0) + { + supportsCorsPreflight = true; + corsPreflightDestination = corsPreflightDestinations.Single().Value; + } + + return new HttpMethodSingleEntryPolicyJumpTable( + exitDestination, + method, + destination, + supportsCorsPreflight, + corsPreflightExitDestination, + corsPreflightDestination); + } + else + { + return new HttpMethodDictionaryPolicyJumpTable( + exitDestination, + destinations, + corsPreflightExitDestination, + corsPreflightDestinations); + } } private Endpoint CreateRejectionEndpoint(IEnumerable httpMethods) @@ -418,50 +445,15 @@ namespace Microsoft.AspNetCore.Routing.Matching return false; } - private class HttpMethodPolicyJumpTable : PolicyJumpTable + internal static bool IsCorsPreflightRequest(HttpContext httpContext, string httpMethod, out StringValues accessControlRequestMethod) { - private readonly int _exitDestination; - private readonly Dictionary? _destinations; - private readonly int _corsPreflightExitDestination; - private readonly Dictionary? _corsPreflightDestinations; + accessControlRequestMethod = default; + var headers = httpContext.Request.Headers; - private readonly bool _supportsCorsPreflight; - - public HttpMethodPolicyJumpTable( - int exitDestination, - Dictionary? destinations, - int corsPreflightExitDestination, - Dictionary? corsPreflightDestinations) - { - _exitDestination = exitDestination; - _destinations = destinations; - _corsPreflightExitDestination = corsPreflightExitDestination; - _corsPreflightDestinations = corsPreflightDestinations; - - _supportsCorsPreflight = _corsPreflightDestinations != null && _corsPreflightDestinations.Count > 0; - } - - public override int GetDestination(HttpContext httpContext) - { - int destination; - - var httpMethod = httpContext.Request.Method; - var headers = httpContext.Request.Headers; - if (_supportsCorsPreflight && - HttpMethods.Equals(httpMethod, PreflightHttpMethod) && - headers.ContainsKey(HeaderNames.Origin) && - headers.TryGetValue(HeaderNames.AccessControlRequestMethod, out var accessControlRequestMethod) && - !StringValues.IsNullOrEmpty(accessControlRequestMethod)) - { - return _corsPreflightDestinations != null && - _corsPreflightDestinations.TryGetValue(accessControlRequestMethod, out destination) - ? destination - : _corsPreflightExitDestination; - } - - return _destinations != null && - _destinations.TryGetValue(httpMethod, out destination) ? destination : _exitDestination; - } + return HttpMethods.Equals(httpMethod, PreflightHttpMethod) && + headers.ContainsKey(HeaderNames.Origin) && + headers.TryGetValue(HeaderNames.AccessControlRequestMethod, out accessControlRequestMethod) && + !StringValues.IsNullOrEmpty(accessControlRequestMethod); } private class HttpMethodMetadataEndpointComparer : EndpointMetadataComparer diff --git a/src/Http/Routing/src/Matching/HttpMethodSingleEntryPolicyJumpTable.cs b/src/Http/Routing/src/Matching/HttpMethodSingleEntryPolicyJumpTable.cs new file mode 100644 index 0000000000..a114b373da --- /dev/null +++ b/src/Http/Routing/src/Matching/HttpMethodSingleEntryPolicyJumpTable.cs @@ -0,0 +1,45 @@ +// 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 Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Matching +{ + internal sealed class HttpMethodSingleEntryPolicyJumpTable : PolicyJumpTable + { + private readonly int _exitDestination; + private readonly string _method; + private readonly int _destination; + private readonly int _corsPreflightExitDestination; + private readonly int _corsPreflightDestination; + + private readonly bool _supportsCorsPreflight; + + public HttpMethodSingleEntryPolicyJumpTable( + int exitDestination, + string method, + int destination, + bool supportsCorsPreflight, + int corsPreflightExitDestination, + int corsPreflightDestination) + { + _exitDestination = exitDestination; + _method = method; + _destination = destination; + _supportsCorsPreflight = supportsCorsPreflight; + _corsPreflightExitDestination = corsPreflightExitDestination; + _corsPreflightDestination = corsPreflightDestination; + } + + public override int GetDestination(HttpContext httpContext) + { + var httpMethod = httpContext.Request.Method; + if (_supportsCorsPreflight && HttpMethodMatcherPolicy.IsCorsPreflightRequest(httpContext, httpMethod, out var accessControlRequestMethod)) + { + return HttpMethods.Equals(accessControlRequestMethod, _method) ? _corsPreflightDestination : _corsPreflightExitDestination; + } + + return HttpMethods.Equals(httpMethod, _method) ? _destination : _exitDestination; + } + } +} diff --git a/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyIntegrationTestBase.cs b/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyIntegrationTestBase.cs index 5f219e329a..fbb8cecc69 100644 --- a/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyIntegrationTestBase.cs +++ b/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyIntegrationTestBase.cs @@ -84,14 +84,16 @@ namespace Microsoft.AspNetCore.Routing.Matching Assert.Same(HttpMethodMatcherPolicy.Http405EndpointDisplayName, httpContext.GetEndpoint().DisplayName); } - [Fact] - public async Task Match_HttpMethod_CaseInsensitive() + [Theory] + [InlineData("GeT", "GET")] + [InlineData("unKNOWN", "UNKNOWN")] + public async Task Match_HttpMethod_CaseInsensitive(string endpointMethod, string requestMethod) { // Arrange - var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { "GeT", }); + var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { endpointMethod, }); var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "GET"); + var httpContext = CreateContext("/hello", requestMethod); // Act await matcher.MatchAsync(httpContext); @@ -100,14 +102,16 @@ namespace Microsoft.AspNetCore.Routing.Matching MatcherAssert.AssertMatch(httpContext, endpoint); } - [Fact] - public async Task Match_HttpMethod_CaseInsensitive_CORS_Preflight() + [Theory] + [InlineData("GeT", "GET")] + [InlineData("unKNOWN", "UNKNOWN")] + public async Task Match_HttpMethod_CaseInsensitive_CORS_Preflight(string endpointMethod, string requestMethod) { // Arrange - var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { "GeT", }, acceptCorsPreflight: true); + var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { endpointMethod, }, acceptCorsPreflight: true); var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "GET", corsPreflight: true); + var httpContext = CreateContext("/hello", requestMethod, corsPreflight: true); // Act await matcher.MatchAsync(httpContext);