From 7904c7756def9c5fd03112783bbc9fa2290d90b8 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Fri, 31 Mar 2017 10:11:30 -0700 Subject: [PATCH] Add a simple action selection benchmark --- .../ActionSelectorBenchmark.cs | 260 ++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 test/Microsoft.AspNetCore.Mvc.Performance/ActionSelectorBenchmark.cs diff --git a/test/Microsoft.AspNetCore.Mvc.Performance/ActionSelectorBenchmark.cs b/test/Microsoft.AspNetCore.Mvc.Performance/ActionSelectorBenchmark.cs new file mode 100644 index 0000000000..3049b3b705 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.Performance/ActionSelectorBenchmark.cs @@ -0,0 +1,260 @@ +// 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 +{ + [Config(typeof(CoreConfig))] + public class ActionSelectorBenchmark + { + private const int Seed = 1000; + + // About 35 or so plausable 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>[] _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 = NaiveSelectCandiates(_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 NaiveSelectCandiates(ActionDescriptor[] actions, RouteValueDictionary routeValues) + { + var results = new List(); + 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(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>[] GetDataSet(ActionDescriptor[] actions) + { + var random = new Random(Seed); + + var data = new List>>(); + + for (var i = 0; i < actions.Length; i += 2) + { + var action = actions[i]; + var routeValues = new RouteValueDictionary(action.RouteValues); + var matches = NaiveSelectCandiates(actions, routeValues); + if (matches.Count == 0) + { + throw new InvalidOperationException("This should have at least one match."); + } + + + data.Add(new KeyValuePair>(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 = NaiveSelectCandiates(actions, routeValues); + if (matches.Count != 0) + { + throw new InvalidOperationException("This should have 0 matches."); + } + + data.Add(new KeyValuePair>(routeValues, matches)); + } + + return data.ToArray(); + } + + private static void Verify(IReadOnlyList expected, IReadOnlyList actual) + { + 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( + new ActionSelectorDecisionTreeProvider(actionCollection), + new ActionConstraintCache(actionCollection, Enumerable.Empty()), + 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(); + } + } + } +}