From fa73350a992201f14bb64930cb536f9ecb645f46 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Sat, 30 Mar 2024 08:57:57 -0700 Subject: [PATCH 1/9] Use Array instead of List in ShapeDescriptorIndex --- .../Descriptors/FeatureShapeDescriptor.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/FeatureShapeDescriptor.cs b/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/FeatureShapeDescriptor.cs index 9e044ec7859..45a859cad16 100644 --- a/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/FeatureShapeDescriptor.cs +++ b/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/FeatureShapeDescriptor.cs @@ -23,15 +23,15 @@ public FeatureShapeDescriptor(IFeatureInfo feature, string shapeType) public class ShapeDescriptorIndex : ShapeDescriptor { private readonly ConcurrentDictionary _descriptors; - private readonly List _alternationDescriptors; - private readonly List _wrappers; - private readonly List _bindingSources; + private readonly FeatureShapeDescriptor[] _alternationDescriptors; + private readonly string[] _wrappers; + private readonly string[] _bindingSources; private readonly Dictionary _bindings; - private readonly List> _creatingAsync; - private readonly List> _createdAsync; - private readonly List> _displayingAsync; - private readonly List> _processingAsync; - private readonly List> _displayedAsync; + private readonly Func[] _creatingAsync; + private readonly Func[] _createdAsync; + private readonly Func[] _displayingAsync; + private readonly Func[] _processingAsync; + private readonly Func[] _displayedAsync; public ShapeDescriptorIndex( string shapeType, @@ -44,15 +44,15 @@ public ShapeDescriptorIndex( // pre-calculate as much as we can _alternationDescriptors = alterationKeys .Select(key => _descriptors[key]) - .ToList(); + .ToArray(); _wrappers = _alternationDescriptors .SelectMany(sd => sd.Wrappers) - .ToList(); + .ToArray(); _bindingSources = _alternationDescriptors .SelectMany(sd => sd.BindingSources) - .ToList(); + .ToArray(); _bindings = _alternationDescriptors .SelectMany(sd => sd.Bindings) @@ -62,23 +62,23 @@ public ShapeDescriptorIndex( _creatingAsync = _alternationDescriptors .SelectMany(sd => sd.CreatingAsync) - .ToList(); + .ToArray(); _createdAsync = _alternationDescriptors .SelectMany(sd => sd.CreatedAsync) - .ToList(); + .ToArray(); _displayingAsync = _alternationDescriptors .SelectMany(sd => sd.DisplayingAsync) - .ToList(); + .ToArray(); _processingAsync = _alternationDescriptors .SelectMany(sd => sd.ProcessingAsync) - .ToList(); + .ToArray(); _displayedAsync = _alternationDescriptors .SelectMany(sd => sd.DisplayedAsync) - .ToList(); + .ToArray(); } /// @@ -108,7 +108,7 @@ public ShapeDescriptorIndex( private PlacementInfo CalculatePlacement(ShapePlacementContext ctx) { PlacementInfo info = null; - for (var i = _alternationDescriptors.Count - 1; i >= 0; i--) + for (var i = _alternationDescriptors.Length - 1; i >= 0; i--) { var descriptor = _alternationDescriptors[i]; info = descriptor.Placement(ctx); From 7061ecfec8bf9098586d0a730bf54bcf54a902e3 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Mon, 1 Apr 2024 09:59:19 -0700 Subject: [PATCH 2/9] Reduce iterations instead of using array --- .../Descriptors/FeatureShapeDescriptor.cs | 99 +++++++++---------- 1 file changed, 47 insertions(+), 52 deletions(-) diff --git a/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/FeatureShapeDescriptor.cs b/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/FeatureShapeDescriptor.cs index 45a859cad16..3594e162411 100644 --- a/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/FeatureShapeDescriptor.cs +++ b/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/FeatureShapeDescriptor.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Html; using OrchardCore.DisplayManagement.Implementation; @@ -22,16 +21,15 @@ public FeatureShapeDescriptor(IFeatureInfo feature, string shapeType) public class ShapeDescriptorIndex : ShapeDescriptor { - private readonly ConcurrentDictionary _descriptors; - private readonly FeatureShapeDescriptor[] _alternationDescriptors; - private readonly string[] _wrappers; - private readonly string[] _bindingSources; - private readonly Dictionary _bindings; - private readonly Func[] _creatingAsync; - private readonly Func[] _createdAsync; - private readonly Func[] _displayingAsync; - private readonly Func[] _processingAsync; - private readonly Func[] _displayedAsync; + private readonly List _alternationDescriptors = []; + private readonly List _wrappers = []; + private readonly List _bindingSources = []; + private readonly Dictionary _bindings = new(StringComparer.OrdinalIgnoreCase); + private readonly List> _creatingAsync = []; + private readonly List> _createdAsync = []; + private readonly List> _displayingAsync = []; + private readonly List> _processingAsync = []; + private readonly List> _displayedAsync = []; public ShapeDescriptorIndex( string shapeType, @@ -39,46 +37,43 @@ public ShapeDescriptorIndex( ConcurrentDictionary descriptors) { ShapeType = shapeType; - _descriptors = descriptors; - - // pre-calculate as much as we can - _alternationDescriptors = alterationKeys - .Select(key => _descriptors[key]) - .ToArray(); - - _wrappers = _alternationDescriptors - .SelectMany(sd => sd.Wrappers) - .ToArray(); - - _bindingSources = _alternationDescriptors - .SelectMany(sd => sd.BindingSources) - .ToArray(); - - _bindings = _alternationDescriptors - .SelectMany(sd => sd.Bindings) - .GroupBy(kv => kv.Key, StringComparer.OrdinalIgnoreCase) - .Select(kv => kv.Last()) - .ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.OrdinalIgnoreCase); - - _creatingAsync = _alternationDescriptors - .SelectMany(sd => sd.CreatingAsync) - .ToArray(); - - _createdAsync = _alternationDescriptors - .SelectMany(sd => sd.CreatedAsync) - .ToArray(); - - _displayingAsync = _alternationDescriptors - .SelectMany(sd => sd.DisplayingAsync) - .ToArray(); - - _processingAsync = _alternationDescriptors - .SelectMany(sd => sd.ProcessingAsync) - .ToArray(); - - _displayedAsync = _alternationDescriptors - .SelectMany(sd => sd.DisplayedAsync) - .ToArray(); + + // Pre-calculate as much as we can for performance reasons. + foreach (var alterationKey in alterationKeys) + { + if (!descriptors.TryGetValue(alterationKey, out var alternationDescriptor)) + { + continue; + } + + _alternationDescriptors.Add(alternationDescriptor); + + if (alternationDescriptor.Wrappers.Count > 0) + { + _wrappers.AddRange(alternationDescriptor.Wrappers); + } + + if (alternationDescriptor.BindingSources.Count > 0) + { + _bindingSources.AddRange(alternationDescriptor.BindingSources); + } + + if (alternationDescriptor.BindingSources.Count > 0) + { + _bindingSources.AddRange(alternationDescriptor.BindingSources); + } + + foreach (var binding in alternationDescriptor.Bindings) + { + _bindings.TryAdd(binding.Key, binding.Value); + } + + _creatingAsync.AddRange(alternationDescriptor.CreatingAsync); + _createdAsync.AddRange(alternationDescriptor.CreatedAsync); + _displayingAsync.AddRange(alternationDescriptor.DisplayingAsync); + _displayedAsync.AddRange(alternationDescriptor.DisplayedAsync); + _processingAsync.AddRange(alternationDescriptor.ProcessingAsync); + } } /// @@ -108,7 +103,7 @@ public ShapeDescriptorIndex( private PlacementInfo CalculatePlacement(ShapePlacementContext ctx) { PlacementInfo info = null; - for (var i = _alternationDescriptors.Length - 1; i >= 0; i--) + for (var i = _alternationDescriptors.Count - 1; i >= 0; i--) { var descriptor = _alternationDescriptors[i]; info = descriptor.Placement(ctx); From 217d60c8f962cee9b17d1c79b1fa86f3f1d25150 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Mon, 1 Apr 2024 10:16:25 -0700 Subject: [PATCH 3/9] Fix tests --- .../Descriptors/FeatureShapeDescriptor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/FeatureShapeDescriptor.cs b/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/FeatureShapeDescriptor.cs index 3594e162411..dd95d47b325 100644 --- a/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/FeatureShapeDescriptor.cs +++ b/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/FeatureShapeDescriptor.cs @@ -65,7 +65,7 @@ public ShapeDescriptorIndex( foreach (var binding in alternationDescriptor.Bindings) { - _bindings.TryAdd(binding.Key, binding.Value); + _bindings[binding.Key] = binding.Value; } _creatingAsync.AddRange(alternationDescriptor.CreatingAsync); From 5717d955be6c5cbe426980856358829f5ea0a49d Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 2 Apr 2024 08:06:04 -0700 Subject: [PATCH 4/9] Add a null check --- .../Descriptors/FeatureShapeDescriptor.cs | 31 ++++++------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/FeatureShapeDescriptor.cs b/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/FeatureShapeDescriptor.cs index dd95d47b325..2e769d259a0 100644 --- a/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/FeatureShapeDescriptor.cs +++ b/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/FeatureShapeDescriptor.cs @@ -36,6 +36,8 @@ public ShapeDescriptorIndex( IEnumerable alterationKeys, ConcurrentDictionary descriptors) { + ArgumentException.ThrowIfNullOrEmpty(shapeType); + ShapeType = shapeType; // Pre-calculate as much as we can for performance reasons. @@ -47,32 +49,19 @@ public ShapeDescriptorIndex( } _alternationDescriptors.Add(alternationDescriptor); - - if (alternationDescriptor.Wrappers.Count > 0) - { - _wrappers.AddRange(alternationDescriptor.Wrappers); - } - - if (alternationDescriptor.BindingSources.Count > 0) - { - _bindingSources.AddRange(alternationDescriptor.BindingSources); - } - - if (alternationDescriptor.BindingSources.Count > 0) - { - _bindingSources.AddRange(alternationDescriptor.BindingSources); - } - - foreach (var binding in alternationDescriptor.Bindings) - { - _bindings[binding.Key] = binding.Value; - } - + _wrappers.AddRange(alternationDescriptor.Wrappers); + _bindingSources.AddRange(alternationDescriptor.BindingSources); + _bindingSources.AddRange(alternationDescriptor.BindingSources); _creatingAsync.AddRange(alternationDescriptor.CreatingAsync); _createdAsync.AddRange(alternationDescriptor.CreatedAsync); _displayingAsync.AddRange(alternationDescriptor.DisplayingAsync); _displayedAsync.AddRange(alternationDescriptor.DisplayedAsync); _processingAsync.AddRange(alternationDescriptor.ProcessingAsync); + + foreach (var binding in alternationDescriptor.Bindings) + { + _bindings[binding.Key] = binding.Value; + } } } From 6110d10868c66eee3d2e16e3475d7c3729dcb5a2 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 2 Apr 2024 08:06:51 -0700 Subject: [PATCH 5/9] Add a benchmark --- .../ShapeDescriptorIndexBenchmark.cs | 117 +++++++++ .../MultiSelectShapeDescriptorIndex.cs | 226 ++++++++++++++++++ 2 files changed, 343 insertions(+) create mode 100644 test/OrchardCore.Benchmarks/ShapeDescriptorIndexBenchmark.cs create mode 100644 test/OrchardCore.Benchmarks/Support/MultiSelectShapeDescriptorIndex.cs diff --git a/test/OrchardCore.Benchmarks/ShapeDescriptorIndexBenchmark.cs b/test/OrchardCore.Benchmarks/ShapeDescriptorIndexBenchmark.cs new file mode 100644 index 00000000000..1591a575915 --- /dev/null +++ b/test/OrchardCore.Benchmarks/ShapeDescriptorIndexBenchmark.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.DependencyInjection; +using OrchardCore.Benchmark.Support; +using OrchardCore.DisplayManagement.Descriptors; +using OrchardCore.Environment.Extensions; +using OrchardCore.Tests.Apis.Context; + +namespace OrchardCore.Benchmark; + +[MemoryDiagnoser] +public class ShapeDescriptorIndexBenchmark +{ + private readonly ConcurrentDictionary _shapeDescriptors = new(); + + [GlobalSetup] + public async Task SetupAsync() + { + using var _content = new BlogContext(); + await _content.InitializeAsync(); + await _content.UsingTenantScopeAsync(async scope => + { + var bindingStrategies = scope.ServiceProvider.GetRequiredService>(); + var typeFeatureProvider = scope.ServiceProvider.GetRequiredService(); + var shapeDescriptors = new Dictionary(); + + foreach (var bindingStrategy in bindingStrategies) + { + var strategyFeature = typeFeatureProvider.GetFeatureForDependency(bindingStrategy.GetType()); + + var builder = new ShapeTableBuilder(strategyFeature, []); + await bindingStrategy.DiscoverAsync(builder); + + BuildDescriptors(bindingStrategy, builder.BuildAlterations(), shapeDescriptors); + } + }); + } + + [Benchmark(Baseline = true)] + public void SingleLoopLists() + { + var descriptors = _shapeDescriptors + .GroupBy(sd => sd.Value.ShapeType, StringComparer.OrdinalIgnoreCase) + .Select(group => new ShapeDescriptorIndex + ( + shapeType: group.Key, + alterationKeys: group.Select(kv => kv.Key), + descriptors: _shapeDescriptors + )) + .ToList(); + } + + [Benchmark] + public void MultipleLoopsUsingLists() + { + var descriptors = _shapeDescriptors + .GroupBy(sd => sd.Value.ShapeType, StringComparer.OrdinalIgnoreCase) + .Select(group => new MultiSelectShapeDescriptorIndex + ( + shapeType: group.Key, + alterationKeys: group.Select(kv => kv.Key), + descriptors: _shapeDescriptors + )) + .ToList(); + } + + [Benchmark] + public void MultipleLoopsArrays() + { + var descriptors = _shapeDescriptors + .GroupBy(sd => sd.Value.ShapeType, StringComparer.OrdinalIgnoreCase) + .Select(group => new MultiSelectShapeDescriptorIndexArray + ( + shapeType: group.Key, + alterationKeys: group.Select(kv => kv.Key), + descriptors: _shapeDescriptors + )) + .ToList(); + } + + private void BuildDescriptors( + IShapeTableProvider bindingStrategy, + IEnumerable builtAlterations, + Dictionary shapeDescriptors) + { + var alterationSets = builtAlterations.GroupBy(a => a.Feature.Id + a.ShapeType); + + foreach (var alterations in alterationSets) + { + var firstAlteration = alterations.First(); + + var key = bindingStrategy.GetType().Name + + firstAlteration.Feature.Id + + firstAlteration.ShapeType.ToLower(); + + if (!_shapeDescriptors.ContainsKey(key)) + { + var descriptor = new FeatureShapeDescriptor + ( + firstAlteration.Feature, + firstAlteration.ShapeType + ); + + foreach (var alteration in alterations) + { + alteration.Alter(descriptor); + } + + shapeDescriptors[key] = descriptor; + } + } + } +} diff --git a/test/OrchardCore.Benchmarks/Support/MultiSelectShapeDescriptorIndex.cs b/test/OrchardCore.Benchmarks/Support/MultiSelectShapeDescriptorIndex.cs new file mode 100644 index 00000000000..dd394c0e0f4 --- /dev/null +++ b/test/OrchardCore.Benchmarks/Support/MultiSelectShapeDescriptorIndex.cs @@ -0,0 +1,226 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Html; +using OrchardCore.DisplayManagement.Descriptors; +using OrchardCore.DisplayManagement.Implementation; + +namespace OrchardCore.Benchmark.Support; + +public class MultiSelectShapeDescriptorIndex : ShapeDescriptor +{ + private readonly ConcurrentDictionary _descriptors; + private readonly List _alternationDescriptors; + private readonly List _wrappers; + private readonly List _bindingSources; + private readonly Dictionary _bindings; + private readonly List> _creatingAsync; + private readonly List> _createdAsync; + private readonly List> _displayingAsync; + private readonly List> _processingAsync; + private readonly List> _displayedAsync; + + public MultiSelectShapeDescriptorIndex( + string shapeType, + IEnumerable alterationKeys, + ConcurrentDictionary descriptors) + { + ArgumentException.ThrowIfNullOrEmpty(shapeType); + + ShapeType = shapeType; + _descriptors = descriptors; + + // pre-calculate as much as we can + _alternationDescriptors = alterationKeys + .Select(key => _descriptors[key]) + .ToList(); + + _wrappers = _alternationDescriptors + .SelectMany(sd => sd.Wrappers) + .ToList(); + + _bindingSources = _alternationDescriptors + .SelectMany(sd => sd.BindingSources) + .ToList(); + + _bindings = _alternationDescriptors + .SelectMany(sd => sd.Bindings) + .GroupBy(kv => kv.Key, StringComparer.OrdinalIgnoreCase) + .Select(kv => kv.Last()) + .ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.OrdinalIgnoreCase); + + _creatingAsync = _alternationDescriptors + .SelectMany(sd => sd.CreatingAsync) + .ToList(); + + _createdAsync = _alternationDescriptors + .SelectMany(sd => sd.CreatedAsync) + .ToList(); + + _displayingAsync = _alternationDescriptors + .SelectMany(sd => sd.DisplayingAsync) + .ToList(); + + _processingAsync = _alternationDescriptors + .SelectMany(sd => sd.ProcessingAsync) + .ToList(); + + _displayedAsync = _alternationDescriptors + .SelectMany(sd => sd.DisplayedAsync) + .ToList(); + } + + /// + /// The BindingSource is informational text about the source of the Binding delegate. Not used except for + /// troubleshooting. + /// + public override string BindingSource => + Bindings.TryGetValue(ShapeType, out var binding) ? binding.BindingSource : null; + + public override Func> Binding => + Bindings.TryGetValue(ShapeType, out var binding) ? binding.BindingAsync : null; + + public override IDictionary Bindings => _bindings; + + public override IEnumerable> CreatingAsync => _creatingAsync; + + public override IEnumerable> CreatedAsync => _createdAsync; + + public override IEnumerable> DisplayingAsync => _displayingAsync; + + public override IEnumerable> ProcessingAsync => _processingAsync; + + public override IEnumerable> DisplayedAsync => _displayedAsync; + + public override Func Placement => CalculatePlacement; + + private PlacementInfo CalculatePlacement(ShapePlacementContext ctx) + { + PlacementInfo info = null; + for (var i = _alternationDescriptors.Count - 1; i >= 0; i--) + { + var descriptor = _alternationDescriptors[i]; + info = descriptor.Placement(ctx); + if (info != null) + { + break; + } + } + + return info ?? DefaultPlacementAction(ctx); + } + + public override IList Wrappers => _wrappers; + + public override IList BindingSources => _bindingSources; +} + +public class MultiSelectShapeDescriptorIndexArray : ShapeDescriptor +{ + private readonly ConcurrentDictionary _descriptors; + private readonly FeatureShapeDescriptor[] _alternationDescriptors; + private readonly string[] _wrappers; + private readonly string[] _bindingSources; + private readonly Dictionary _bindings; + private readonly Func[] _creatingAsync; + private readonly Func[] _createdAsync; + private readonly Func[] _displayingAsync; + private readonly Func[] _processingAsync; + private readonly Func[] _displayedAsync; + + public MultiSelectShapeDescriptorIndexArray( + string shapeType, + IEnumerable alterationKeys, + ConcurrentDictionary descriptors) + { + ArgumentException.ThrowIfNullOrEmpty(shapeType); + + ShapeType = shapeType; + _descriptors = descriptors; + + // pre-calculate as much as we can + _alternationDescriptors = alterationKeys + .Select(key => _descriptors[key]) + .ToArray(); + + _wrappers = _alternationDescriptors + .SelectMany(sd => sd.Wrappers) + .ToArray(); + + _bindingSources = _alternationDescriptors + .SelectMany(sd => sd.BindingSources) + .ToArray(); + + _bindings = _alternationDescriptors + .SelectMany(sd => sd.Bindings) + .GroupBy(kv => kv.Key, StringComparer.OrdinalIgnoreCase) + .Select(kv => kv.Last()) + .ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.OrdinalIgnoreCase); + + _creatingAsync = _alternationDescriptors + .SelectMany(sd => sd.CreatingAsync) + .ToArray(); + + _createdAsync = _alternationDescriptors + .SelectMany(sd => sd.CreatedAsync) + .ToArray(); + + _displayingAsync = _alternationDescriptors + .SelectMany(sd => sd.DisplayingAsync) + .ToArray(); + + _processingAsync = _alternationDescriptors + .SelectMany(sd => sd.ProcessingAsync) + .ToArray(); + + _displayedAsync = _alternationDescriptors + .SelectMany(sd => sd.DisplayedAsync) + .ToArray(); + } + + /// + /// The BindingSource is informational text about the source of the Binding delegate. Not used except for + /// troubleshooting. + /// + public override string BindingSource => + Bindings.TryGetValue(ShapeType, out var binding) ? binding.BindingSource : null; + + public override Func> Binding => + Bindings.TryGetValue(ShapeType, out var binding) ? binding.BindingAsync : null; + + public override IDictionary Bindings => _bindings; + + public override IEnumerable> CreatingAsync => _creatingAsync; + + public override IEnumerable> CreatedAsync => _createdAsync; + + public override IEnumerable> DisplayingAsync => _displayingAsync; + + public override IEnumerable> ProcessingAsync => _processingAsync; + + public override IEnumerable> DisplayedAsync => _displayedAsync; + + public override Func Placement => CalculatePlacement; + + private PlacementInfo CalculatePlacement(ShapePlacementContext ctx) + { + PlacementInfo info = null; + for (var i = _alternationDescriptors.Length - 1; i >= 0; i--) + { + var descriptor = _alternationDescriptors[i]; + info = descriptor.Placement(ctx); + if (info != null) + { + break; + } + } + + return info ?? DefaultPlacementAction(ctx); + } + + public override IList Wrappers => _wrappers; + + public override IList BindingSources => _bindingSources; +} From c8b686fa9105ea4ada6102953d1db323239e61f9 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 2 Apr 2024 15:33:25 -0700 Subject: [PATCH 6/9] Fix the benchmark --- .../ShapeDescriptorIndexBenchmark.cs | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/test/OrchardCore.Benchmarks/ShapeDescriptorIndexBenchmark.cs b/test/OrchardCore.Benchmarks/ShapeDescriptorIndexBenchmark.cs index 1591a575915..1f836aec7cc 100644 --- a/test/OrchardCore.Benchmarks/ShapeDescriptorIndexBenchmark.cs +++ b/test/OrchardCore.Benchmarks/ShapeDescriptorIndexBenchmark.cs @@ -20,13 +20,12 @@ public class ShapeDescriptorIndexBenchmark [GlobalSetup] public async Task SetupAsync() { - using var _content = new BlogContext(); - await _content.InitializeAsync(); - await _content.UsingTenantScopeAsync(async scope => + using var content = new BlogContext(); + await content.InitializeAsync(); + await content.UsingTenantScopeAsync(async scope => { var bindingStrategies = scope.ServiceProvider.GetRequiredService>(); var typeFeatureProvider = scope.ServiceProvider.GetRequiredService(); - var shapeDescriptors = new Dictionary(); foreach (var bindingStrategy in bindingStrategies) { @@ -35,15 +34,15 @@ await _content.UsingTenantScopeAsync(async scope => var builder = new ShapeTableBuilder(strategyFeature, []); await bindingStrategy.DiscoverAsync(builder); - BuildDescriptors(bindingStrategy, builder.BuildAlterations(), shapeDescriptors); + BuildDescriptors(bindingStrategy, builder.BuildAlterations()); } }); } [Benchmark(Baseline = true)] - public void SingleLoopLists() + public List SingleLoopLists() { - var descriptors = _shapeDescriptors + return _shapeDescriptors .GroupBy(sd => sd.Value.ShapeType, StringComparer.OrdinalIgnoreCase) .Select(group => new ShapeDescriptorIndex ( @@ -55,9 +54,9 @@ public void SingleLoopLists() } [Benchmark] - public void MultipleLoopsUsingLists() + public List MultipleLoopsUsingLists() { - var descriptors = _shapeDescriptors + return _shapeDescriptors .GroupBy(sd => sd.Value.ShapeType, StringComparer.OrdinalIgnoreCase) .Select(group => new MultiSelectShapeDescriptorIndex ( @@ -69,9 +68,9 @@ public void MultipleLoopsUsingLists() } [Benchmark] - public void MultipleLoopsArrays() + public List MultipleLoopsArrays() { - var descriptors = _shapeDescriptors + return _shapeDescriptors .GroupBy(sd => sd.Value.ShapeType, StringComparer.OrdinalIgnoreCase) .Select(group => new MultiSelectShapeDescriptorIndexArray ( @@ -83,9 +82,8 @@ public void MultipleLoopsArrays() } private void BuildDescriptors( - IShapeTableProvider bindingStrategy, - IEnumerable builtAlterations, - Dictionary shapeDescriptors) + IShapeTableProvider bindingStrategy, + IEnumerable builtAlterations) { var alterationSets = builtAlterations.GroupBy(a => a.Feature.Id + a.ShapeType); @@ -110,7 +108,7 @@ private void BuildDescriptors( alteration.Alter(descriptor); } - shapeDescriptors[key] = descriptor; + _shapeDescriptors[key] = descriptor; } } } From 40418723ecc224873f15703725d2221df6378bff Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 2 Apr 2024 15:39:08 -0700 Subject: [PATCH 7/9] Remove benchmarks --- .../ShapeDescriptorIndexBenchmark.cs | 115 --------- .../MultiSelectShapeDescriptorIndex.cs | 226 ------------------ 2 files changed, 341 deletions(-) delete mode 100644 test/OrchardCore.Benchmarks/ShapeDescriptorIndexBenchmark.cs delete mode 100644 test/OrchardCore.Benchmarks/Support/MultiSelectShapeDescriptorIndex.cs diff --git a/test/OrchardCore.Benchmarks/ShapeDescriptorIndexBenchmark.cs b/test/OrchardCore.Benchmarks/ShapeDescriptorIndexBenchmark.cs deleted file mode 100644 index 1f836aec7cc..00000000000 --- a/test/OrchardCore.Benchmarks/ShapeDescriptorIndexBenchmark.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using BenchmarkDotNet.Attributes; -using Microsoft.Extensions.DependencyInjection; -using OrchardCore.Benchmark.Support; -using OrchardCore.DisplayManagement.Descriptors; -using OrchardCore.Environment.Extensions; -using OrchardCore.Tests.Apis.Context; - -namespace OrchardCore.Benchmark; - -[MemoryDiagnoser] -public class ShapeDescriptorIndexBenchmark -{ - private readonly ConcurrentDictionary _shapeDescriptors = new(); - - [GlobalSetup] - public async Task SetupAsync() - { - using var content = new BlogContext(); - await content.InitializeAsync(); - await content.UsingTenantScopeAsync(async scope => - { - var bindingStrategies = scope.ServiceProvider.GetRequiredService>(); - var typeFeatureProvider = scope.ServiceProvider.GetRequiredService(); - - foreach (var bindingStrategy in bindingStrategies) - { - var strategyFeature = typeFeatureProvider.GetFeatureForDependency(bindingStrategy.GetType()); - - var builder = new ShapeTableBuilder(strategyFeature, []); - await bindingStrategy.DiscoverAsync(builder); - - BuildDescriptors(bindingStrategy, builder.BuildAlterations()); - } - }); - } - - [Benchmark(Baseline = true)] - public List SingleLoopLists() - { - return _shapeDescriptors - .GroupBy(sd => sd.Value.ShapeType, StringComparer.OrdinalIgnoreCase) - .Select(group => new ShapeDescriptorIndex - ( - shapeType: group.Key, - alterationKeys: group.Select(kv => kv.Key), - descriptors: _shapeDescriptors - )) - .ToList(); - } - - [Benchmark] - public List MultipleLoopsUsingLists() - { - return _shapeDescriptors - .GroupBy(sd => sd.Value.ShapeType, StringComparer.OrdinalIgnoreCase) - .Select(group => new MultiSelectShapeDescriptorIndex - ( - shapeType: group.Key, - alterationKeys: group.Select(kv => kv.Key), - descriptors: _shapeDescriptors - )) - .ToList(); - } - - [Benchmark] - public List MultipleLoopsArrays() - { - return _shapeDescriptors - .GroupBy(sd => sd.Value.ShapeType, StringComparer.OrdinalIgnoreCase) - .Select(group => new MultiSelectShapeDescriptorIndexArray - ( - shapeType: group.Key, - alterationKeys: group.Select(kv => kv.Key), - descriptors: _shapeDescriptors - )) - .ToList(); - } - - private void BuildDescriptors( - IShapeTableProvider bindingStrategy, - IEnumerable builtAlterations) - { - var alterationSets = builtAlterations.GroupBy(a => a.Feature.Id + a.ShapeType); - - foreach (var alterations in alterationSets) - { - var firstAlteration = alterations.First(); - - var key = bindingStrategy.GetType().Name - + firstAlteration.Feature.Id - + firstAlteration.ShapeType.ToLower(); - - if (!_shapeDescriptors.ContainsKey(key)) - { - var descriptor = new FeatureShapeDescriptor - ( - firstAlteration.Feature, - firstAlteration.ShapeType - ); - - foreach (var alteration in alterations) - { - alteration.Alter(descriptor); - } - - _shapeDescriptors[key] = descriptor; - } - } - } -} diff --git a/test/OrchardCore.Benchmarks/Support/MultiSelectShapeDescriptorIndex.cs b/test/OrchardCore.Benchmarks/Support/MultiSelectShapeDescriptorIndex.cs deleted file mode 100644 index dd394c0e0f4..00000000000 --- a/test/OrchardCore.Benchmarks/Support/MultiSelectShapeDescriptorIndex.cs +++ /dev/null @@ -1,226 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Html; -using OrchardCore.DisplayManagement.Descriptors; -using OrchardCore.DisplayManagement.Implementation; - -namespace OrchardCore.Benchmark.Support; - -public class MultiSelectShapeDescriptorIndex : ShapeDescriptor -{ - private readonly ConcurrentDictionary _descriptors; - private readonly List _alternationDescriptors; - private readonly List _wrappers; - private readonly List _bindingSources; - private readonly Dictionary _bindings; - private readonly List> _creatingAsync; - private readonly List> _createdAsync; - private readonly List> _displayingAsync; - private readonly List> _processingAsync; - private readonly List> _displayedAsync; - - public MultiSelectShapeDescriptorIndex( - string shapeType, - IEnumerable alterationKeys, - ConcurrentDictionary descriptors) - { - ArgumentException.ThrowIfNullOrEmpty(shapeType); - - ShapeType = shapeType; - _descriptors = descriptors; - - // pre-calculate as much as we can - _alternationDescriptors = alterationKeys - .Select(key => _descriptors[key]) - .ToList(); - - _wrappers = _alternationDescriptors - .SelectMany(sd => sd.Wrappers) - .ToList(); - - _bindingSources = _alternationDescriptors - .SelectMany(sd => sd.BindingSources) - .ToList(); - - _bindings = _alternationDescriptors - .SelectMany(sd => sd.Bindings) - .GroupBy(kv => kv.Key, StringComparer.OrdinalIgnoreCase) - .Select(kv => kv.Last()) - .ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.OrdinalIgnoreCase); - - _creatingAsync = _alternationDescriptors - .SelectMany(sd => sd.CreatingAsync) - .ToList(); - - _createdAsync = _alternationDescriptors - .SelectMany(sd => sd.CreatedAsync) - .ToList(); - - _displayingAsync = _alternationDescriptors - .SelectMany(sd => sd.DisplayingAsync) - .ToList(); - - _processingAsync = _alternationDescriptors - .SelectMany(sd => sd.ProcessingAsync) - .ToList(); - - _displayedAsync = _alternationDescriptors - .SelectMany(sd => sd.DisplayedAsync) - .ToList(); - } - - /// - /// The BindingSource is informational text about the source of the Binding delegate. Not used except for - /// troubleshooting. - /// - public override string BindingSource => - Bindings.TryGetValue(ShapeType, out var binding) ? binding.BindingSource : null; - - public override Func> Binding => - Bindings.TryGetValue(ShapeType, out var binding) ? binding.BindingAsync : null; - - public override IDictionary Bindings => _bindings; - - public override IEnumerable> CreatingAsync => _creatingAsync; - - public override IEnumerable> CreatedAsync => _createdAsync; - - public override IEnumerable> DisplayingAsync => _displayingAsync; - - public override IEnumerable> ProcessingAsync => _processingAsync; - - public override IEnumerable> DisplayedAsync => _displayedAsync; - - public override Func Placement => CalculatePlacement; - - private PlacementInfo CalculatePlacement(ShapePlacementContext ctx) - { - PlacementInfo info = null; - for (var i = _alternationDescriptors.Count - 1; i >= 0; i--) - { - var descriptor = _alternationDescriptors[i]; - info = descriptor.Placement(ctx); - if (info != null) - { - break; - } - } - - return info ?? DefaultPlacementAction(ctx); - } - - public override IList Wrappers => _wrappers; - - public override IList BindingSources => _bindingSources; -} - -public class MultiSelectShapeDescriptorIndexArray : ShapeDescriptor -{ - private readonly ConcurrentDictionary _descriptors; - private readonly FeatureShapeDescriptor[] _alternationDescriptors; - private readonly string[] _wrappers; - private readonly string[] _bindingSources; - private readonly Dictionary _bindings; - private readonly Func[] _creatingAsync; - private readonly Func[] _createdAsync; - private readonly Func[] _displayingAsync; - private readonly Func[] _processingAsync; - private readonly Func[] _displayedAsync; - - public MultiSelectShapeDescriptorIndexArray( - string shapeType, - IEnumerable alterationKeys, - ConcurrentDictionary descriptors) - { - ArgumentException.ThrowIfNullOrEmpty(shapeType); - - ShapeType = shapeType; - _descriptors = descriptors; - - // pre-calculate as much as we can - _alternationDescriptors = alterationKeys - .Select(key => _descriptors[key]) - .ToArray(); - - _wrappers = _alternationDescriptors - .SelectMany(sd => sd.Wrappers) - .ToArray(); - - _bindingSources = _alternationDescriptors - .SelectMany(sd => sd.BindingSources) - .ToArray(); - - _bindings = _alternationDescriptors - .SelectMany(sd => sd.Bindings) - .GroupBy(kv => kv.Key, StringComparer.OrdinalIgnoreCase) - .Select(kv => kv.Last()) - .ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.OrdinalIgnoreCase); - - _creatingAsync = _alternationDescriptors - .SelectMany(sd => sd.CreatingAsync) - .ToArray(); - - _createdAsync = _alternationDescriptors - .SelectMany(sd => sd.CreatedAsync) - .ToArray(); - - _displayingAsync = _alternationDescriptors - .SelectMany(sd => sd.DisplayingAsync) - .ToArray(); - - _processingAsync = _alternationDescriptors - .SelectMany(sd => sd.ProcessingAsync) - .ToArray(); - - _displayedAsync = _alternationDescriptors - .SelectMany(sd => sd.DisplayedAsync) - .ToArray(); - } - - /// - /// The BindingSource is informational text about the source of the Binding delegate. Not used except for - /// troubleshooting. - /// - public override string BindingSource => - Bindings.TryGetValue(ShapeType, out var binding) ? binding.BindingSource : null; - - public override Func> Binding => - Bindings.TryGetValue(ShapeType, out var binding) ? binding.BindingAsync : null; - - public override IDictionary Bindings => _bindings; - - public override IEnumerable> CreatingAsync => _creatingAsync; - - public override IEnumerable> CreatedAsync => _createdAsync; - - public override IEnumerable> DisplayingAsync => _displayingAsync; - - public override IEnumerable> ProcessingAsync => _processingAsync; - - public override IEnumerable> DisplayedAsync => _displayedAsync; - - public override Func Placement => CalculatePlacement; - - private PlacementInfo CalculatePlacement(ShapePlacementContext ctx) - { - PlacementInfo info = null; - for (var i = _alternationDescriptors.Length - 1; i >= 0; i--) - { - var descriptor = _alternationDescriptors[i]; - info = descriptor.Placement(ctx); - if (info != null) - { - break; - } - } - - return info ?? DefaultPlacementAction(ctx); - } - - public override IList Wrappers => _wrappers; - - public override IList BindingSources => _bindingSources; -} From 683d9cafb9d2e2e2b583d88040a4299281869440 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Tue, 2 Apr 2024 18:16:07 -0700 Subject: [PATCH 8/9] Update FeatureShapeDescriptor.cs --- .../Descriptors/FeatureShapeDescriptor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/FeatureShapeDescriptor.cs b/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/FeatureShapeDescriptor.cs index 2e769d259a0..a664305d4cd 100644 --- a/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/FeatureShapeDescriptor.cs +++ b/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/FeatureShapeDescriptor.cs @@ -51,7 +51,6 @@ public ShapeDescriptorIndex( _alternationDescriptors.Add(alternationDescriptor); _wrappers.AddRange(alternationDescriptor.Wrappers); _bindingSources.AddRange(alternationDescriptor.BindingSources); - _bindingSources.AddRange(alternationDescriptor.BindingSources); _creatingAsync.AddRange(alternationDescriptor.CreatingAsync); _createdAsync.AddRange(alternationDescriptor.CreatedAsync); _displayingAsync.AddRange(alternationDescriptor.DisplayingAsync); From b59cf409fbf136dcb59c51a513e95ca6041bf8b8 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Wed, 3 Apr 2024 10:37:17 -0700 Subject: [PATCH 9/9] Adding back the benchmarks --- .../ShapeDescriptorIndexBenchmark.cs | 110 +++++++++ .../MultiSelectShapeDescriptorIndex.cs | 226 ++++++++++++++++++ 2 files changed, 336 insertions(+) create mode 100644 test/OrchardCore.Benchmarks/ShapeDescriptorIndexBenchmark.cs create mode 100644 test/OrchardCore.Benchmarks/Support/MultiSelectShapeDescriptorIndex.cs diff --git a/test/OrchardCore.Benchmarks/ShapeDescriptorIndexBenchmark.cs b/test/OrchardCore.Benchmarks/ShapeDescriptorIndexBenchmark.cs new file mode 100644 index 00000000000..35543892307 --- /dev/null +++ b/test/OrchardCore.Benchmarks/ShapeDescriptorIndexBenchmark.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.DependencyInjection; +using OrchardCore.Benchmark.Support; +using OrchardCore.DisplayManagement.Descriptors; +using OrchardCore.Environment.Extensions; +using OrchardCore.Tests.Apis.Context; + +namespace OrchardCore.Benchmark; + +[MemoryDiagnoser] +public class ShapeDescriptorIndexBenchmark +{ + private readonly ConcurrentDictionary _shapeDescriptors = new(); + + [GlobalSetup] + public async Task SetupAsync() + { + using var content = new BlogContext(); + await content.InitializeAsync(); + await content.UsingTenantScopeAsync(async scope => + { + var bindingStrategies = scope.ServiceProvider.GetRequiredService>(); + var typeFeatureProvider = scope.ServiceProvider.GetRequiredService(); + foreach (var bindingStrategy in bindingStrategies) + { + var strategyFeature = typeFeatureProvider.GetFeatureForDependency(bindingStrategy.GetType()); + var builder = new ShapeTableBuilder(strategyFeature, []); + await bindingStrategy.DiscoverAsync(builder); + BuildDescriptors(bindingStrategy, builder.BuildAlterations()); + } + }); + } + + [Benchmark(Baseline = true)] + public List SingleLoopLists() + { + return _shapeDescriptors + .GroupBy(sd => sd.Value.ShapeType, StringComparer.OrdinalIgnoreCase) + .Select(group => new ShapeDescriptorIndex + ( + shapeType: group.Key, + alterationKeys: group.Select(kv => kv.Key), + descriptors: _shapeDescriptors + )) + .ToList(); + } + + [Benchmark] + public List MultipleLoopsUsingLists() + { + return _shapeDescriptors + .GroupBy(sd => sd.Value.ShapeType, StringComparer.OrdinalIgnoreCase) + .Select(group => new MultiSelectShapeDescriptorIndex + ( + shapeType: group.Key, + alterationKeys: group.Select(kv => kv.Key), + descriptors: _shapeDescriptors + )) + .ToList(); + } + + [Benchmark] + public List MultipleLoopsArrays() + { + return _shapeDescriptors + .GroupBy(sd => sd.Value.ShapeType, StringComparer.OrdinalIgnoreCase) + .Select(group => new MultiSelectShapeDescriptorIndexArray + ( + shapeType: group.Key, + alterationKeys: group.Select(kv => kv.Key), + descriptors: _shapeDescriptors + )) + .ToList(); + } + + private void BuildDescriptors(IShapeTableProvider bindingStrategy, IEnumerable builtAlterations) + { + var alterationSets = builtAlterations.GroupBy(a => a.Feature.Id + a.ShapeType); + + foreach (var alterations in alterationSets) + { + var firstAlteration = alterations.First(); + + var key = bindingStrategy.GetType().Name + + firstAlteration.Feature.Id + + firstAlteration.ShapeType.ToLower(); + + if (!_shapeDescriptors.ContainsKey(key)) + { + var descriptor = new FeatureShapeDescriptor + ( + firstAlteration.Feature, + firstAlteration.ShapeType + ); + + foreach (var alteration in alterations) + { + alteration.Alter(descriptor); + } + + _shapeDescriptors[key] = descriptor; + } + } + } +} diff --git a/test/OrchardCore.Benchmarks/Support/MultiSelectShapeDescriptorIndex.cs b/test/OrchardCore.Benchmarks/Support/MultiSelectShapeDescriptorIndex.cs new file mode 100644 index 00000000000..dd394c0e0f4 --- /dev/null +++ b/test/OrchardCore.Benchmarks/Support/MultiSelectShapeDescriptorIndex.cs @@ -0,0 +1,226 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Html; +using OrchardCore.DisplayManagement.Descriptors; +using OrchardCore.DisplayManagement.Implementation; + +namespace OrchardCore.Benchmark.Support; + +public class MultiSelectShapeDescriptorIndex : ShapeDescriptor +{ + private readonly ConcurrentDictionary _descriptors; + private readonly List _alternationDescriptors; + private readonly List _wrappers; + private readonly List _bindingSources; + private readonly Dictionary _bindings; + private readonly List> _creatingAsync; + private readonly List> _createdAsync; + private readonly List> _displayingAsync; + private readonly List> _processingAsync; + private readonly List> _displayedAsync; + + public MultiSelectShapeDescriptorIndex( + string shapeType, + IEnumerable alterationKeys, + ConcurrentDictionary descriptors) + { + ArgumentException.ThrowIfNullOrEmpty(shapeType); + + ShapeType = shapeType; + _descriptors = descriptors; + + // pre-calculate as much as we can + _alternationDescriptors = alterationKeys + .Select(key => _descriptors[key]) + .ToList(); + + _wrappers = _alternationDescriptors + .SelectMany(sd => sd.Wrappers) + .ToList(); + + _bindingSources = _alternationDescriptors + .SelectMany(sd => sd.BindingSources) + .ToList(); + + _bindings = _alternationDescriptors + .SelectMany(sd => sd.Bindings) + .GroupBy(kv => kv.Key, StringComparer.OrdinalIgnoreCase) + .Select(kv => kv.Last()) + .ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.OrdinalIgnoreCase); + + _creatingAsync = _alternationDescriptors + .SelectMany(sd => sd.CreatingAsync) + .ToList(); + + _createdAsync = _alternationDescriptors + .SelectMany(sd => sd.CreatedAsync) + .ToList(); + + _displayingAsync = _alternationDescriptors + .SelectMany(sd => sd.DisplayingAsync) + .ToList(); + + _processingAsync = _alternationDescriptors + .SelectMany(sd => sd.ProcessingAsync) + .ToList(); + + _displayedAsync = _alternationDescriptors + .SelectMany(sd => sd.DisplayedAsync) + .ToList(); + } + + /// + /// The BindingSource is informational text about the source of the Binding delegate. Not used except for + /// troubleshooting. + /// + public override string BindingSource => + Bindings.TryGetValue(ShapeType, out var binding) ? binding.BindingSource : null; + + public override Func> Binding => + Bindings.TryGetValue(ShapeType, out var binding) ? binding.BindingAsync : null; + + public override IDictionary Bindings => _bindings; + + public override IEnumerable> CreatingAsync => _creatingAsync; + + public override IEnumerable> CreatedAsync => _createdAsync; + + public override IEnumerable> DisplayingAsync => _displayingAsync; + + public override IEnumerable> ProcessingAsync => _processingAsync; + + public override IEnumerable> DisplayedAsync => _displayedAsync; + + public override Func Placement => CalculatePlacement; + + private PlacementInfo CalculatePlacement(ShapePlacementContext ctx) + { + PlacementInfo info = null; + for (var i = _alternationDescriptors.Count - 1; i >= 0; i--) + { + var descriptor = _alternationDescriptors[i]; + info = descriptor.Placement(ctx); + if (info != null) + { + break; + } + } + + return info ?? DefaultPlacementAction(ctx); + } + + public override IList Wrappers => _wrappers; + + public override IList BindingSources => _bindingSources; +} + +public class MultiSelectShapeDescriptorIndexArray : ShapeDescriptor +{ + private readonly ConcurrentDictionary _descriptors; + private readonly FeatureShapeDescriptor[] _alternationDescriptors; + private readonly string[] _wrappers; + private readonly string[] _bindingSources; + private readonly Dictionary _bindings; + private readonly Func[] _creatingAsync; + private readonly Func[] _createdAsync; + private readonly Func[] _displayingAsync; + private readonly Func[] _processingAsync; + private readonly Func[] _displayedAsync; + + public MultiSelectShapeDescriptorIndexArray( + string shapeType, + IEnumerable alterationKeys, + ConcurrentDictionary descriptors) + { + ArgumentException.ThrowIfNullOrEmpty(shapeType); + + ShapeType = shapeType; + _descriptors = descriptors; + + // pre-calculate as much as we can + _alternationDescriptors = alterationKeys + .Select(key => _descriptors[key]) + .ToArray(); + + _wrappers = _alternationDescriptors + .SelectMany(sd => sd.Wrappers) + .ToArray(); + + _bindingSources = _alternationDescriptors + .SelectMany(sd => sd.BindingSources) + .ToArray(); + + _bindings = _alternationDescriptors + .SelectMany(sd => sd.Bindings) + .GroupBy(kv => kv.Key, StringComparer.OrdinalIgnoreCase) + .Select(kv => kv.Last()) + .ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.OrdinalIgnoreCase); + + _creatingAsync = _alternationDescriptors + .SelectMany(sd => sd.CreatingAsync) + .ToArray(); + + _createdAsync = _alternationDescriptors + .SelectMany(sd => sd.CreatedAsync) + .ToArray(); + + _displayingAsync = _alternationDescriptors + .SelectMany(sd => sd.DisplayingAsync) + .ToArray(); + + _processingAsync = _alternationDescriptors + .SelectMany(sd => sd.ProcessingAsync) + .ToArray(); + + _displayedAsync = _alternationDescriptors + .SelectMany(sd => sd.DisplayedAsync) + .ToArray(); + } + + /// + /// The BindingSource is informational text about the source of the Binding delegate. Not used except for + /// troubleshooting. + /// + public override string BindingSource => + Bindings.TryGetValue(ShapeType, out var binding) ? binding.BindingSource : null; + + public override Func> Binding => + Bindings.TryGetValue(ShapeType, out var binding) ? binding.BindingAsync : null; + + public override IDictionary Bindings => _bindings; + + public override IEnumerable> CreatingAsync => _creatingAsync; + + public override IEnumerable> CreatedAsync => _createdAsync; + + public override IEnumerable> DisplayingAsync => _displayingAsync; + + public override IEnumerable> ProcessingAsync => _processingAsync; + + public override IEnumerable> DisplayedAsync => _displayedAsync; + + public override Func Placement => CalculatePlacement; + + private PlacementInfo CalculatePlacement(ShapePlacementContext ctx) + { + PlacementInfo info = null; + for (var i = _alternationDescriptors.Length - 1; i >= 0; i--) + { + var descriptor = _alternationDescriptors[i]; + info = descriptor.Placement(ctx); + if (info != null) + { + break; + } + } + + return info ?? DefaultPlacementAction(ctx); + } + + public override IList Wrappers => _wrappers; + + public override IList BindingSources => _bindingSources; +}