From e734087a9da1565c33a899100a7c191f609c89f8 Mon Sep 17 00:00:00 2001 From: Jake Meiergerd Date: Thu, 7 Dec 2023 07:16:24 -0600 Subject: [PATCH] Benchmarks Enhancements and Organization (#785) * Fixed broken setup for displaying Benchmark artifacts in Solution Explorer. * Moved performance tests in the .Tests project over to the .Benchmarks project. * Fix Nullable errors in Benchmarks --------- Co-authored-by: Roland Pheasant Co-authored-by: Chris Pulman --- src/DynamicData.Benchmarks/Cache/EditDiff.cs | 76 +++++++++++++++++++ .../Cache/SourceCache.cs | 8 +- .../Cache/TransformMany.cs | 75 ++++++++++++++++++ .../DynamicData.Benchmarks.csproj | 3 +- src/DynamicData.Benchmarks/List/GroupAdd.cs | 12 +-- .../List/GroupRemove.cs | 18 ++--- src/DynamicData.Benchmarks/List/SourceList.cs | 10 +-- src/DynamicData.Benchmarks/Program.cs | 15 +++- .../Cache/EditDiffChangeSetFixture.cs | 37 --------- .../Cache/EditDiffChangeSetOptionalFixture.cs | 22 ------ .../TransformManyObservableCacheFixture.cs | 34 +-------- 11 files changed, 192 insertions(+), 118 deletions(-) create mode 100644 src/DynamicData.Benchmarks/Cache/EditDiff.cs create mode 100644 src/DynamicData.Benchmarks/Cache/TransformMany.cs diff --git a/src/DynamicData.Benchmarks/Cache/EditDiff.cs b/src/DynamicData.Benchmarks/Cache/EditDiff.cs new file mode 100644 index 000000000..a6b4fd53d --- /dev/null +++ b/src/DynamicData.Benchmarks/Cache/EditDiff.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; + +using BenchmarkDotNet.Attributes; + +using DynamicData.Kernel; + +namespace DynamicData.Benchmarks.Cache; + +[MemoryDiagnoser] +[MarkdownExporterAttribute.GitHub] +public class EditDiff +{ + public const int MaxItems + = 1097; + + [Benchmark] + [Arguments(7, 3, 5)] + [Arguments(233, 113, MaxItems)] + [Arguments(233, 0, MaxItems)] + [Arguments(233, 233, MaxItems)] + [Arguments(2521, 1187, MaxItems)] + [Arguments(2521, 0, MaxItems)] + [Arguments(2521, 2521, MaxItems)] + [Arguments(5081, 2683, MaxItems)] + [Arguments(5081, 0, MaxItems)] + [Arguments(5081, 5081, MaxItems)] + public void AddsRemovesAndUpdates(int collectionSize, int updateSize, int maxItems) + { + using var subscription = Enumerable + .Range(1, maxItems - 1) + .Select(n => n * (collectionSize - updateSize)) + .Select(index => Person.CreateRange(index, updateSize, "Overlap") + .Concat(Person.CreateRange(index + updateSize, collectionSize - updateSize, "Name"))) + .Prepend(Person.CreateRange(0, collectionSize, "Name")) + .ToObservable() + .EditDiff(p => p.Id) + .Subscribe(); + } + + [Benchmark] + [Arguments(7)] + [Arguments(MaxItems)] + public void OptionalAddsAndRemoves(int maxItems) + { + using var subscription = Enumerable + .Range(0, MaxItems) + .Select(n => (n % 2) == 0 + ? new Person(n, "Name") + : Optional.None()) + .ToObservable() + .EditDiff(p => p.Id) + .Subscribe(); + } + + private class Person + { + public static IReadOnlyList CreateRange(int baseId, int count, string baseName) + => Enumerable + .Range(baseId, count) + .Select(i => new Person(i, baseName + i)) + .ToArray(); + + public Person(int id, string name) + { + Id = id; + Name = name; + } + + public int Id { get; } + + public string Name { get; } + } +} diff --git a/src/DynamicData.Benchmarks/Cache/SourceCache.cs b/src/DynamicData.Benchmarks/Cache/SourceCache.cs index ecf4265ab..f2157221d 100644 --- a/src/DynamicData.Benchmarks/Cache/SourceCache.cs +++ b/src/DynamicData.Benchmarks/Cache/SourceCache.cs @@ -24,7 +24,7 @@ public BenchmarkItem(int id) [MarkdownExporterAttribute.GitHub] public class SourceCache { - private SourceCache _cache; + private SourceCache? _cache; private BenchmarkItem[] _items = Enumerable.Range(1, 100).Select(i => new BenchmarkItem(i)).ToArray(); [GlobalSetup] @@ -39,18 +39,18 @@ public void Setup() [IterationSetup] public void SetupIteration() { - _cache.Clear(); + _cache?.Clear(); _items = Enumerable.Range(1, N).Select(i => new BenchmarkItem(i)).ToArray(); } [GlobalCleanup] public void Teardown() { - _cache.Dispose(); + _cache?.Dispose(); _cache = null; } [Benchmark] - public void Add() => _cache.AddOrUpdate(_items); + public void Add() => _cache?.AddOrUpdate(_items); } } diff --git a/src/DynamicData.Benchmarks/Cache/TransformMany.cs b/src/DynamicData.Benchmarks/Cache/TransformMany.cs new file mode 100644 index 000000000..5b91eaa41 --- /dev/null +++ b/src/DynamicData.Benchmarks/Cache/TransformMany.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; + +using BenchmarkDotNet.Attributes; + +namespace DynamicData.Benchmarks.Cache; + +[MemoryDiagnoser] +[MarkdownExporterAttribute.GitHub] +public class TransformMany +{ + [Benchmark] + public void Perf() + { + var children = Enumerable + .Range(1, 10000) + .Select(i => new Child( + id: i, + name: $"Child #{i}")) + .ToArray(); + + var childIndex = 0; + var parents = Enumerable + .Range(1, 5000) + .Select(i => new Parent( + id: i, + children: new[] + { + children[childIndex++], + children[childIndex++] + })) + .ToArray(); + + using var source = new SourceCache(x => x.Id); + + using var subscription = source + .Connect() + .TransformMany(p => p.Children, c => c.Name) + .Subscribe(); + + source.AddOrUpdate(parents); + } + + private class Parent + { + public Parent( + int id, + IEnumerable children) + { + Id = id; + Children = children.ToArray(); + } + + public int Id { get; } + + public IReadOnlyList Children { get; } + } + + private class Child + { + public Child( + int id, + string name) + { + Id = id; + Name = name; + } + + public int Id { get; } + + public string Name { get; } + } +} diff --git a/src/DynamicData.Benchmarks/DynamicData.Benchmarks.csproj b/src/DynamicData.Benchmarks/DynamicData.Benchmarks.csproj index 1dfdca743..a7c1f9389 100644 --- a/src/DynamicData.Benchmarks/DynamicData.Benchmarks.csproj +++ b/src/DynamicData.Benchmarks/DynamicData.Benchmarks.csproj @@ -4,6 +4,7 @@ Exe net6.0-windows AnyCPU + enable false ;1591;1701;1702;1705;CA1822;CA1001 @@ -19,6 +20,6 @@ - + diff --git a/src/DynamicData.Benchmarks/List/GroupAdd.cs b/src/DynamicData.Benchmarks/List/GroupAdd.cs index eb013336e..92572496e 100644 --- a/src/DynamicData.Benchmarks/List/GroupAdd.cs +++ b/src/DynamicData.Benchmarks/List/GroupAdd.cs @@ -15,8 +15,8 @@ namespace DynamicData.Benchmarks.List [MarkdownExporterAttribute.GitHub] public class GroupAdd { - private IDisposable _groupSubscription; - private SourceList _sourceList; + private IDisposable? _groupSubscription; + private SourceList? _sourceList; private int[] _items = Enumerable.Range(1, 100).ToArray(); [Params(1, 100, 1_000, 10_000, 100_000)] @@ -32,15 +32,15 @@ public void Setup() [IterationSetup] public void SetupIteration() { - _sourceList.Clear(); + _sourceList?.Clear(); _items = Enumerable.Range(1, N).ToArray(); } [GlobalCleanup] public void Teardown() { - _groupSubscription.Dispose(); - _sourceList.Dispose(); + _groupSubscription?.Dispose(); + _sourceList?.Dispose(); _sourceList = null; } @@ -52,6 +52,6 @@ public void Teardown() //} [Benchmark] - public void AddRange() => _sourceList.AddRange(_items); + public void AddRange() => _sourceList?.AddRange(_items); } } diff --git a/src/DynamicData.Benchmarks/List/GroupRemove.cs b/src/DynamicData.Benchmarks/List/GroupRemove.cs index c362b903b..d4ba95e05 100644 --- a/src/DynamicData.Benchmarks/List/GroupRemove.cs +++ b/src/DynamicData.Benchmarks/List/GroupRemove.cs @@ -15,8 +15,8 @@ namespace DynamicData.Benchmarks.List [MarkdownExporterAttribute.GitHub] public class GroupRemove { - private IDisposable _groupSubscription; - private SourceList _sourceList; + private IDisposable? _groupSubscription; + private SourceList? _sourceList; private static readonly int[] _items = Enumerable.Range(1, 100).ToArray(); @@ -30,27 +30,27 @@ public void Setup() [IterationSetup] public void SetupIteration() { - _sourceList.AddRange(_items); + _sourceList?.AddRange(_items); } [GlobalCleanup] public void Teardown() { - _groupSubscription.Dispose(); - _sourceList.Dispose(); + _groupSubscription?.Dispose(); + _sourceList?.Dispose(); _sourceList = null; } [Benchmark] - public void RemoveAt() => _sourceList.RemoveAt(1); + public void RemoveAt() => _sourceList?.RemoveAt(1); [Benchmark] - public void Remove() => _sourceList.RemoveAt(_items[0]); + public void Remove() => _sourceList?.RemoveAt(_items[0]); [Benchmark] - public void RemoveRange() => _sourceList.RemoveRange(40, 20); + public void RemoveRange() => _sourceList?.RemoveRange(40, 20); [Benchmark] - public void Clear() => _sourceList.Clear(); + public void Clear() => _sourceList?.Clear(); } } \ No newline at end of file diff --git a/src/DynamicData.Benchmarks/List/SourceList.cs b/src/DynamicData.Benchmarks/List/SourceList.cs index 2ed4100d1..6556d6ecf 100644 --- a/src/DynamicData.Benchmarks/List/SourceList.cs +++ b/src/DynamicData.Benchmarks/List/SourceList.cs @@ -14,8 +14,8 @@ namespace DynamicData.Benchmarks.List [MarkdownExporterAttribute.GitHub] public class SourceList { - private SourceList _sourceList; - private string[] _items; + private SourceList? _sourceList; + private string[]? _items; [Params(1, 100, 1_000, 10_000, 100_000)] public int N; @@ -26,7 +26,7 @@ public class SourceList [IterationSetup] public void SetupIteration() { - _sourceList.Clear(); + _sourceList?.Clear(); _items = Enumerable.Range(1, N).Select(i => i.ToString()).ToArray(); } @@ -34,9 +34,9 @@ public void SetupIteration() public void Teardown() => _sourceList = null; [Benchmark] - public void AddRange() => _sourceList.AddRange(_items); + public void AddRange() => _sourceList?.AddRange(_items!); [Benchmark] - public void Insert() => _sourceList.InsertRange(_items, 0); + public void Insert() => _sourceList?.InsertRange(_items!, 0); } } \ No newline at end of file diff --git a/src/DynamicData.Benchmarks/Program.cs b/src/DynamicData.Benchmarks/Program.cs index 89167b06c..0bb88df89 100644 --- a/src/DynamicData.Benchmarks/Program.cs +++ b/src/DynamicData.Benchmarks/Program.cs @@ -2,12 +2,25 @@ // Roland Pheasant licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. +using System.IO; +using System.Runtime.CompilerServices; + +using BenchmarkDotNet.Configs; using BenchmarkDotNet.Running; namespace DynamicData.Benchmarks { public static class Program { - public static void Main(string[] args) => BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); + public static void Main(string[] args) + => BenchmarkSwitcher + .FromAssembly(typeof(Program).Assembly) + .Run(args, DefaultConfig.Instance + .WithArtifactsPath(Path.Combine( + GetProjectRootDirectory(), + Path.GetFileName(DefaultConfig.Instance.ArtifactsPath)))); + + private static string GetProjectRootDirectory([CallerFilePath] string? callerFilePath = null) + => Path.GetDirectoryName(callerFilePath)!; } } diff --git a/src/DynamicData.Tests/Cache/EditDiffChangeSetFixture.cs b/src/DynamicData.Tests/Cache/EditDiffChangeSetFixture.cs index a1acb56ff..d178c9ea6 100644 --- a/src/DynamicData.Tests/Cache/EditDiffChangeSetFixture.cs +++ b/src/DynamicData.Tests/Cache/EditDiffChangeSetFixture.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Reactive.Linq; using FluentAssertions; @@ -118,42 +117,6 @@ public void ResultFailsIfAndOnlyIfSourceFails (bool failSource) receivedError.Should().Be(failSource ? testException : default); } - [Trait("Performance", "Manual run only")] - [Theory] - [InlineData(7, 3, 5)] - [InlineData(233, 113, MaxItems)] - [InlineData(233, 0, MaxItems)] - [InlineData(233, 233, MaxItems)] - [InlineData(2521, 1187, MaxItems)] - [InlineData(2521, 0, MaxItems)] - [InlineData(2521, 2521, MaxItems)] - [InlineData(5081, 2683, MaxItems)] - [InlineData(5081, 0, MaxItems)] - [InlineData(5081, 5081, MaxItems)] - public void Perf(int collectionSize, int updateSize, int maxItems) - { - Debug.Assert(updateSize <= collectionSize); - - // having - var enumerables = Enumerable.Range(1, maxItems - 1) - .Select(n => n * (collectionSize - updateSize)) - .Select(index => CreatePeople(index, updateSize, "Overlap") - .Concat(CreatePeople(index + updateSize, collectionSize - updateSize, "Name"))) - .Prepend(CreatePeople(0, collectionSize, "Name")); - var enumObservable = enumerables.ToObservable(); - - // when - var observableChangeSet = enumObservable.EditDiff(p => p.Id); - using var results = observableChangeSet.AsAggregator(); - - // then - results.Data.Count.Should().Be(collectionSize); - results.Messages.Count.Should().Be(maxItems); - results.Summary.Overall.Adds.Should().Be((collectionSize * maxItems) - (updateSize * (maxItems - 1))); - results.Summary.Overall.Removes.Should().Be((collectionSize - updateSize) * (maxItems - 1)); - results.Summary.Overall.Updates.Should().Be(updateSize * (maxItems - 1)); - } - private static Person CreatePerson(int id, string name) => new(id, name); private static IEnumerable CreatePeople(int baseId, int count, string baseName) => diff --git a/src/DynamicData.Tests/Cache/EditDiffChangeSetOptionalFixture.cs b/src/DynamicData.Tests/Cache/EditDiffChangeSetOptionalFixture.cs index d87313af0..1aa24f68d 100644 --- a/src/DynamicData.Tests/Cache/EditDiffChangeSetOptionalFixture.cs +++ b/src/DynamicData.Tests/Cache/EditDiffChangeSetOptionalFixture.cs @@ -184,28 +184,6 @@ public void ResultFailsIfAndOnlyIfSourceFails (bool failSource) receivedError.Should().Be(failSource ? testException : default); } - [Trait("Performance", "Manual run only")] - [Theory] - [InlineData(7)] - [InlineData(MaxItems)] - public void Perf(int maxItems) - { - // having - var optionals = Enumerable.Range(0, maxItems).Select(n => (n % 2) == 0 ? CreatePerson(n, "Name") : s_noPerson); - var optObservable = optionals.ToObservable(); - - // when - var observableChangeSet = optObservable.EditDiff(p => p.Id); - using var results = observableChangeSet.AsAggregator(); - - // then - results.Data.Count.Should().Be(1); - results.Messages.Count.Should().Be(maxItems); - results.Summary.Overall.Adds.Should().Be((maxItems / 2) + ((maxItems % 2) == 0 ? 0 : 1)); - results.Summary.Overall.Removes.Should().Be(maxItems / 2); - results.Summary.Overall.Updates.Should().Be(0); - } - private static Optional CreatePerson(int id, string name) => Optional.Some(new Person(id, name)); private class PersonComparer : IEqualityComparer diff --git a/src/DynamicData.Tests/Cache/TransformManyObservableCacheFixture.cs b/src/DynamicData.Tests/Cache/TransformManyObservableCacheFixture.cs index 1e497d259..a15d1c051 100644 --- a/src/DynamicData.Tests/Cache/TransformManyObservableCacheFixture.cs +++ b/src/DynamicData.Tests/Cache/TransformManyObservableCacheFixture.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics; using System.Linq; using System.Reactive.Linq; using DynamicData.Binding; @@ -186,36 +184,6 @@ public void ObservableCollectionWithoutInitialData() collection.Count.Should().Be(2); } - [Fact] - [Trait("Performance", "Manual run only")] - public void Perf() - { - var children = Enumerable.Range(1, 10000).Select(i => new Person("Name" + i, i)).ToArray(); - - var childIndex = 0; - var parents = Enumerable.Range(1, 5000).Select( - i => - { - var parent = new Parent( - i, - new[] - { - children[childIndex], - children[childIndex + 1] - }); - - childIndex += 2; - return parent; - }).ToArray(); - - var sw = new Stopwatch(); - - using var source = new SourceCache(x => x.Id); - using var sut = source.Connect().Do(_ => sw.Start()).TransformMany(p => p.Children, c => c.Name).Do(_ => sw.Stop()).Subscribe(c => Console.WriteLine($"Changes = {c.Count:N0}")); - source.AddOrUpdate(parents); - Console.WriteLine($"{sw.ElapsedMilliseconds}"); - } - [Fact] public void ReadOnlyObservableCollectionWithoutInitialData() {