Optimize builder and HTTP policy allocations (#768)
This commit is contained in:
parent
abc378d3dc
commit
fe8c633224
|
|
@ -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<DfaNode> parents)>();
|
||||
var work = new List<(RouteEndpoint endpoint, List<DfaNode> parents)>(_endpoints.Count);
|
||||
List<(RouteEndpoint endpoint, List<DfaNode> 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<Candidate>(), 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<Candidate>(),
|
||||
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<Endpoint> endpoints)
|
||||
|
|
|
|||
|
|
@ -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<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var allHttpMethods = new List<string>();
|
||||
var edges = new Dictionary<EdgeKey, List<Endpoint>>();
|
||||
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>() { endpoint, };
|
||||
edges[new EdgeKey(AnyMethod, false)] = matches;
|
||||
}
|
||||
|
|
@ -261,6 +266,19 @@ namespace Microsoft.AspNetCore.Routing.Matching
|
|||
Http405EndpointDisplayName);
|
||||
}
|
||||
|
||||
private static bool ContainsHttpMethod(List<string> 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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<PolicyJumpTableEdge> _entries;
|
||||
|
||||
public PolicyJumpTableBuilder(INodeBuilderPolicy nodeBuilder)
|
||||
{
|
||||
_nodeBuilder = nodeBuilder;
|
||||
_entries = new List<PolicyJumpTableEdge>();
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue