Improve performance and reduce allocations in RouteValuesAddressScheme. (#879)

This commit is contained in:
Gert Driesen 2018-10-23 02:25:43 +02:00 committed by James Newton-King
parent bc482cd2b0
commit 25b5ab2c39
2 changed files with 125 additions and 15 deletions

View File

@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Routing
{ {
private readonly CompositeEndpointDataSource _dataSource; private readonly CompositeEndpointDataSource _dataSource;
private LinkGenerationDecisionTree _allMatchesLinkGenerationTree; private LinkGenerationDecisionTree _allMatchesLinkGenerationTree;
private IDictionary<string, List<OutboundMatchResult>> _namedMatchResults; private Dictionary<string, List<OutboundMatchResult>> _namedMatchResults;
public RouteValuesAddressScheme(CompositeEndpointDataSource dataSource) public RouteValuesAddressScheme(CompositeEndpointDataSource dataSource)
{ {
@ -33,7 +33,7 @@ namespace Microsoft.AspNetCore.Routing
public IEnumerable<Endpoint> FindEndpoints(RouteValuesAddress address) public IEnumerable<Endpoint> FindEndpoints(RouteValuesAddress address)
{ {
IEnumerable<OutboundMatchResult> matchResults = null; IList<OutboundMatchResult> matchResults = null;
if (string.IsNullOrEmpty(address.RouteName)) if (string.IsNullOrEmpty(address.RouteName))
{ {
matchResults = _allMatchesLinkGenerationTree.GetMatches( matchResults = _allMatchesLinkGenerationTree.GetMatches(
@ -45,14 +45,33 @@ namespace Microsoft.AspNetCore.Routing
matchResults = namedMatchResults; matchResults = namedMatchResults;
} }
if (matchResults == null || !matchResults.Any()) if (matchResults != null)
{ {
return Array.Empty<Endpoint>(); var matchCount = matchResults.Count;
if (matchCount > 0)
{
if (matchResults.Count == 1)
{
// Special case having a single result to avoid creating iterator state machine
return new[] { (RouteEndpoint)matchResults[0].Match.Entry.Data };
}
else
{
// Use separate method since one cannot have regular returns in an iterator method
return GetEndpoints(matchResults, matchCount);
}
}
} }
return matchResults return Array.Empty<Endpoint>();
.Select(matchResult => matchResult.Match) }
.Select(match => (RouteEndpoint)match.Entry.Data);
private static IEnumerable<Endpoint> GetEndpoints(IList<OutboundMatchResult> matchResults, int matchCount)
{
for (var i = 0; i < matchCount; i++)
{
yield return (RouteEndpoint)matchResults[i].Match.Entry.Data;
}
} }
private void HandleChange() private void HandleChange()
@ -73,7 +92,7 @@ namespace Microsoft.AspNetCore.Routing
// as refresh of new endpoints happens within a lock and also these fields are not publicly accessible. // as refresh of new endpoints happens within a lock and also these fields are not publicly accessible.
var (allMatches, namedMatchResults) = GetOutboundMatches(); var (allMatches, namedMatchResults) = GetOutboundMatches();
_namedMatchResults = namedMatchResults; _namedMatchResults = namedMatchResults;
_allMatchesLinkGenerationTree = new LinkGenerationDecisionTree(allMatches.ToArray()); _allMatchesLinkGenerationTree = new LinkGenerationDecisionTree(allMatches);
} }
/// Decision tree is built using the 'required values' of actions. /// Decision tree is built using the 'required values' of actions.
@ -93,21 +112,25 @@ namespace Microsoft.AspNetCore.Routing
/// requiredValues: new { controller = "Orders", action = "GetById" }, /// requiredValues: new { controller = "Orders", action = "GetById" },
/// A call to GetLink("OrdersApi", new { id = "10" }) cannot generate url as neither the supplied values or /// A call to GetLink("OrdersApi", new { id = "10" }) cannot generate url as neither the supplied values or
/// current ambient values do not satisfy the decision tree that is built based on the required values. /// current ambient values do not satisfy the decision tree that is built based on the required values.
protected virtual (IEnumerable<OutboundMatch>, IDictionary<string, List<OutboundMatchResult>>) GetOutboundMatches() protected virtual (List<OutboundMatch>, Dictionary<string, List<OutboundMatchResult>>) GetOutboundMatches()
{ {
var allOutboundMatches = new List<OutboundMatch>(); var allOutboundMatches = new List<OutboundMatch>();
var namedOutboundMatchResults = new Dictionary<string, List<OutboundMatchResult>>( var namedOutboundMatchResults = new Dictionary<string, List<OutboundMatchResult>>(
StringComparer.OrdinalIgnoreCase); StringComparer.OrdinalIgnoreCase);
var endpoints = _dataSource.Endpoints.OfType<RouteEndpoint>(); foreach (var endpoint in _dataSource.Endpoints)
foreach (var endpoint in endpoints)
{ {
if (!(endpoint is RouteEndpoint routeEndpoint))
{
continue;
}
if (endpoint.Metadata.GetMetadata<ISuppressLinkGenerationMetadata>()?.SuppressLinkGeneration == true) if (endpoint.Metadata.GetMetadata<ISuppressLinkGenerationMetadata>()?.SuppressLinkGeneration == true)
{ {
continue; continue;
} }
var entry = CreateOutboundRouteEntry(endpoint); var entry = CreateOutboundRouteEntry(routeEndpoint);
var outboundMatch = new OutboundMatch() { Entry = entry }; var outboundMatch = new OutboundMatch() { Entry = entry };
allOutboundMatches.Add(outboundMatch); allOutboundMatches.Add(outboundMatch);
@ -117,8 +140,7 @@ namespace Microsoft.AspNetCore.Routing
continue; continue;
} }
List<OutboundMatchResult> matchResults; if (!namedOutboundMatchResults.TryGetValue(entry.RouteName, out var matchResults))
if (!namedOutboundMatchResults.TryGetValue(entry.RouteName, out matchResults))
{ {
matchResults = new List<OutboundMatchResult>(); matchResults = new List<OutboundMatchResult>();
namedOutboundMatchResults.Add(entry.RouteName, matchResults); namedOutboundMatchResults.Add(entry.RouteName, matchResults);

View File

@ -139,6 +139,94 @@ namespace Microsoft.AspNetCore.Routing
}); });
} }
[Fact]
public void FindEndpoints_LookedUpByCriteria_NoMatch()
{
// Arrange
var endpoint1 = CreateEndpoint(
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
defaults: new { zipCode = 3510 },
requiredValues: new { id = 7 },
routeName: "OrdersApi");
var endpoint2 = CreateEndpoint(
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
defaults: new { id = 12 },
requiredValues: new { zipCode = 3510 },
routeName: "OrdersApi");
var addressScheme = CreateAddressScheme(endpoint1, endpoint2);
// Act
var foundEndpoints = addressScheme.FindEndpoints(
new RouteValuesAddress
{
ExplicitValues = new RouteValueDictionary(new { id = 8 }),
AmbientValues = new RouteValueDictionary(new { urgent = false }),
});
// Assert
Assert.Empty(foundEndpoints);
}
[Fact]
public void FindEndpoints_LookedUpByCriteria_OneMatch()
{
// Arrange
var endpoint1 = CreateEndpoint(
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
defaults: new { zipCode = 3510 },
requiredValues: new { id = 7 },
routeName: "OrdersApi");
var endpoint2 = CreateEndpoint(
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
defaults: new { id = 12 },
routeName: "OrdersApi");
var addressScheme = CreateAddressScheme(endpoint1, endpoint2);
// Act
var foundEndpoints = addressScheme.FindEndpoints(
new RouteValuesAddress
{
ExplicitValues = new RouteValueDictionary(new { id = 13 }),
AmbientValues = new RouteValueDictionary(new { zipCode = 3500 }),
});
// Assert
var actual = Assert.Single(foundEndpoints);
Assert.Same(endpoint2, actual);
}
[Fact]
public void FindEndpoints_LookedUpByCriteria_MultipleMatches()
{
// Arrange
var endpoint1 = CreateEndpoint(
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
defaults: new { zipCode = 3510 },
requiredValues: new { id = 7 },
routeName: "OrdersApi");
var endpoint2 = CreateEndpoint(
"api/orders/{id}/{name?}/{urgent}/{zipCode}",
defaults: new { id = 12 },
routeName: "OrdersApi");
var endpoint3 = CreateEndpoint(
"api/orders/{id}/{name?}/{urgent=true}/{zipCode}",
defaults: new { id = 12 },
routeName: "OrdersApi");
var addressScheme = CreateAddressScheme(endpoint1, endpoint2, endpoint3);
// Act
var foundEndpoints = addressScheme.FindEndpoints(
new RouteValuesAddress
{
ExplicitValues = new RouteValueDictionary(new { id = 7 }),
AmbientValues = new RouteValueDictionary(new { zipCode = 3500 }),
});
// Assert
Assert.Contains(endpoint1, foundEndpoints);
Assert.Contains(endpoint1, foundEndpoints);
}
[Fact] [Fact]
public void FindEndpoints_ReturnsEndpoint_WhenLookedUpByRouteName() public void FindEndpoints_ReturnsEndpoint_WhenLookedUpByRouteName()
{ {
@ -270,7 +358,7 @@ namespace Microsoft.AspNetCore.Routing
public IDictionary<string, List<OutboundMatchResult>> NamedMatches { get; private set; } public IDictionary<string, List<OutboundMatchResult>> NamedMatches { get; private set; }
protected override (IEnumerable<OutboundMatch>, IDictionary<string, List<OutboundMatchResult>>) GetOutboundMatches() protected override (List<OutboundMatch>, Dictionary<string, List<OutboundMatchResult>>) GetOutboundMatches()
{ {
var matches = base.GetOutboundMatches(); var matches = base.GetOutboundMatches();
AllMatches = matches.Item1; AllMatches = matches.Item1;