265 lines
12 KiB
C#
265 lines
12 KiB
C#
// 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.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using BenchmarkDotNet.Attributes;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc.Abstractions;
|
|
using Microsoft.AspNetCore.Mvc.ActionConstraints;
|
|
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
|
using Microsoft.AspNetCore.Mvc.Internal;
|
|
using Microsoft.AspNetCore.Routing;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
|
|
namespace Microsoft.AspNetCore.Mvc.Performance
|
|
{
|
|
public class ActionSelectorBenchmark
|
|
{
|
|
private const int Seed = 1000;
|
|
|
|
// About 35 or so plausible sounding conventional routing actions.
|
|
//
|
|
// We include some duplicates here, because that's what happens when you have one method that handles
|
|
// GET and one that handles POST.
|
|
private static readonly ActionDescriptor[] _actions = new ActionDescriptor[]
|
|
{
|
|
CreateActionDescriptor(new { area = "Admin", controller = "Account", action = "AddUser" }),
|
|
CreateActionDescriptor(new { area = "Admin", controller = "Account", action = "AddUser" }),
|
|
CreateActionDescriptor(new { area = "Admin", controller = "Account", action = "DeleteUser" }),
|
|
CreateActionDescriptor(new { area = "Admin", controller = "Account", action = "DeleteUser" }),
|
|
CreateActionDescriptor(new { area = "Admin", controller = "Account", action = "Details" }),
|
|
CreateActionDescriptor(new { area = "Admin", controller = "Account", action = "List" }),
|
|
|
|
CreateActionDescriptor(new { area = "Admin", controller = "Diagnostics", action = "Stats" }),
|
|
CreateActionDescriptor(new { area = "Admin", controller = "Diagnostics", action = "Performance" }),
|
|
|
|
CreateActionDescriptor(new { area = "Admin", controller = "Products", action = "CreateProduct" }),
|
|
CreateActionDescriptor(new { area = "Admin", controller = "Products", action = "CreateProduct" }),
|
|
CreateActionDescriptor(new { area = "Admin", controller = "Products", action = "DeleteProduct" }),
|
|
CreateActionDescriptor(new { area = "Admin", controller = "Products", action = "DeleteProduct" }),
|
|
CreateActionDescriptor(new { area = "Admin", controller = "Products", action = "EditProduct" }),
|
|
CreateActionDescriptor(new { area = "Admin", controller = "Products", action = "EditProduct" }),
|
|
CreateActionDescriptor(new { area = "Admin", controller = "Products", action = "Index" }),
|
|
CreateActionDescriptor(new { area = "Admin", controller = "Products", action = "Inventory" }),
|
|
|
|
CreateActionDescriptor(new { area = "Store", controller = "Search", action = "FindProduct" }),
|
|
CreateActionDescriptor(new { area = "Store", controller = "Search", action = "ShowCategory" }),
|
|
CreateActionDescriptor(new { area = "Store", controller = "Search", action = "HotItems" }),
|
|
|
|
CreateActionDescriptor(new { area = "Store", controller = "Product", action = "Index" }),
|
|
CreateActionDescriptor(new { area = "Store", controller = "Product", action = "Details" }),
|
|
CreateActionDescriptor(new { area = "Store", controller = "Product", action = "Buy" }),
|
|
|
|
CreateActionDescriptor(new { area = "Store", controller = "Checkout", action = "ViewCart" }),
|
|
CreateActionDescriptor(new { area = "Store", controller = "Checkout", action = "Billing" }),
|
|
CreateActionDescriptor(new { area = "Store", controller = "Checkout", action = "Confim" }),
|
|
CreateActionDescriptor(new { area = "Store", controller = "Checkout", action = "Confim" }),
|
|
|
|
CreateActionDescriptor(new { area = "", controller = "Blog", action = "Index" }),
|
|
CreateActionDescriptor(new { area = "", controller = "Blog", action = "Search" }),
|
|
CreateActionDescriptor(new { area = "", controller = "Blog", action = "ViewPost" }),
|
|
CreateActionDescriptor(new { area = "", controller = "Blog", action = "PostComment" }),
|
|
|
|
CreateActionDescriptor(new { area = "", controller = "Home", action = "Index" }),
|
|
CreateActionDescriptor(new { area = "", controller = "Home", action = "Search" }),
|
|
CreateActionDescriptor(new { area = "", controller = "Home", action = "About" }),
|
|
CreateActionDescriptor(new { area = "", controller = "Home", action = "Contact" }),
|
|
CreateActionDescriptor(new { area = "", controller = "Home", action = "Support" }),
|
|
};
|
|
|
|
private static readonly KeyValuePair<RouteValueDictionary, IReadOnlyList<ActionDescriptor>>[] _dataSet = GetDataSet(_actions);
|
|
|
|
private static readonly IActionSelector _actionSelector = CreateActionSelector(_actions);
|
|
|
|
[Benchmark(Description = "conventional action selection implementation")]
|
|
public void SelectCandidates_MatchRouteData()
|
|
{
|
|
var routeContext = new RouteContext(new DefaultHttpContext());
|
|
|
|
for (var i = 0; i < _dataSet.Length; i++)
|
|
{
|
|
var routeValues = _dataSet[i].Key;
|
|
var expected = _dataSet[i].Value;
|
|
|
|
var state = routeContext.RouteData.PushState(MockRouter.Instance, routeValues, null);
|
|
|
|
var actual = _actionSelector.SelectCandidates(routeContext);
|
|
Verify(expected, actual);
|
|
|
|
state.Restore();
|
|
}
|
|
}
|
|
|
|
[Benchmark(Baseline = true, Description = "conventional action selection baseline")]
|
|
public void SelectCandidates_Baseline()
|
|
{
|
|
var routeContext = new RouteContext(new DefaultHttpContext());
|
|
|
|
for (var i = 0; i < _dataSet.Length; i++)
|
|
{
|
|
var routeValues = _dataSet[i].Key;
|
|
var expected = _dataSet[i].Value;
|
|
|
|
var state = routeContext.RouteData.PushState(MockRouter.Instance, routeValues, null);
|
|
|
|
var actual = NaiveSelectCandidates(_actions, routeContext.RouteData.Values);
|
|
Verify(expected, actual);
|
|
|
|
state.Restore();
|
|
}
|
|
}
|
|
|
|
// A naive implementation we can use to generate match data for inputs, and for a baseline.
|
|
private static IReadOnlyList<ActionDescriptor> NaiveSelectCandidates(ActionDescriptor[] actions, RouteValueDictionary routeValues)
|
|
{
|
|
var results = new List<ActionDescriptor>();
|
|
for (var i = 0; i < actions.Length; i++)
|
|
{
|
|
var action = actions[i];
|
|
|
|
var isMatch = true;
|
|
foreach (var kvp in action.RouteValues)
|
|
{
|
|
var routeValue = Convert.ToString(routeValues[kvp.Key]) ?? string.Empty;
|
|
|
|
if (string.IsNullOrEmpty(kvp.Value) && string.IsNullOrEmpty(routeValue))
|
|
{
|
|
// Match
|
|
}
|
|
else if (string.Equals(kvp.Value, routeValue, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// Match;
|
|
}
|
|
else
|
|
{
|
|
isMatch = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (isMatch)
|
|
{
|
|
results.Add(action);
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
private static ActionDescriptor CreateActionDescriptor(object obj)
|
|
{
|
|
// Our real ActionDescriptors don't use RVD, they use a regular old dictionary.
|
|
// Just using RVD here to understand the anonymous object for brevity.
|
|
var routeValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (var kvp in new RouteValueDictionary(obj))
|
|
{
|
|
routeValues.Add(kvp.Key, Convert.ToString(kvp.Value) ?? string.Empty);
|
|
}
|
|
|
|
return new ActionDescriptor()
|
|
{
|
|
RouteValues = routeValues,
|
|
};
|
|
}
|
|
|
|
private static KeyValuePair<RouteValueDictionary, IReadOnlyList<ActionDescriptor>>[] GetDataSet(ActionDescriptor[] actions)
|
|
{
|
|
var random = new Random(Seed);
|
|
|
|
var data = new List<KeyValuePair<RouteValueDictionary, IReadOnlyList<ActionDescriptor>>>();
|
|
|
|
for (var i = 0; i < actions.Length; i += 2)
|
|
{
|
|
var action = actions[i];
|
|
var routeValues = new RouteValueDictionary(action.RouteValues);
|
|
var matches = NaiveSelectCandidates(actions, routeValues);
|
|
if (matches.Count == 0)
|
|
{
|
|
throw new InvalidOperationException("This should have at least one match.");
|
|
}
|
|
|
|
|
|
data.Add(new KeyValuePair<RouteValueDictionary, IReadOnlyList<ActionDescriptor>>(routeValues, matches));
|
|
}
|
|
|
|
for (var i = 1; i < actions.Length; i += 3)
|
|
{
|
|
var action = actions[i];
|
|
var routeValues = new RouteValueDictionary(action.RouteValues);
|
|
|
|
// Make one of the route values not match.
|
|
routeValues[routeValues.First().Key] = ((string)routeValues.First().Value) + "fkdkfdkkf";
|
|
|
|
var matches = NaiveSelectCandidates(actions, routeValues);
|
|
if (matches.Count != 0)
|
|
{
|
|
throw new InvalidOperationException("This should have 0 matches.");
|
|
}
|
|
|
|
data.Add(new KeyValuePair<RouteValueDictionary, IReadOnlyList<ActionDescriptor>>(routeValues, matches));
|
|
}
|
|
|
|
return data.ToArray();
|
|
}
|
|
|
|
private static void Verify(IReadOnlyList<ActionDescriptor> expected, IReadOnlyList<ActionDescriptor> actual)
|
|
{
|
|
if (expected.Count == 0 && actual == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (expected.Count != actual.Count)
|
|
{
|
|
throw new InvalidOperationException("The count is different.");
|
|
}
|
|
|
|
for (var i = 0; i < actual.Count; i++)
|
|
{
|
|
if (!object.ReferenceEquals(expected[i], actual[i]))
|
|
{
|
|
throw new InvalidOperationException("The actions don't match.");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static IActionSelector CreateActionSelector(ActionDescriptor[] actions)
|
|
{
|
|
var actionCollection = new MockActionDescriptorCollectionProvider(actions);
|
|
|
|
return new ActionSelector(
|
|
actionCollection,
|
|
new ActionConstraintCache(actionCollection, Enumerable.Empty<IActionConstraintProvider>()),
|
|
NullLoggerFactory.Instance);
|
|
}
|
|
|
|
private class MockActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider
|
|
{
|
|
public MockActionDescriptorCollectionProvider(ActionDescriptor[] actions)
|
|
{
|
|
ActionDescriptors = new ActionDescriptorCollection(actions, 0);
|
|
}
|
|
|
|
public ActionDescriptorCollection ActionDescriptors { get; }
|
|
}
|
|
|
|
private class MockRouter : IRouter
|
|
{
|
|
public static readonly IRouter Instance = new MockRouter();
|
|
|
|
public VirtualPathData GetVirtualPath(VirtualPathContext context)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
public Task RouteAsync(RouteContext context)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
}
|
|
}
|
|
}
|