From d1f3b90a0e38ff22afe7c05357f48e2f87de898c Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sun, 30 Sep 2018 08:54:59 +1300 Subject: [PATCH 1/2] RouteValuesAddressMetadata ctors and XML docs (#818) --- .../EndpointMetadataCollection.cs | 4 +- .../RouteContext.cs | 2 +- .../RouteData.cs | 8 ++-- .../RouteValueDictionary.cs | 2 +- .../VirtualPathContext.cs | 4 +- .../Constraints/HttpMethodRouteConstraint.cs | 2 +- .../EndpointNameMetadata.cs | 2 +- .../IRouteValuesAddressMetadata.cs | 11 ++++++ .../Patterns/RoutePatternException.cs | 2 +- .../Patterns/RoutePatternFactory.cs | 8 ++-- .../RouteConstraintBuilder.cs | 2 +- .../RouteValuesAddressMetadata.cs | 39 +++++++++++++++++++ .../Template/InlineConstraint.cs | 2 +- .../Tree/TreeRouter.cs | 2 +- .../EndpointFactory.cs | 2 +- ...neratorRouteValuesAddressExtensionsTest.cs | 28 ++++++------- .../RouteValuesAddressMetadataTests.cs | 2 +- 17 files changed, 86 insertions(+), 36 deletions(-) diff --git a/src/Microsoft.AspNetCore.Routing.Abstractions/EndpointMetadataCollection.cs b/src/Microsoft.AspNetCore.Routing.Abstractions/EndpointMetadataCollection.cs index a792fff295..137423a886 100644 --- a/src/Microsoft.AspNetCore.Routing.Abstractions/EndpointMetadataCollection.cs +++ b/src/Microsoft.AspNetCore.Routing.Abstractions/EndpointMetadataCollection.cs @@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Http private readonly ConcurrentDictionary _cache; /// - /// Creates a new . + /// Creates a new instance of . /// /// The metadata items. public EndpointMetadataCollection(IEnumerable items) @@ -44,7 +44,7 @@ namespace Microsoft.AspNetCore.Http } /// - /// Creates a new . + /// Creates a new instance of . /// /// The metadata items. public EndpointMetadataCollection(params object[] items) diff --git a/src/Microsoft.AspNetCore.Routing.Abstractions/RouteContext.cs b/src/Microsoft.AspNetCore.Routing.Abstractions/RouteContext.cs index 767f39b1ec..7162446e7d 100644 --- a/src/Microsoft.AspNetCore.Routing.Abstractions/RouteContext.cs +++ b/src/Microsoft.AspNetCore.Routing.Abstractions/RouteContext.cs @@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.Routing private RouteData _routeData; /// - /// Creates a new for the provided . + /// Creates a new instance of for the provided . /// /// The associated with the current request. public RouteContext(HttpContext httpContext) diff --git a/src/Microsoft.AspNetCore.Routing.Abstractions/RouteData.cs b/src/Microsoft.AspNetCore.Routing.Abstractions/RouteData.cs index e0628dc1bb..858c3a67f4 100644 --- a/src/Microsoft.AspNetCore.Routing.Abstractions/RouteData.cs +++ b/src/Microsoft.AspNetCore.Routing.Abstractions/RouteData.cs @@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Routing private RouteValueDictionary _values; /// - /// Creates a new instance. + /// Creates a new instance of instance. /// public RouteData() { @@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Routing } /// - /// Creates a new instance with values copied from . + /// Creates a new instance of instance with values copied from . /// /// The other instance to copy. public RouteData(RouteData other) @@ -52,7 +52,7 @@ namespace Microsoft.AspNetCore.Routing } /// - /// Creates a new instance with the specified values. + /// Creates a new instance of instance with the specified values. /// /// The values. public RouteData(RouteValueDictionary values) @@ -197,7 +197,7 @@ namespace Microsoft.AspNetCore.Routing private readonly RouteValueDictionary _values; /// - /// Creates a new for . + /// Creates a new instance of for . /// /// The . /// The data tokens. diff --git a/src/Microsoft.AspNetCore.Routing.Abstractions/RouteValueDictionary.cs b/src/Microsoft.AspNetCore.Routing.Abstractions/RouteValueDictionary.cs index 837e466d28..ca132a7889 100644 --- a/src/Microsoft.AspNetCore.Routing.Abstractions/RouteValueDictionary.cs +++ b/src/Microsoft.AspNetCore.Routing.Abstractions/RouteValueDictionary.cs @@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Routing private int _count; /// - /// Creates a new from the provided array. + /// Creates a new instance of from the provided array. /// The new instance will take ownership of the array, and may mutate it. /// /// The items array. diff --git a/src/Microsoft.AspNetCore.Routing.Abstractions/VirtualPathContext.cs b/src/Microsoft.AspNetCore.Routing.Abstractions/VirtualPathContext.cs index 036aa445f8..88f899e925 100644 --- a/src/Microsoft.AspNetCore.Routing.Abstractions/VirtualPathContext.cs +++ b/src/Microsoft.AspNetCore.Routing.Abstractions/VirtualPathContext.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Routing public class VirtualPathContext { /// - /// Creates a new . + /// Creates a new instance of . /// /// The associated with the current request. /// The set of route values associated with the current request. @@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Routing } /// - /// Creates a new . + /// Creates a new instance of . /// /// The associated with the current request. /// The set of route values associated with the current request. diff --git a/src/Microsoft.AspNetCore.Routing/Constraints/HttpMethodRouteConstraint.cs b/src/Microsoft.AspNetCore.Routing/Constraints/HttpMethodRouteConstraint.cs index a894acaacb..f5a4d81830 100644 --- a/src/Microsoft.AspNetCore.Routing/Constraints/HttpMethodRouteConstraint.cs +++ b/src/Microsoft.AspNetCore.Routing/Constraints/HttpMethodRouteConstraint.cs @@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.Routing.Constraints public class HttpMethodRouteConstraint : IRouteConstraint { /// - /// Creates a new that accepts the HTTP methods specified + /// Creates a new instance of that accepts the HTTP methods specified /// by . /// /// The allowed HTTP methods. diff --git a/src/Microsoft.AspNetCore.Routing/EndpointNameMetadata.cs b/src/Microsoft.AspNetCore.Routing/EndpointNameMetadata.cs index 925c355807..1342962797 100644 --- a/src/Microsoft.AspNetCore.Routing/EndpointNameMetadata.cs +++ b/src/Microsoft.AspNetCore.Routing/EndpointNameMetadata.cs @@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Routing public class EndpointNameMetadata : IEndpointNameMetadata { /// - /// Creates a new with the provided endpoint name. + /// Creates a new instance of with the provided endpoint name. /// /// The endpoint name. public EndpointNameMetadata(string endpointName) diff --git a/src/Microsoft.AspNetCore.Routing/IRouteValuesAddressMetadata.cs b/src/Microsoft.AspNetCore.Routing/IRouteValuesAddressMetadata.cs index 1e771c27d8..076dea8a92 100644 --- a/src/Microsoft.AspNetCore.Routing/IRouteValuesAddressMetadata.cs +++ b/src/Microsoft.AspNetCore.Routing/IRouteValuesAddressMetadata.cs @@ -5,9 +5,20 @@ using System.Collections.Generic; namespace Microsoft.AspNetCore.Routing { + /// + /// Represents metadata used during link generation to find + /// the associated endpoint using route values. + /// public interface IRouteValuesAddressMetadata { + /// + /// Gets the route name. Can be null. + /// string RouteName { get; } + + /// + /// Gets the required route values. + /// IReadOnlyDictionary RequiredValues { get; } } } diff --git a/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternException.cs b/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternException.cs index 04dc26daf4..7b21ac0ac5 100644 --- a/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternException.cs +++ b/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternException.cs @@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns } /// - /// Creates a new . + /// Creates a new instance of . /// /// The route pattern as raw text. /// The exception message. diff --git a/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternFactory.cs b/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternFactory.cs index bbc1e50aca..71941ffde6 100644 --- a/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternFactory.cs +++ b/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternFactory.cs @@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns } /// - /// Creates a new from a collection of segments. + /// Creates a new instance of from a collection of segments. /// /// The collection of segments. /// The . @@ -82,7 +82,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns } /// - /// Creates a new from a collection of segments. + /// Creates a new instance of from a collection of segments. /// /// The raw text to associate with the route pattern. May be null. /// The collection of segments. @@ -158,7 +158,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns } /// - /// Creates a new from a collection of segments. + /// Creates a new instance of from a collection of segments. /// /// The collection of segments. /// The . @@ -173,7 +173,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns } /// - /// Creates a new from a collection of segments. + /// Creates a new instance of from a collection of segments. /// /// The raw text to associate with the route pattern. May be null. /// The collection of segments. diff --git a/src/Microsoft.AspNetCore.Routing/RouteConstraintBuilder.cs b/src/Microsoft.AspNetCore.Routing/RouteConstraintBuilder.cs index f170ac81fa..0c444236a5 100644 --- a/src/Microsoft.AspNetCore.Routing/RouteConstraintBuilder.cs +++ b/src/Microsoft.AspNetCore.Routing/RouteConstraintBuilder.cs @@ -23,7 +23,7 @@ namespace Microsoft.AspNetCore.Routing private readonly Dictionary> _constraints; private readonly HashSet _optionalParameters; /// - /// Creates a new instance. + /// Creates a new instance of instance. /// /// The . /// The display name (for use in error messages). diff --git a/src/Microsoft.AspNetCore.Routing/RouteValuesAddressMetadata.cs b/src/Microsoft.AspNetCore.Routing/RouteValuesAddressMetadata.cs index a65c48101a..c901d185ee 100644 --- a/src/Microsoft.AspNetCore.Routing/RouteValuesAddressMetadata.cs +++ b/src/Microsoft.AspNetCore.Routing/RouteValuesAddressMetadata.cs @@ -3,22 +3,61 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; namespace Microsoft.AspNetCore.Routing { + /// + /// Metadata used during link generation to find the associated endpoint using route values. + /// [DebuggerDisplay("{DebuggerToString(),nq}")] public sealed class RouteValuesAddressMetadata : IRouteValuesAddressMetadata { + private static readonly IReadOnlyDictionary EmptyRouteValues = + new ReadOnlyDictionary(new Dictionary()); + + /// + /// Creates a new instance of with the provided route name. + /// + /// The route name. Can be null. + public RouteValuesAddressMetadata(string routeName) : this(routeName, EmptyRouteValues) + { + } + + /// + /// Creates a new instance of with the provided required route values. + /// + /// The required route values. + public RouteValuesAddressMetadata(IReadOnlyDictionary requiredValues) : this(null, requiredValues) + { + } + + /// + /// Creates a new instance of with the provided route name and required route values. + /// + /// The route name. Can be null. + /// The required route values. public RouteValuesAddressMetadata(string routeName, IReadOnlyDictionary requiredValues) { + if (requiredValues == null) + { + throw new ArgumentNullException(nameof(requiredValues)); + } + RouteName = routeName; RequiredValues = requiredValues; } + /// + /// Gets the route name. Can be null. + /// public string RouteName { get; } + /// + /// Gets the required route values. + /// public IReadOnlyDictionary RequiredValues { get; } internal string DebuggerToString() diff --git a/src/Microsoft.AspNetCore.Routing/Template/InlineConstraint.cs b/src/Microsoft.AspNetCore.Routing/Template/InlineConstraint.cs index 4d58ce6181..a711ecb136 100644 --- a/src/Microsoft.AspNetCore.Routing/Template/InlineConstraint.cs +++ b/src/Microsoft.AspNetCore.Routing/Template/InlineConstraint.cs @@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Routing.Template public class InlineConstraint { /// - /// Creates a new . + /// Creates a new instance of . /// /// The constraint text. public InlineConstraint(string constraint) diff --git a/src/Microsoft.AspNetCore.Routing/Tree/TreeRouter.cs b/src/Microsoft.AspNetCore.Routing/Tree/TreeRouter.cs index 679cbf5261..8670b6cdff 100644 --- a/src/Microsoft.AspNetCore.Routing/Tree/TreeRouter.cs +++ b/src/Microsoft.AspNetCore.Routing/Tree/TreeRouter.cs @@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Routing.Tree private readonly ILogger _constraintLogger; /// - /// Creates a new . + /// Creates a new instance of . /// /// The list of that contains the route entries. /// The set of . diff --git a/test/Microsoft.AspNetCore.Routing.Tests/EndpointFactory.cs b/test/Microsoft.AspNetCore.Routing.Tests/EndpointFactory.cs index eb23c5daab..e3a5b0f802 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/EndpointFactory.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/EndpointFactory.cs @@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.Routing var d = new List(metadata ?? Array.Empty()); if (requiredValues != null) { - d.Add(new RouteValuesAddressMetadata(null, new RouteValueDictionary(requiredValues))); + d.Add(new RouteValuesAddressMetadata(new RouteValueDictionary(requiredValues))); } return new RouteEndpoint( diff --git a/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorRouteValuesAddressExtensionsTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorRouteValuesAddressExtensionsTest.cs index 57f530cdb8..3c3e63c78e 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorRouteValuesAddressExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/LinkGeneratorRouteValuesAddressExtensionsTest.cs @@ -24,11 +24,11 @@ namespace Microsoft.AspNetCore.Routing var endpoint1 = EndpointFactory.CreateRouteEndpoint( "Home/Index/{id}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); var endpoint2 = EndpointFactory.CreateRouteEndpoint( "Home/Index/{id?}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); @@ -59,11 +59,11 @@ namespace Microsoft.AspNetCore.Routing var endpoint1 = EndpointFactory.CreateRouteEndpoint( "Home/Index/{id}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); var endpoint2 = EndpointFactory.CreateRouteEndpoint( "Home/Index/{id?}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); @@ -86,11 +86,11 @@ namespace Microsoft.AspNetCore.Routing var endpoint1 = EndpointFactory.CreateRouteEndpoint( "Home/Index/{id}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); var endpoint2 = EndpointFactory.CreateRouteEndpoint( "Home/Index/{id?}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); @@ -116,11 +116,11 @@ namespace Microsoft.AspNetCore.Routing var endpoint1 = EndpointFactory.CreateRouteEndpoint( "Home/Index/{id}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); var endpoint2 = EndpointFactory.CreateRouteEndpoint( "Home/Index/{id?}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); @@ -145,11 +145,11 @@ namespace Microsoft.AspNetCore.Routing var endpoint1 = EndpointFactory.CreateRouteEndpoint( "Home/Index/{id}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); var endpoint2 = EndpointFactory.CreateRouteEndpoint( "Home/Index/{id?}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); @@ -177,11 +177,11 @@ namespace Microsoft.AspNetCore.Routing var endpoint1 = EndpointFactory.CreateRouteEndpoint( "Home/Index/{id}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); var endpoint2 = EndpointFactory.CreateRouteEndpoint( "Home/Index/{id?}", defaults: new { controller = "Home", action = "Index", }, - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); + metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "Index", })) }); var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); @@ -208,10 +208,10 @@ namespace Microsoft.AspNetCore.Routing // Arrange var endpoint1 = EndpointFactory.CreateRouteEndpoint( "{controller}/{action}/{id}", - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "In?dex", })) }); + metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "In?dex", })) }); var endpoint2 = EndpointFactory.CreateRouteEndpoint( "{controller}/{action}/{id?}", - metadata: new[] { new RouteValuesAddressMetadata(routeName: null, new RouteValueDictionary(new { controller = "Home", action = "In?dex", })) }); + metadata: new[] { new RouteValuesAddressMetadata(new RouteValueDictionary(new { controller = "Home", action = "In?dex", })) }); var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); diff --git a/test/Microsoft.AspNetCore.Routing.Tests/RouteValuesAddressMetadataTests.cs b/test/Microsoft.AspNetCore.Routing.Tests/RouteValuesAddressMetadataTests.cs index f05e763484..1a237c2ad1 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/RouteValuesAddressMetadataTests.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/RouteValuesAddressMetadataTests.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Routing [Fact] public void DebuggerToString_NoNameAndRequiredValues_ReturnsString() { - var metadata = new RouteValuesAddressMetadata(null, null); + var metadata = new RouteValuesAddressMetadata(null, new Dictionary()); Assert.Equal("Name: - Required values: ", metadata.DebuggerToString()); } From 8b99832eaf353604d420469a31c9c2bc96364630 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Thu, 16 Aug 2018 16:41:09 -0700 Subject: [PATCH 2/2] Add ASCII optimized jump tables --- .../Matching/JumpTableSingleEntryBenchmark.cs | 196 +----------------- .../Matching/Ascii.cs | 75 +++++++ .../Matching/JumpTableBuilder.cs | 10 +- .../Matching/SingleEntryAsciiJumpTable.cs | 55 +++++ .../Matching/AsciiTest.cs | 114 ++++++++++ .../Matching/SingleEntryAsciiJumpTableTest.cs | 13 ++ .../Matching/SingleEntryJumpTableTest.cs | 55 +---- .../Matching/SingleEntryJumpTableTestBase.cs | 68 ++++++ 8 files changed, 347 insertions(+), 239 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Routing/Matching/Ascii.cs create mode 100644 src/Microsoft.AspNetCore.Routing/Matching/SingleEntryAsciiJumpTable.cs create mode 100644 test/Microsoft.AspNetCore.Routing.Tests/Matching/AsciiTest.cs create mode 100644 test/Microsoft.AspNetCore.Routing.Tests/Matching/SingleEntryAsciiJumpTableTest.cs create mode 100644 test/Microsoft.AspNetCore.Routing.Tests/Matching/SingleEntryJumpTableTestBase.cs diff --git a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matching/JumpTableSingleEntryBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matching/JumpTableSingleEntryBenchmark.cs index 1f676ce5ad..99f774620c 100644 --- a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matching/JumpTableSingleEntryBenchmark.cs +++ b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matching/JumpTableSingleEntryBenchmark.cs @@ -10,10 +10,10 @@ namespace Microsoft.AspNetCore.Routing.Matching { public class JumpTableSingleEntryBenchmark { - private JumpTable _implementation; - private JumpTable _prototype; + private JumpTable _default; private JumpTable _trie; private JumpTable _vectorTrie; + private JumpTable _ascii; private string[] _strings; private PathSegment[] _segments; @@ -21,10 +21,10 @@ namespace Microsoft.AspNetCore.Routing.Matching [GlobalSetup] public void Setup() { - _implementation = new SingleEntryJumpTable(0, -1, "hello-world", 1); - _prototype = new SingleEntryAsciiVectorizedJumpTable(0, -2, "hello-world", 1); - _trie = new ILEmitTrieJumpTable(0, -1, new[] { ("hello-world", 1), }, vectorize: false, _implementation); - _vectorTrie = new ILEmitTrieJumpTable(0, -1, new[] { ("hello-world", 1), }, vectorize: true, _implementation); + _default = new SingleEntryJumpTable(0, -1, "hello-world", 1); + _trie = new ILEmitTrieJumpTable(0, -1, new[] { ("hello-world", 1), }, vectorize: false, _default); + _vectorTrie = new ILEmitTrieJumpTable(0, -1, new[] { ("hello-world", 1), }, vectorize: true, _default); + _ascii = new SingleEntryAsciiJumpTable(0, -1, "hello-world", 1); _strings = new string[] { @@ -80,7 +80,7 @@ namespace Microsoft.AspNetCore.Routing.Matching } [Benchmark(OperationsPerInvoke = 5)] - public int Implementation() + public int Default() { var strings = _strings; var segments = _segments; @@ -88,14 +88,14 @@ namespace Microsoft.AspNetCore.Routing.Matching var destination = 0; for (var i = 0; i < strings.Length; i++) { - destination = _implementation.GetDestination(strings[i], segments[i]); + destination = _default.GetDestination(strings[i], segments[i]); } return destination; } [Benchmark(OperationsPerInvoke = 5)] - public int Prototype() + public int Ascii() { var strings = _strings; var segments = _segments; @@ -103,7 +103,7 @@ namespace Microsoft.AspNetCore.Routing.Matching var destination = 0; for (var i = 0; i < strings.Length; i++) { - destination = _prototype.GetDestination(strings[i], segments[i]); + destination = _ascii.GetDestination(strings[i], segments[i]); } return destination; @@ -138,181 +138,5 @@ namespace Microsoft.AspNetCore.Routing.Matching return destination; } - - private class SingleEntryAsciiVectorizedJumpTable : JumpTable - { - private readonly int _defaultDestination; - private readonly int _exitDestination; - private readonly string _text; - private readonly int _destination; - - private readonly ulong[] _values; - private readonly int _residue0Lower; - private readonly int _residue0Upper; - private readonly int _residue1Lower; - private readonly int _residue1Upper; - private readonly int _residue2Lower; - private readonly int _residue2Upper; - - public SingleEntryAsciiVectorizedJumpTable( - int defaultDestination, - int exitDestination, - string text, - int destination) - { - _defaultDestination = defaultDestination; - _exitDestination = exitDestination; - _text = text; - _destination = destination; - - var length = text.Length; - var span = text.ToLowerInvariant().AsSpan(); - ref var p = ref Unsafe.As(ref MemoryMarshal.GetReference(span)); - - _values = new ulong[length / 4]; - for (var i = 0; i < length / 4; i++) - { - _values[i] = Unsafe.ReadUnaligned(ref p); - p = Unsafe.Add(ref p, 64); - } - switch (length % 4) - { - case 1: - { - var c = Unsafe.ReadUnaligned(ref p); - _residue0Lower = char.ToLowerInvariant(c); - _residue0Upper = char.ToUpperInvariant(c); - - break; - } - - case 2: - { - var c = Unsafe.ReadUnaligned(ref p); - _residue0Lower = char.ToLowerInvariant(c); - _residue0Upper = char.ToUpperInvariant(c); - - p = Unsafe.Add(ref p, 2); - c = Unsafe.ReadUnaligned(ref p); - _residue1Lower = char.ToLowerInvariant(c); - _residue1Upper = char.ToUpperInvariant(c); - - break; - } - - case 3: - { - var c = Unsafe.ReadUnaligned(ref p); - _residue0Lower = char.ToLowerInvariant(c); - _residue0Upper = char.ToUpperInvariant(c); - - p = Unsafe.Add(ref p, 2); - c = Unsafe.ReadUnaligned(ref p); - _residue1Lower = char.ToLowerInvariant(c); - _residue1Upper = char.ToUpperInvariant(c); - - p = Unsafe.Add(ref p, 2); - c = Unsafe.ReadUnaligned(ref p); - _residue2Lower = char.ToLowerInvariant(c); - _residue2Upper = char.ToUpperInvariant(c); - - break; - } - } - } - - public override int GetDestination(string path, PathSegment segment) - { - var length = segment.Length; - var span = path.AsSpan(segment.Start, length); - ref var p = ref Unsafe.As(ref MemoryMarshal.GetReference(span)); - - var i = 0; - while (length > 3) - { - var value = Unsafe.ReadUnaligned(ref p); - - if ((value & ~0x007F007F007F007FUL) == 0) - { - return _defaultDestination; - } - - var ulongLowerIndicator = value + (0x0080008000800080UL - 0x0041004100410041UL); - var ulongUpperIndicator = value + (0x0080008000800080UL - 0x005B005B005B005BUL); - var ulongCombinedIndicator = (ulongLowerIndicator ^ ulongUpperIndicator) & 0x0080008000800080UL; - var mask = (ulongCombinedIndicator) >> 2; - - value ^= mask; - - if (value != _values[i]) - { - return _defaultDestination; - } - - i++; - length -= 4; - p = ref Unsafe.Add(ref p, 64); - } - - switch (length) - { - case 1: - { - var c = Unsafe.ReadUnaligned(ref p); - if (c != _residue0Lower && c != _residue0Upper) - { - return _defaultDestination; - } - - break; - } - - case 2: - { - var c = Unsafe.ReadUnaligned(ref p); - if (c != _residue0Lower && c != _residue0Upper) - { - return _defaultDestination; - } - - p = ref Unsafe.Add(ref p, 2); - c = Unsafe.ReadUnaligned(ref p); - if (c != _residue1Lower && c != _residue1Upper) - { - return _defaultDestination; - } - - break; - } - - case 3: - { - var c = Unsafe.ReadUnaligned(ref p); - if (c != _residue0Lower && c != _residue0Upper) - { - return _defaultDestination; - } - - p = ref Unsafe.Add(ref p, 2); - c = Unsafe.ReadUnaligned(ref p); - if (c != _residue1Lower && c != _residue1Upper) - { - return _defaultDestination; - } - - p = ref Unsafe.Add(ref p, 2); - c = Unsafe.ReadUnaligned(ref p); - if (c != _residue2Lower && c != _residue2Upper) - { - return _defaultDestination; - } - - break; - } - } - - return _destination; - } - } } } diff --git a/src/Microsoft.AspNetCore.Routing/Matching/Ascii.cs b/src/Microsoft.AspNetCore.Routing/Matching/Ascii.cs new file mode 100644 index 0000000000..6ff7db28ff --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Matching/Ascii.cs @@ -0,0 +1,75 @@ +// 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.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Microsoft.AspNetCore.Routing.Matching +{ + internal static class Ascii + { + // case-sensitive equality comparison when we KNOW that 'a' is in the ASCII range + // and we know that the spans are the same length. + // + // Similar to https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/Globalization/CompareInfo.cs#L549 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool AsciiIgnoreCaseEquals(ReadOnlySpan a, ReadOnlySpan b, int length) + { + // The caller should have checked the length. We enforce that here by THROWING if the + // lengths are unequal. + if (a.Length < length || b.Length < length) + { + // This should never happen, but we don't want to have undefined + // behavior if it does. + ThrowArgumentExceptionForLength(); + } + + ref var charA = ref MemoryMarshal.GetReference(a); + ref var charB = ref MemoryMarshal.GetReference(b); + + // Iterates each span for the provided length and compares each character + // case-insensitively. This looks funky because we're using unsafe operations + // to elide bounds-checks. + while (length > 0 && AsciiIgnoreCaseEquals(charA, charB)) + { + charA = ref Unsafe.Add(ref charA, 1); + charB = ref Unsafe.Add(ref charB, 1); + length--; + } + + return length == 0; + } + + // case-insensitive equality comparison for characters in the ASCII range + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool AsciiIgnoreCaseEquals(char charA, char charB) + { + const uint AsciiToLower = 0x20; + return + // Equal when chars are exactly equal + charA == charB || + + // Equal when converted to-lower AND they are letters + ((charA | AsciiToLower) == (charB | AsciiToLower) && (uint)((charA | AsciiToLower) - 'a') <= (uint)('z' - 'a')); + } + + public static bool IsAscii(string text) + { + for (var i = 0; i < text.Length; i++) + { + if (text[i] > (char)0x7F) + { + return false; + } + } + + return true; + } + + private static void ThrowArgumentExceptionForLength() + { + throw new ArgumentException("length"); + } + } +} diff --git a/src/Microsoft.AspNetCore.Routing/Matching/JumpTableBuilder.cs b/src/Microsoft.AspNetCore.Routing/Matching/JumpTableBuilder.cs index 69939ffeeb..efee38f0f1 100644 --- a/src/Microsoft.AspNetCore.Routing/Matching/JumpTableBuilder.cs +++ b/src/Microsoft.AspNetCore.Routing/Matching/JumpTableBuilder.cs @@ -39,7 +39,15 @@ namespace Microsoft.AspNetCore.Routing.Matching return new ZeroEntryJumpTable(defaultDestination, exitDestination); } - // The IL Emit jump table is not faster for a single entry + // The IL Emit jump table is not faster for a single entry - but we have an optimized version when all text + // is ASCII + if (pathEntries.Length == 1 && Ascii.IsAscii(pathEntries[0].text)) + { + var entry = pathEntries[0]; + return new SingleEntryAsciiJumpTable(defaultDestination, exitDestination, entry.text, entry.destination); + } + + // We have a fallback that works for non-ASCII if (pathEntries.Length == 1) { var entry = pathEntries[0]; diff --git a/src/Microsoft.AspNetCore.Routing/Matching/SingleEntryAsciiJumpTable.cs b/src/Microsoft.AspNetCore.Routing/Matching/SingleEntryAsciiJumpTable.cs new file mode 100644 index 0000000000..5ca0bab566 --- /dev/null +++ b/src/Microsoft.AspNetCore.Routing/Matching/SingleEntryAsciiJumpTable.cs @@ -0,0 +1,55 @@ +// 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.Runtime.CompilerServices; + +namespace Microsoft.AspNetCore.Routing.Matching +{ + // Optimized implementation for cases where we know that we're + // comparing to ASCII. + internal class SingleEntryAsciiJumpTable : JumpTable + { + private readonly int _defaultDestination; + private readonly int _exitDestination; + private readonly string _text; + private readonly int _destination; + + public SingleEntryAsciiJumpTable( + int defaultDestination, + int exitDestination, + string text, + int destination) + { + _defaultDestination = defaultDestination; + _exitDestination = exitDestination; + _text = text; + _destination = destination; + } + + public unsafe override int GetDestination(string path, PathSegment segment) + { + var length = segment.Length; + if (length == 0) + { + return _exitDestination; + } + + var text = _text; + if (length != text.Length) + { + return _defaultDestination; + } + + var a = path.AsSpan(segment.Start, length); + var b = text.AsSpan(); + + return Ascii.AsciiIgnoreCaseEquals(a, b, length) ? _destination : _defaultDestination; + } + + public override string DebuggerToString() + { + return $"{{ {_text}: {_destination}, $+: {_defaultDestination}, $0: {_exitDestination} }}"; + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matching/AsciiTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matching/AsciiTest.cs new file mode 100644 index 0000000000..c6266183a8 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matching/AsciiTest.cs @@ -0,0 +1,114 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Routing.Matching +{ + // Note that while we don't intend for this code to be used with non-ASCII test, + // we still call into these methods with some non-ASCII characters so that + // we are sure of how it behaves. + public class AsciiTest + { + [Fact] + public void IsAscii_ReturnsTrueForAscii() + { + // Arrange + var text = "abcd\u007F"; + + // Act + var result = Ascii.IsAscii(text); + + // Assert + Assert.True(result); + } + + [Fact] + public void IsAscii_ReturnsFalseForNonAscii() + { + // Arrange + var text = "abcd\u0080"; + + // Act + var result = Ascii.IsAscii(text); + + // Assert + Assert.False(result); + } + + [Theory] + + // Identity + [InlineData('c', 'c')] + [InlineData('C', 'C')] + [InlineData('#', '#')] + [InlineData('\u0080', '\u0080')] + + // Case-insensitive + [InlineData('c', 'C')] + public void AsciiIgnoreCaseEquals_ReturnsTrue(char x, char y) + { + // Arrange + + // Act + var result = Ascii.AsciiIgnoreCaseEquals(x, y); + + // Assert + Assert.True(result); + } + + [Theory] + + // Different letter + [InlineData('c', 'd')] + [InlineData('C', 'D')] + + // Non-letter + casing difference - 'a' and 'A' are 32 bits apart and so are ' ' and '@' + [InlineData(' ', '@')] + [InlineData('\u0080', '\u0080' + 32)] // Outside of ASCII range + public void AsciiIgnoreCaseEquals_ReturnsFalse(char x, char y) + { + // Arrange + + // Act + var result = Ascii.AsciiIgnoreCaseEquals(x, y); + + // Assert + Assert.False(result); + } + + [Theory] + [InlineData("", "", 0)] + [InlineData("abCD", "abcF", 3)] + [InlineData("ab#\u0080-$%", "Ab#\u0080-$%", 7)] + public void UnsafeAsciiIgnoreCaseEquals_ReturnsTrue(string x, string y, int length) + { + // Arrange + var spanX = x.AsSpan(); + var spanY = y.AsSpan(); + + // Act + var result = Ascii.AsciiIgnoreCaseEquals(spanX, spanY, length); + + // Assert + Assert.True(result); + } + + [Theory] + [InlineData("abcD", "abCE", 4)] + [InlineData("ab#\u0080-$%", "Ab#\u0081-$%", 7)] + public void UnsafeAsciiIgnoreCaseEquals_ReturnsFalse(string x, string y, int length) + { + // Arrange + var spanX = x.AsSpan(); + var spanY = y.AsSpan(); + + // Act + var result = Ascii.AsciiIgnoreCaseEquals(spanX, spanY, length); + + // Assert + Assert.False(result); + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matching/SingleEntryAsciiJumpTableTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matching/SingleEntryAsciiJumpTableTest.cs new file mode 100644 index 0000000000..bd068178e9 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matching/SingleEntryAsciiJumpTableTest.cs @@ -0,0 +1,13 @@ +// 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. + +namespace Microsoft.AspNetCore.Routing.Matching +{ + public class SingleEntryAsciiJumpTableTest : SingleEntryJumpTableTestBase + { + private protected override JumpTable CreateJumpTable(int defaultDestination, int exitDestination, string text, int destination) + { + return new SingleEntryAsciiJumpTable(defaultDestination, exitDestination, text, destination); + } + } +} diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matching/SingleEntryJumpTableTest.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matching/SingleEntryJumpTableTest.cs index e21e8918cd..77c3366f70 100644 --- a/test/Microsoft.AspNetCore.Routing.Tests/Matching/SingleEntryJumpTableTest.cs +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matching/SingleEntryJumpTableTest.cs @@ -1,62 +1,13 @@ // 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 Xunit; - namespace Microsoft.AspNetCore.Routing.Matching { - public class SingleEntryJumpTableTest + public class SingleEntryJumpTableTest : SingleEntryJumpTableTestBase { - [Fact] - public void GetDestination_ZeroLengthSegment_JumpsToExit() + private protected override JumpTable CreateJumpTable(int defaultDestination, int exitDestination, string text, int destination) { - // Arrange - var table = new SingleEntryJumpTable(0, 1, "text", 2); - - // Act - var result = table.GetDestination("ignored", new PathSegment(0, 0)); - - // Assert - Assert.Equal(1, result); - } - - [Fact] - public void GetDestination_NonMatchingSegment_JumpsToDefault() - { - // Arrange - var table = new SingleEntryJumpTable(0, 1, "text", 2); - - // Act - var result = table.GetDestination("text", new PathSegment(1, 2)); - - // Assert - Assert.Equal(0, result); - } - - [Fact] - public void GetDestination_SegmentMatchingText_JumpsToDestination() - { - // Arrange - var table = new SingleEntryJumpTable(0, 1, "text", 2); - - // Act - var result = table.GetDestination("some-text", new PathSegment(5, 4)); - - // Assert - Assert.Equal(2, result); - } - - [Fact] - public void GetDestination_SegmentMatchingTextIgnoreCase_JumpsToDestination() - { - // Arrange - var table = new SingleEntryJumpTable(0, 1, "text", 2); - - // Act - var result = table.GetDestination("some-tExt", new PathSegment(5, 4)); - - // Assert - Assert.Equal(2, result); + return new SingleEntryJumpTable(defaultDestination, exitDestination, text, destination); } } } diff --git a/test/Microsoft.AspNetCore.Routing.Tests/Matching/SingleEntryJumpTableTestBase.cs b/test/Microsoft.AspNetCore.Routing.Tests/Matching/SingleEntryJumpTableTestBase.cs new file mode 100644 index 0000000000..dd3c5953a6 --- /dev/null +++ b/test/Microsoft.AspNetCore.Routing.Tests/Matching/SingleEntryJumpTableTestBase.cs @@ -0,0 +1,68 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Routing.Matching +{ + public abstract class SingleEntryJumpTableTestBase + { + private protected abstract JumpTable CreateJumpTable( + int defaultDestination, + int exitDestination, + string text, + int destination); + + [Fact] + public void GetDestination_ZeroLengthSegment_JumpsToExit() + { + // Arrange + var table = CreateJumpTable(0, 1, "text", 2); + + // Act + var result = table.GetDestination("ignored", new PathSegment(0, 0)); + + // Assert + Assert.Equal(1, result); + } + + [Fact] + public void GetDestination_NonMatchingSegment_JumpsToDefault() + { + // Arrange + var table = CreateJumpTable(0, 1, "text", 2); + + // Act + var result = table.GetDestination("text", new PathSegment(1, 2)); + + // Assert + Assert.Equal(0, result); + } + + [Fact] + public void GetDestination_SegmentMatchingText_JumpsToDestination() + { + // Arrange + var table = CreateJumpTable(0, 1, "text", 2); + + // Act + var result = table.GetDestination("some-text", new PathSegment(5, 4)); + + // Assert + Assert.Equal(2, result); + } + + [Fact] + public void GetDestination_SegmentMatchingTextIgnoreCase_JumpsToDestination() + { + // Arrange + var table = CreateJumpTable(0, 1, "text", 2); + + // Act + var result = table.GetDestination("some-tExt", new PathSegment(5, 4)); + + // Assert + Assert.Equal(2, result); + } + } +}