From dcfb63a7684bfd1ad1a24804b08015abe68e836d Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Thu, 6 Sep 2018 14:00:52 +1200 Subject: [PATCH] Avoid RoutePattern allocating empty dictionaries (#772) --- .../Matching/RouteEndpointAzureBenchmark.cs | 16 ++++++ .../Patterns/RoutePatternFactory.cs | 55 +++++++++++++++---- 2 files changed, 59 insertions(+), 12 deletions(-) create mode 100644 benchmarks/Microsoft.AspNetCore.Routing.Performance/Matching/RouteEndpointAzureBenchmark.cs diff --git a/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matching/RouteEndpointAzureBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matching/RouteEndpointAzureBenchmark.cs new file mode 100644 index 0000000000..5d3af48562 --- /dev/null +++ b/benchmarks/Microsoft.AspNetCore.Routing.Performance/Matching/RouteEndpointAzureBenchmark.cs @@ -0,0 +1,16 @@ +// 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 BenchmarkDotNet.Attributes; + +namespace Microsoft.AspNetCore.Routing.Matching +{ + public class RouteEndpointAzureBenchmark : MatcherAzureBenchmarkBase + { + [Benchmark] + public void CreateEndpoints() + { + SetupEndpoints(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternFactory.cs b/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternFactory.cs index 51b869c58c..bbc1e50aca 100644 --- a/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternFactory.cs +++ b/src/Microsoft.AspNetCore.Routing/Patterns/RoutePatternFactory.cs @@ -2,7 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.AspNetCore.Routing.Matching; @@ -16,6 +18,12 @@ namespace Microsoft.AspNetCore.Routing.Patterns /// public static class RoutePatternFactory { + private static readonly IReadOnlyDictionary EmptyDefaultsDictionary = + new ReadOnlyDictionary(new Dictionary()); + + private static readonly IReadOnlyDictionary> EmptyPoliciesDictionary = + new ReadOnlyDictionary>(new Dictionary>()); + /// /// Creates a from its string representation. /// @@ -242,8 +250,8 @@ namespace Microsoft.AspNetCore.Routing.Patterns private static RoutePattern PatternCore( string rawText, - IDictionary defaults, - IDictionary parameterPolicies, + RouteValueDictionary defaults, + RouteValueDictionary parameterPolicies, IEnumerable segments) { // We want to merge the segment data with the 'out of line' defaults and parameter policies. @@ -257,18 +265,22 @@ namespace Microsoft.AspNetCore.Routing.Patterns // It's important that these two views of the data are consistent. We don't want // values specified out of line to have a different behavior. - var updatedDefaults = new Dictionary(StringComparer.OrdinalIgnoreCase); - if (defaults != null) + Dictionary updatedDefaults = null; + if (defaults != null && defaults.Count > 0) { + updatedDefaults = new Dictionary(defaults.Count, StringComparer.OrdinalIgnoreCase); + foreach (var kvp in defaults) { updatedDefaults.Add(kvp.Key, kvp.Value); } } - var updatedParameterPolicies = new Dictionary>(StringComparer.OrdinalIgnoreCase); - if (parameterPolicies != null) + Dictionary> updatedParameterPolicies = null; + if (parameterPolicies != null && parameterPolicies.Count > 0) { + updatedParameterPolicies = new Dictionary>(parameterPolicies.Count, StringComparer.OrdinalIgnoreCase); + foreach (var kvp in parameterPolicies) { updatedParameterPolicies.Add(kvp.Key, new List() @@ -280,7 +292,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns } } - var parameters = new List(); + List parameters = null; var updatedSegments = segments.ToArray(); for (var i = 0; i < updatedSegments.Length; i++) { @@ -291,6 +303,11 @@ namespace Microsoft.AspNetCore.Routing.Patterns { if (segment.Parts[j] is RoutePatternParameterPart parameter) { + if (parameters == null) + { + parameters = new List(); + } + parameters.Add(parameter); } } @@ -298,9 +315,11 @@ namespace Microsoft.AspNetCore.Routing.Patterns return new RoutePattern( rawText, - updatedDefaults, - updatedParameterPolicies.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList)kvp.Value.ToArray()), - parameters, + updatedDefaults ?? EmptyDefaultsDictionary, + updatedParameterPolicies != null + ? updatedParameterPolicies.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList)kvp.Value.ToArray()) + : EmptyPoliciesDictionary, + (IReadOnlyList)parameters ?? Array.Empty(), updatedSegments); RoutePatternPathSegment VisitSegment(RoutePatternPathSegment segment) @@ -341,7 +360,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns var parameter = (RoutePatternParameterPart)part; var @default = parameter.Default; - if (updatedDefaults.TryGetValue(parameter.Name, out var newDefault)) + if (updatedDefaults != null && updatedDefaults.TryGetValue(parameter.Name, out var newDefault)) { if (parameter.Default != null && !Equals(newDefault, parameter.Default)) { @@ -360,12 +379,23 @@ namespace Microsoft.AspNetCore.Routing.Patterns if (parameter.Default != null) { + if (updatedDefaults == null) + { + updatedDefaults = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + updatedDefaults[parameter.Name] = parameter.Default; } - if (!updatedParameterPolicies.TryGetValue(parameter.Name, out var parameterConstraints) && + List parameterConstraints = null; + if ((updatedParameterPolicies == null || !updatedParameterPolicies.TryGetValue(parameter.Name, out parameterConstraints)) && parameter.ParameterPolicies.Count > 0) { + if (updatedParameterPolicies == null) + { + updatedParameterPolicies = new Dictionary>(StringComparer.OrdinalIgnoreCase); + } + parameterConstraints = new List(); updatedParameterPolicies.Add(parameter.Name, parameterConstraints); } @@ -391,6 +421,7 @@ namespace Microsoft.AspNetCore.Routing.Patterns parameter.EncodeSlashes); } } + /// /// Creates a from the provided collection /// of parts.