diff --git a/src/Microsoft.AspNetCore.Routing/Matching/DfaMatcherBuilder.cs b/src/Microsoft.AspNetCore.Routing/Matching/DfaMatcherBuilder.cs index 5e06a36a5c..3592918a9a 100644 --- a/src/Microsoft.AspNetCore.Routing/Matching/DfaMatcherBuilder.cs +++ b/src/Microsoft.AspNetCore.Routing/Matching/DfaMatcherBuilder.cs @@ -64,7 +64,7 @@ namespace Microsoft.AspNetCore.Routing.Matching // Since we're doing a BFS we will process each 'level' of the tree in stages // this list will hold the set of items we need to process at the current // stage. - var work = new List<(RouteEndpoint endpoint, List parents)>(); + var work = new List<(RouteEndpoint endpoint, List parents)>(_endpoints.Count); List<(RouteEndpoint endpoint, List parents)> previousWork = null; var root = new DfaNode() { PathDepth = 0, Label = "/" }; @@ -290,38 +290,13 @@ namespace Microsoft.AspNetCore.Routing.Matching maxSegmentCount++; var states = new DfaState[stateCount]; - var tableBuilders = new (JumpTableBuilder pathBuilder, PolicyJumpTableBuilder policyBuilder)[stateCount]; - AddNode(root, states, tableBuilders); + var exitDestination = stateCount - 1; + AddNode(root, states, exitDestination); - var exit = stateCount - 1; - states[exit] = new DfaState(Array.Empty(), null, null); - tableBuilders[exit] = (new JumpTableBuilder() { DefaultDestination = exit, ExitDestination = exit, }, null); - - for (var i = 0; i < tableBuilders.Length; i++) - { - if (tableBuilders[i].pathBuilder?.DefaultDestination == JumpTableBuilder.InvalidDestination) - { - tableBuilders[i].pathBuilder.DefaultDestination = exit; - } - - if (tableBuilders[i].pathBuilder?.ExitDestination == JumpTableBuilder.InvalidDestination) - { - tableBuilders[i].pathBuilder.ExitDestination = exit; - } - - if (tableBuilders[i].policyBuilder?.ExitDestination == JumpTableBuilder.InvalidDestination) - { - tableBuilders[i].policyBuilder.ExitDestination = exit; - } - } - - for (var i = 0; i < states.Length; i++) - { - states[i] = new DfaState( - states[i].Candidates, - tableBuilders[i].pathBuilder?.Build(), - tableBuilders[i].policyBuilder?.Build()); - } + states[exitDestination] = new DfaState( + Array.Empty(), + JumpTableBuilder.Build(exitDestination, exitDestination, null), + null); return new DfaMatcher(_selector, states, maxSegmentCount); } @@ -329,29 +304,26 @@ namespace Microsoft.AspNetCore.Routing.Matching private int AddNode( DfaNode node, DfaState[] states, - (JumpTableBuilder pathBuilder, PolicyJumpTableBuilder policyBuilder)[] tableBuilders) + int exitDestination) { node.Matches?.Sort(_comparer); var currentStateIndex = _stateIndex; - var candidates = CreateCandidates(node.Matches); - states[currentStateIndex] = new DfaState(candidates, null, null); - - var pathBuilder = new JumpTableBuilder(); - tableBuilders[currentStateIndex] = (pathBuilder, null); + var currentDefaultDestination = exitDestination; + var currentExitDestination = exitDestination; + (string text, int destination)[] pathEntries = null; + PolicyJumpTableEdge[] policyEntries = null; if (node.Literals != null) { + pathEntries = new (string text, int destination)[node.Literals.Count]; + + var index = 0; foreach (var kvp in node.Literals) { - if (kvp.Key == null) - { - continue; - } - var transition = Transition(kvp.Value); - pathBuilder.AddEntry(kvp.Key, transition); + pathEntries[index++] = (kvp.Key, transition); } } @@ -361,39 +333,43 @@ namespace Microsoft.AspNetCore.Routing.Matching { // This node has a single transition to but it should accept zero-width segments // this can happen when a node only has catchall parameters. - pathBuilder.DefaultDestination = Transition(node.Parameters); - pathBuilder.ExitDestination = pathBuilder.DefaultDestination; + currentExitDestination = currentDefaultDestination = Transition(node.Parameters); } else if (node.Parameters != null && node.CatchAll != null) { // This node has a separate transition for zero-width segments // this can happen when a node has both parameters and catchall parameters. - pathBuilder.DefaultDestination = Transition(node.Parameters); - pathBuilder.ExitDestination = Transition(node.CatchAll); + currentDefaultDestination = Transition(node.Parameters); + currentExitDestination = Transition(node.CatchAll); } else if (node.Parameters != null) { // This node has paramters but no catchall. - pathBuilder.DefaultDestination = Transition(node.Parameters); + currentDefaultDestination = Transition(node.Parameters); } else if (node.CatchAll != null) { // This node has a catchall but no parameters - pathBuilder.DefaultDestination = Transition(node.CatchAll); - pathBuilder.ExitDestination = pathBuilder.DefaultDestination; + currentExitDestination = currentDefaultDestination = Transition(node.CatchAll); } if (node.PolicyEdges != null && node.PolicyEdges.Count > 0) { - var policyBuilder = new PolicyJumpTableBuilder(node.NodeBuilder); - tableBuilders[currentStateIndex] = (pathBuilder, policyBuilder); + policyEntries = new PolicyJumpTableEdge[node.PolicyEdges.Count]; + var index = 0; foreach (var kvp in node.PolicyEdges) { - policyBuilder.AddEntry(kvp.Key, Transition(kvp.Value)); + policyEntries[index++] = new PolicyJumpTableEdge(kvp.Key, Transition(kvp.Value)); } } + var candidates = CreateCandidates(node.Matches); + states[currentStateIndex] = new DfaState( + candidates, + JumpTableBuilder.Build(currentDefaultDestination, currentExitDestination, pathEntries), + BuildPolicy(currentExitDestination, node.NodeBuilder, policyEntries)); + return currentStateIndex; int Transition(DfaNode next) @@ -406,11 +382,21 @@ namespace Microsoft.AspNetCore.Routing.Matching else { _stateIndex++; - return AddNode(next, states, tableBuilders); + return AddNode(next, states, exitDestination); } } } + private static PolicyJumpTable BuildPolicy(int exitDestination, INodeBuilderPolicy nodeBuilder, PolicyJumpTableEdge[] policyEntries) + { + if (policyEntries == null) + { + return null; + } + + return nodeBuilder.BuildJumpTable(exitDestination, policyEntries); + } + // Builds an array of candidates for a node, assigns a 'score' for each // endpoint. internal Candidate[] CreateCandidates(IReadOnlyList endpoints) diff --git a/src/Microsoft.AspNetCore.Routing/Matching/HttpMethodMatcherPolicy.cs b/src/Microsoft.AspNetCore.Routing/Matching/HttpMethodMatcherPolicy.cs index d51c51c2ba..652a290230 100644 --- a/src/Microsoft.AspNetCore.Routing/Matching/HttpMethodMatcherPolicy.cs +++ b/src/Microsoft.AspNetCore.Routing/Matching/HttpMethodMatcherPolicy.cs @@ -80,7 +80,7 @@ namespace Microsoft.AspNetCore.Routing.Matching // // For now we're just building up the set of keys. We don't add any endpoints // to lists now because we don't want ordering problems. - var allHttpMethods = new HashSet(StringComparer.OrdinalIgnoreCase); + var allHttpMethods = new List(); var edges = new Dictionary>(); for (var i = 0; i < endpoints.Count; i++) { @@ -120,11 +120,16 @@ namespace Microsoft.AspNetCore.Routing.Matching // Also if it's not the *any* method key, then track it. if (!string.Equals(AnyMethod, httpMethod, StringComparison.OrdinalIgnoreCase)) { - allHttpMethods.Add(httpMethod); + if (!ContainsHttpMethod(allHttpMethods, httpMethod)) + { + allHttpMethods.Add(httpMethod); + } } } } + allHttpMethods.Sort(StringComparer.OrdinalIgnoreCase); + // Now in a second loop, add endpoints to these lists. We've enumerated all of // the states, so we want to see which states this endpoint matches. for (var i = 0; i < endpoints.Count; i++) @@ -185,7 +190,7 @@ namespace Microsoft.AspNetCore.Routing.Matching if (!edges.TryGetValue(new EdgeKey(AnyMethod, false), out var matches)) { // Methods sorted for testability. - var endpoint = CreateRejectionEndpoint(allHttpMethods.OrderBy(m => m)); + var endpoint = CreateRejectionEndpoint(allHttpMethods); matches = new List() { endpoint, }; edges[new EdgeKey(AnyMethod, false)] = matches; } @@ -261,6 +266,19 @@ namespace Microsoft.AspNetCore.Routing.Matching Http405EndpointDisplayName); } + private static bool ContainsHttpMethod(List httpMethods, string httpMethod) + { + for (var i = 0; i < httpMethods.Count; i++) + { + if (StringComparer.OrdinalIgnoreCase.Equals(httpMethods[i], httpMethod)) + { + return true; + } + } + + return false; + } + private class HttpMethodPolicyJumpTable : PolicyJumpTable { private readonly int _exitDestination; diff --git a/src/Microsoft.AspNetCore.Routing/Matching/JumpTableBuilder.cs b/src/Microsoft.AspNetCore.Routing/Matching/JumpTableBuilder.cs index dcd5e3abc3..69939ffeeb 100644 --- a/src/Microsoft.AspNetCore.Routing/Matching/JumpTableBuilder.cs +++ b/src/Microsoft.AspNetCore.Routing/Matching/JumpTableBuilder.cs @@ -2,39 +2,24 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; namespace Microsoft.AspNetCore.Routing.Matching { - internal class JumpTableBuilder + internal static class JumpTableBuilder { public static readonly int InvalidDestination = -1; - private readonly List<(string text, int destination)> _entries = new List<(string text, int destination)>(); - - // The destination state when none of the text entries match. - public int DefaultDestination { get; set; } = InvalidDestination; - - // The destination state for a zero-length segment. This is a special - // case because parameters don't match a zero-length segment. - public int ExitDestination { get; set; } = InvalidDestination; - - public void AddEntry(string text, int destination) + public static JumpTable Build(int defaultDestination, int exitDestination, (string text, int destination)[] pathEntries) { - _entries.Add((text, destination)); - } - - public JumpTable Build() - { - if (DefaultDestination == InvalidDestination) + if (defaultDestination == InvalidDestination) { - var message = $"{nameof(DefaultDestination)} is not set. Please report this as a bug."; + var message = $"{nameof(defaultDestination)} is not set. Please report this as a bug."; throw new InvalidOperationException(message); } - if (ExitDestination == InvalidDestination) + if (exitDestination == InvalidDestination) { - var message = $"{nameof(ExitDestination)} is not set. Please report this as a bug."; + var message = $"{nameof(exitDestination)} is not set. Please report this as a bug."; throw new InvalidOperationException(message); } @@ -49,16 +34,16 @@ namespace Microsoft.AspNetCore.Routing.Matching // We have an optimized fast path for zero entries since we don't have to // do any string comparisons. - if (_entries.Count == 0) + if (pathEntries == null || pathEntries.Length == 0) { - return new ZeroEntryJumpTable(DefaultDestination, ExitDestination); + return new ZeroEntryJumpTable(defaultDestination, exitDestination); } // The IL Emit jump table is not faster for a single entry - if (_entries.Count == 1) + if (pathEntries.Length == 1) { - var entry = _entries[0]; - return new SingleEntryJumpTable(DefaultDestination, ExitDestination, entry.text, entry.destination); + var entry = pathEntries[0]; + return new SingleEntryJumpTable(defaultDestination, exitDestination, entry.text, entry.destination); } // We choose a hard upper bound of 100 as the limit for when we switch to a dictionary @@ -72,9 +57,9 @@ namespace Microsoft.AspNetCore.Routing.Matching // Additionally if we're on 32bit, the scalability is worse, so switch to the dictionary at 50 // entries. var threshold = IntPtr.Size == 8 ? 100 : 50; - if (_entries.Count >= threshold) + if (pathEntries.Length >= threshold) { - return new DictionaryJumpTable(DefaultDestination, ExitDestination, _entries.ToArray()); + return new DictionaryJumpTable(defaultDestination, exitDestination, pathEntries); } // If we have more than a single string, the IL emit strategy is the fastest - but we need to decide @@ -82,18 +67,17 @@ namespace Microsoft.AspNetCore.Routing.Matching JumpTable fallback; // Based on our testing a linear search is still faster than a dictionary at ten entries. - if (_entries.Count <= 10) + if (pathEntries.Length <= 10) { - fallback = new LinearSearchJumpTable(DefaultDestination, ExitDestination, _entries.ToArray()); + fallback = new LinearSearchJumpTable(defaultDestination, exitDestination, pathEntries); } else { - fallback = new DictionaryJumpTable(DefaultDestination, ExitDestination, _entries.ToArray()); + fallback = new DictionaryJumpTable(defaultDestination, exitDestination, pathEntries); } #if IL_EMIT - - return new ILEmitTrieJumpTable(DefaultDestination, ExitDestination, _entries.ToArray(), vectorize: null, fallback); + return new ILEmitTrieJumpTable(defaultDestination, exitDestination, pathEntries, vectorize: null, fallback); #else return fallback; #endif diff --git a/src/Microsoft.AspNetCore.Routing/Matching/PolicyJumpTableBuilder.cs b/src/Microsoft.AspNetCore.Routing/Matching/PolicyJumpTableBuilder.cs deleted file mode 100644 index f533a9813c..0000000000 --- a/src/Microsoft.AspNetCore.Routing/Matching/PolicyJumpTableBuilder.cs +++ /dev/null @@ -1,32 +0,0 @@ -// 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; - -namespace Microsoft.AspNetCore.Routing.Matching -{ - internal class PolicyJumpTableBuilder - { - private readonly INodeBuilderPolicy _nodeBuilder; - private readonly List _entries; - - public PolicyJumpTableBuilder(INodeBuilderPolicy nodeBuilder) - { - _nodeBuilder = nodeBuilder; - _entries = new List(); - } - - // The destination state for a non-match. - public int ExitDestination { get; set; } = JumpTableBuilder.InvalidDestination; - - public void AddEntry(object state, int destination) - { - _entries.Add(new PolicyJumpTableEdge(state, destination)); - } - - public PolicyJumpTable Build() - { - return _nodeBuilder.BuildJumpTable(ExitDestination, _entries); - } - } -}