diff --git a/src/DynamicData.Benchmarks/Cache/Sum_Cache.cs b/src/DynamicData.Benchmarks/Cache/Sum_Cache.cs new file mode 100644 index 000000000..008192d9b --- /dev/null +++ b/src/DynamicData.Benchmarks/Cache/Sum_Cache.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Subjects; + +using BenchmarkDotNet.Attributes; + +using DynamicData.Aggregation; + +namespace DynamicData.Benchmarks.Cache; + +[MemoryDiagnoser] +[MarkdownExporterAttribute.GitHub] +public class Sum_Cache +{ + private IReadOnlyList> _addChangeSets = null!; + private IReadOnlyList> _replaceChangeSets = null!; + private IReadOnlyList> _removeChangeSets = null!; + private IReadOnlyList> _refreshChangeSets = null!; + + private IChangeSet _seedAfterAdds = null!; + private IChangeSet _seedAfterReplaces = null!; + + [Params(100, 500, 1_000, 10_000)] + public int Count { get; set; } + + [GlobalSetup] + public void Setup() + { + var source = new ChangeAwareCache(capacity: Count); + var items = new Item[Count + 1]; + + var addChangeSets = new List>(capacity: Count); + for (var id = 1; id <= Count; ++id) + { + var item = new Item() + { + Id = id, + Value = id + }; + items[id] = item; + source.Add(item, key: id); + addChangeSets.Add(source.CaptureChanges()); + } + _addChangeSets = addChangeSets; + + var addedItems = (Item[])items.Clone(); + + var replaceChangeSets = new List>(capacity: Count); + for (var id = 1; id <= Count; ++id) + { + var replacement = new Item() + { + Id = id, + Value = id * 2 + }; + items[id] = replacement; + source.AddOrUpdate(replacement, key: id); + replaceChangeSets.Add(source.CaptureChanges()); + } + _replaceChangeSets = replaceChangeSets; + + var refreshChangeSets = new List>(capacity: Count); + for (var id = 1; id <= Count; ++id) + { + // Mutate in place, then refresh - the scenario stateless aggregation cannot currently observe. + items[id].Value += 1; + source.Refresh(id); + refreshChangeSets.Add(source.CaptureChanges()); + } + _refreshChangeSets = refreshChangeSets; + + var removeChangeSets = new List>(capacity: Count); + for (var id = 1; id <= Count; ++id) + { + source.Remove(id); + removeChangeSets.Add(source.CaptureChanges()); + } + _removeChangeSets = removeChangeSets; + + // Replaces, refreshes, and removes only form a valid sequence for an operator that has already + // seen the items they refer to, so each of those runs gets seeded with the population as it stood + // beforehand. Collapsing the seed into a single change set keeps its cost off the measurement as + // far as possible: replaces follow on from the items that were added, while refreshes and removes + // follow on from the items that replaced them. + _seedAfterAdds = BuildSeed(addedItems); + _seedAfterReplaces = BuildSeed(items); + } + + [Benchmark] + public void Adds() => Run(seed: null, _addChangeSets); + + [Benchmark] + public void Replaces() => Run(_seedAfterAdds, _replaceChangeSets); + + [Benchmark] + public void Refreshes() => Run(_seedAfterReplaces, _refreshChangeSets); + + [Benchmark] + public void Removes() => Run(_seedAfterReplaces, _removeChangeSets); + + private static IChangeSet BuildSeed(Item[] items) + { + var seed = new ChangeAwareCache(capacity: items.Length - 1); + + for (var id = 1; id < items.Length; ++id) + seed.Add(items[id], key: id); + + return seed.CaptureChanges(); + } + + private static void Run(IChangeSet? seed, IReadOnlyList> changeSets) + { + using var source = new Subject>(); + + using var subscription = source + .Sum(static item => item.Value) + .Subscribe(); + + if (seed is not null) + source.OnNext(seed); + + foreach (var changeSet in changeSets) + source.OnNext(changeSet); + + source.OnCompleted(); + } + + private sealed class Item + { + public required int Id { get; init; } + + public int Value { get; set; } + } +} diff --git a/src/DynamicData.Benchmarks/List/Sum_List.cs b/src/DynamicData.Benchmarks/List/Sum_List.cs new file mode 100644 index 000000000..87a90c97e --- /dev/null +++ b/src/DynamicData.Benchmarks/List/Sum_List.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Subjects; + +using BenchmarkDotNet.Attributes; + +using DynamicData.Aggregation; + +namespace DynamicData.Benchmarks.List; + +[MemoryDiagnoser] +[MarkdownExporterAttribute.GitHub] +public class Sum_List +{ + private IReadOnlyList> _addChangeSets = null!; + private IReadOnlyList> _replaceChangeSets = null!; + private IReadOnlyList> _removeChangeSets = null!; + private IReadOnlyList> _refreshChangeSets = null!; + + private IChangeSet _seedAfterAdds = null!; + private IChangeSet _seedAfterReplaces = null!; + + [Params(100, 500, 1_000, 10_000)] + public int Count { get; set; } + + [GlobalSetup] + public void Setup() + { + var source = new ChangeAwareList(capacity: Count); + var items = new Item[Count]; + + var addChangeSets = new List>(capacity: Count); + for (var index = 0; index < Count; ++index) + { + items[index] = new Item() + { + Id = index + 1, + Value = index + 1 + }; + source.Add(items[index]); + addChangeSets.Add(source.CaptureChanges()); + } + _addChangeSets = addChangeSets; + + var addedItems = (Item[])items.Clone(); + + var replaceChangeSets = new List>(capacity: Count); + for (var index = 0; index < Count; ++index) + { + items[index] = new Item() + { + Id = index + 1, + Value = (index + 1) * 2 + }; + source[index] = items[index]; + replaceChangeSets.Add(source.CaptureChanges()); + } + _replaceChangeSets = replaceChangeSets; + + var refreshChangeSets = new List>(capacity: Count); + for (var index = 0; index < Count; ++index) + { + // Mutate in place, then refresh - the scenario stateless aggregation cannot currently observe. + items[index].Value += 1; + source.RefreshAt(index); + refreshChangeSets.Add(source.CaptureChanges()); + } + _refreshChangeSets = refreshChangeSets; + + var removeChangeSets = new List>(capacity: Count); + for (var id = 1; id <= Count; ++id) + { + source.RemoveAt(source.Count - 1); + removeChangeSets.Add(source.CaptureChanges()); + } + _removeChangeSets = removeChangeSets; + + // Replaces, refreshes, and removes only form a valid sequence for an operator that has already + // seen the items they refer to, so each of those runs gets seeded with the population as it stood + // beforehand. Collapsing the seed into a single change set keeps its cost off the measurement as + // far as possible: replaces follow on from the items that were added, while refreshes and removes + // follow on from the items that replaced them. + _seedAfterAdds = BuildSeed(addedItems); + _seedAfterReplaces = BuildSeed(items); + } + + [Benchmark] + public void Adds() => Run(seed: null, _addChangeSets); + + [Benchmark] + public void Replaces() => Run(_seedAfterAdds, _replaceChangeSets); + + [Benchmark] + public void Refreshes() => Run(_seedAfterReplaces, _refreshChangeSets); + + [Benchmark] + public void Removes() => Run(_seedAfterReplaces, _removeChangeSets); + + private static IChangeSet BuildSeed(Item[] items) + { + var seed = new ChangeAwareList(capacity: items.Length); + + seed.AddRange(items); + + return seed.CaptureChanges(); + } + + private static void Run(IChangeSet? seed, IReadOnlyList> changeSets) + { + using var source = new Subject>(); + + using var subscription = source + .Sum(static item => item.Value) + .Subscribe(); + + if (seed is not null) + source.OnNext(seed); + + foreach (var changeSet in changeSets) + source.OnNext(changeSet); + + source.OnCompleted(); + } + + private sealed class Item + { + public required int Id { get; init; } + + public int Value { get; set; } + } +}