From af5fc8504aae1c9388914865dc2a25fdfeb66fbd Mon Sep 17 00:00:00 2001 From: Alexandre Giard Date: Mon, 22 Jun 2026 20:34:20 -0400 Subject: [PATCH 1/3] feat: add Sum operator Cache benchmarks --- src/DynamicData.Benchmarks/Cache/Sum_Cache.cs | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 src/DynamicData.Benchmarks/Cache/Sum_Cache.cs diff --git a/src/DynamicData.Benchmarks/Cache/Sum_Cache.cs b/src/DynamicData.Benchmarks/Cache/Sum_Cache.cs new file mode 100644 index 00000000..af3d5a8b --- /dev/null +++ b/src/DynamicData.Benchmarks/Cache/Sum_Cache.cs @@ -0,0 +1,168 @@ +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 readonly IReadOnlyList> _addChangeSets; + private readonly IReadOnlyList> _replaceChangeSets; + private readonly IReadOnlyList> _removeChangeSets; + private readonly IReadOnlyList> _refreshChangeSets; + + public Sum_Cache() + { + var source = new ChangeAwareCache(capacity: 1_000); + var items = new Item[1_001]; + + var addChangeSets = new List>(capacity: 1_000); + for (var id = 1; id <= 1_000; ++id) + { + var item = new Item() + { + Id = id, + Value = id + }; + items[id] = item; + source.Add(item, key: id); + addChangeSets.Add(source.CaptureChanges()); + } + _addChangeSets = addChangeSets; + + var replaceChangeSets = new List>(capacity: 500); + for (var id = 2; id <= 1_000; id += 2) + { + source.AddOrUpdate( + item: new Item() + { + Id = id, + Value = id * 2 + }, + key: id); + replaceChangeSets.Add(source.CaptureChanges()); + } + _replaceChangeSets = replaceChangeSets; + + var refreshChangeSets = new List>(capacity: 1_000); + for (var id = 1; id <= 1_000; ++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: 1_000); + for (var id = 1; id <= 1_000; ++id) + { + source.Remove(id); + removeChangeSets.Add(source.CaptureChanges()); + } + _removeChangeSets = removeChangeSets; + } + + [Benchmark] + public void Adds() + { + using var source = new Subject>(); + + using var subscription = source + .Sum(static item => item.Value) + .Subscribe(); + + foreach (var changeSet in _addChangeSets) + source.OnNext(changeSet); + + source.OnCompleted(); + } + + [Benchmark] + public void AddsAndReplacements() + { + using var source = new Subject>(); + + using var subscription = source + .Sum(static item => item.Value) + .Subscribe(); + + foreach (var changeSet in _addChangeSets) + source.OnNext(changeSet); + + foreach (var changeSet in _replaceChangeSets) + source.OnNext(changeSet); + + source.OnCompleted(); + } + + [Benchmark] + public void AddsAndRefreshes() + { + using var source = new Subject>(); + + using var subscription = source + .Sum(static item => item.Value) + .Subscribe(); + + foreach (var changeSet in _addChangeSets) + source.OnNext(changeSet); + + foreach (var changeSet in _refreshChangeSets) + source.OnNext(changeSet); + + source.OnCompleted(); + } + + [Benchmark] + public void AddsAndRemoves() + { + using var source = new Subject>(); + + using var subscription = source + .Sum(static item => item.Value) + .Subscribe(); + + foreach (var changeSet in _addChangeSets) + source.OnNext(changeSet); + + foreach (var changeSet in _removeChangeSets) + source.OnNext(changeSet); + + source.OnCompleted(); + } + + [Benchmark] + public void AddsReplacementsAndRemoves() + { + using var source = new Subject>(); + + using var subscription = source + .Sum(static item => item.Value) + .Subscribe(); + + foreach (var changeSet in _addChangeSets) + source.OnNext(changeSet); + + foreach (var changeSet in _replaceChangeSets) + source.OnNext(changeSet); + + foreach (var changeSet in _removeChangeSets) + source.OnNext(changeSet); + + source.OnCompleted(); + } + + private sealed class Item + { + public required int Id { get; init; } + + public int Value { get; set; } + } +} From 56599134092821f46781d25d4a339e3ac4368d5c Mon Sep 17 00:00:00 2001 From: Alexandre Giard Date: Mon, 22 Jun 2026 20:42:24 -0400 Subject: [PATCH 2/3] feat: add Sum operator List benchmarks --- src/DynamicData.Benchmarks/List/Sum_List.cs | 163 ++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 src/DynamicData.Benchmarks/List/Sum_List.cs diff --git a/src/DynamicData.Benchmarks/List/Sum_List.cs b/src/DynamicData.Benchmarks/List/Sum_List.cs new file mode 100644 index 00000000..a27f5496 --- /dev/null +++ b/src/DynamicData.Benchmarks/List/Sum_List.cs @@ -0,0 +1,163 @@ +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 readonly IReadOnlyList> _addChangeSets; + private readonly IReadOnlyList> _replaceChangeSets; + private readonly IReadOnlyList> _removeChangeSets; + private readonly IReadOnlyList> _refreshChangeSets; + + public Sum_List() + { + var source = new ChangeAwareList(capacity: 1_000); + + var addChangeSets = new List>(capacity: 1_000); + for (var id = 1; id <= 1_000; ++id) + { + source.Add(new Item() + { + Id = id, + Value = id + }); + addChangeSets.Add(source.CaptureChanges()); + } + _addChangeSets = addChangeSets; + + var replaceChangeSets = new List>(capacity: 500); + for (var index = 0; index < 1_000; index += 2) + { + source[index] = new Item() + { + Id = index + 1, + Value = (index + 1) * 2 + }; + replaceChangeSets.Add(source.CaptureChanges()); + } + _replaceChangeSets = replaceChangeSets; + + var refreshChangeSets = new List>(capacity: 1_000); + for (var index = 0; index < 1_000; ++index) + { + // Mutate in place, then refresh - the scenario stateless aggregation cannot currently observe. + source[index].Value += 1; + source.RefreshAt(index); + refreshChangeSets.Add(source.CaptureChanges()); + } + _refreshChangeSets = refreshChangeSets; + + var removeChangeSets = new List>(capacity: 1_000); + for (var id = 1; id <= 1_000; ++id) + { + source.RemoveAt(source.Count - 1); + removeChangeSets.Add(source.CaptureChanges()); + } + _removeChangeSets = removeChangeSets; + } + + [Benchmark] + public void Adds() + { + using var source = new Subject>(); + + using var subscription = source + .Sum(static item => item.Value) + .Subscribe(); + + foreach (var changeSet in _addChangeSets) + source.OnNext(changeSet); + + source.OnCompleted(); + } + + [Benchmark] + public void AddsAndReplacements() + { + using var source = new Subject>(); + + using var subscription = source + .Sum(static item => item.Value) + .Subscribe(); + + foreach (var changeSet in _addChangeSets) + source.OnNext(changeSet); + + foreach (var changeSet in _replaceChangeSets) + source.OnNext(changeSet); + + source.OnCompleted(); + } + + [Benchmark] + public void AddsAndRefreshes() + { + using var source = new Subject>(); + + using var subscription = source + .Sum(static item => item.Value) + .Subscribe(); + + foreach (var changeSet in _addChangeSets) + source.OnNext(changeSet); + + foreach (var changeSet in _refreshChangeSets) + source.OnNext(changeSet); + + source.OnCompleted(); + } + + [Benchmark] + public void AddsAndRemoves() + { + using var source = new Subject>(); + + using var subscription = source + .Sum(static item => item.Value) + .Subscribe(); + + foreach (var changeSet in _addChangeSets) + source.OnNext(changeSet); + + foreach (var changeSet in _removeChangeSets) + source.OnNext(changeSet); + + source.OnCompleted(); + } + + [Benchmark] + public void AddsReplacementsAndRemoves() + { + using var source = new Subject>(); + + using var subscription = source + .Sum(static item => item.Value) + .Subscribe(); + + foreach (var changeSet in _addChangeSets) + source.OnNext(changeSet); + + foreach (var changeSet in _replaceChangeSets) + source.OnNext(changeSet); + + foreach (var changeSet in _removeChangeSets) + source.OnNext(changeSet); + + source.OnCompleted(); + } + + private sealed class Item + { + public required int Id { get; init; } + + public int Value { get; set; } + } +} From 39e97cb2bd6631d3e77aea0300dcc15a2dba5064 Mon Sep 17 00:00:00 2001 From: Alexandre Giard Date: Mon, 22 Jun 2026 21:42:05 -0400 Subject: [PATCH 3/3] fix: actually add multiple counts for tests --- src/DynamicData.Benchmarks/Cache/Sum_Cache.cs | 124 +++++------------- src/DynamicData.Benchmarks/List/Sum_List.cs | 108 ++++----------- 2 files changed, 56 insertions(+), 176 deletions(-) diff --git a/src/DynamicData.Benchmarks/Cache/Sum_Cache.cs b/src/DynamicData.Benchmarks/Cache/Sum_Cache.cs index af3d5a8b..b1aacae6 100644 --- a/src/DynamicData.Benchmarks/Cache/Sum_Cache.cs +++ b/src/DynamicData.Benchmarks/Cache/Sum_Cache.cs @@ -12,18 +12,22 @@ namespace DynamicData.Benchmarks.Cache; [MarkdownExporterAttribute.GitHub] public class Sum_Cache { - private readonly IReadOnlyList> _addChangeSets; - private readonly IReadOnlyList> _replaceChangeSets; - private readonly IReadOnlyList> _removeChangeSets; - private readonly IReadOnlyList> _refreshChangeSets; + private IReadOnlyList> _addChangeSets = null!; + private IReadOnlyList> _replaceChangeSets = null!; + private IReadOnlyList> _removeChangeSets = null!; + private IReadOnlyList> _refreshChangeSets = null!; - public Sum_Cache() + [Params(100, 500, 1_000, 10_000)] + public int Count { get; set; } + + [GlobalSetup] + public void Setup() { - var source = new ChangeAwareCache(capacity: 1_000); - var items = new Item[1_001]; + var source = new ChangeAwareCache(capacity: Count); + var items = new Item[Count + 1]; - var addChangeSets = new List>(capacity: 1_000); - for (var id = 1; id <= 1_000; ++id) + var addChangeSets = new List>(capacity: Count); + for (var id = 1; id <= Count; ++id) { var item = new Item() { @@ -36,22 +40,22 @@ public Sum_Cache() } _addChangeSets = addChangeSets; - var replaceChangeSets = new List>(capacity: 500); - for (var id = 2; id <= 1_000; id += 2) + var replaceChangeSets = new List>(capacity: Count); + for (var id = 1; id <= Count; ++id) { - source.AddOrUpdate( - item: new Item() - { - Id = id, - Value = id * 2 - }, - key: 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: 1_000); - for (var id = 1; id <= 1_000; ++id) + 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; @@ -60,8 +64,8 @@ public Sum_Cache() } _refreshChangeSets = refreshChangeSets; - var removeChangeSets = new List>(capacity: 1_000); - for (var id = 1; id <= 1_000; ++id) + var removeChangeSets = new List>(capacity: Count); + for (var id = 1; id <= Count; ++id) { source.Remove(id); removeChangeSets.Add(source.CaptureChanges()); @@ -70,76 +74,18 @@ public Sum_Cache() } [Benchmark] - public void Adds() - { - using var source = new Subject>(); - - using var subscription = source - .Sum(static item => item.Value) - .Subscribe(); - - foreach (var changeSet in _addChangeSets) - source.OnNext(changeSet); - - source.OnCompleted(); - } + public void Adds() => Run(_addChangeSets); [Benchmark] - public void AddsAndReplacements() - { - using var source = new Subject>(); - - using var subscription = source - .Sum(static item => item.Value) - .Subscribe(); - - foreach (var changeSet in _addChangeSets) - source.OnNext(changeSet); - - foreach (var changeSet in _replaceChangeSets) - source.OnNext(changeSet); - - source.OnCompleted(); - } + public void Replaces() => Run(_replaceChangeSets); [Benchmark] - public void AddsAndRefreshes() - { - using var source = new Subject>(); - - using var subscription = source - .Sum(static item => item.Value) - .Subscribe(); - - foreach (var changeSet in _addChangeSets) - source.OnNext(changeSet); - - foreach (var changeSet in _refreshChangeSets) - source.OnNext(changeSet); - - source.OnCompleted(); - } + public void Refreshes() => Run(_refreshChangeSets); [Benchmark] - public void AddsAndRemoves() - { - using var source = new Subject>(); + public void Removes() => Run(_removeChangeSets); - using var subscription = source - .Sum(static item => item.Value) - .Subscribe(); - - foreach (var changeSet in _addChangeSets) - source.OnNext(changeSet); - - foreach (var changeSet in _removeChangeSets) - source.OnNext(changeSet); - - source.OnCompleted(); - } - - [Benchmark] - public void AddsReplacementsAndRemoves() + private static void Run(IReadOnlyList> changeSets) { using var source = new Subject>(); @@ -147,13 +93,7 @@ public void AddsReplacementsAndRemoves() .Sum(static item => item.Value) .Subscribe(); - foreach (var changeSet in _addChangeSets) - source.OnNext(changeSet); - - foreach (var changeSet in _replaceChangeSets) - source.OnNext(changeSet); - - foreach (var changeSet in _removeChangeSets) + foreach (var changeSet in changeSets) source.OnNext(changeSet); source.OnCompleted(); diff --git a/src/DynamicData.Benchmarks/List/Sum_List.cs b/src/DynamicData.Benchmarks/List/Sum_List.cs index a27f5496..74aa18f4 100644 --- a/src/DynamicData.Benchmarks/List/Sum_List.cs +++ b/src/DynamicData.Benchmarks/List/Sum_List.cs @@ -12,17 +12,21 @@ namespace DynamicData.Benchmarks.List; [MarkdownExporterAttribute.GitHub] public class Sum_List { - private readonly IReadOnlyList> _addChangeSets; - private readonly IReadOnlyList> _replaceChangeSets; - private readonly IReadOnlyList> _removeChangeSets; - private readonly IReadOnlyList> _refreshChangeSets; + private IReadOnlyList> _addChangeSets = null!; + private IReadOnlyList> _replaceChangeSets = null!; + private IReadOnlyList> _removeChangeSets = null!; + private IReadOnlyList> _refreshChangeSets = null!; - public Sum_List() + [Params(100, 500, 1_000, 10_000)] + public int Count { get; set; } + + [GlobalSetup] + public void Setup() { - var source = new ChangeAwareList(capacity: 1_000); + var source = new ChangeAwareList(capacity: Count); - var addChangeSets = new List>(capacity: 1_000); - for (var id = 1; id <= 1_000; ++id) + var addChangeSets = new List>(capacity: Count); + for (var id = 1; id <= Count; ++id) { source.Add(new Item() { @@ -33,8 +37,8 @@ public Sum_List() } _addChangeSets = addChangeSets; - var replaceChangeSets = new List>(capacity: 500); - for (var index = 0; index < 1_000; index += 2) + var replaceChangeSets = new List>(capacity: Count); + for (var index = 0; index < Count; ++index) { source[index] = new Item() { @@ -45,8 +49,8 @@ public Sum_List() } _replaceChangeSets = replaceChangeSets; - var refreshChangeSets = new List>(capacity: 1_000); - for (var index = 0; index < 1_000; ++index) + 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. source[index].Value += 1; @@ -55,8 +59,8 @@ public Sum_List() } _refreshChangeSets = refreshChangeSets; - var removeChangeSets = new List>(capacity: 1_000); - for (var id = 1; id <= 1_000; ++id) + var removeChangeSets = new List>(capacity: Count); + for (var id = 1; id <= Count; ++id) { source.RemoveAt(source.Count - 1); removeChangeSets.Add(source.CaptureChanges()); @@ -65,76 +69,18 @@ public Sum_List() } [Benchmark] - public void Adds() - { - using var source = new Subject>(); - - using var subscription = source - .Sum(static item => item.Value) - .Subscribe(); - - foreach (var changeSet in _addChangeSets) - source.OnNext(changeSet); - - source.OnCompleted(); - } + public void Adds() => Run(_addChangeSets); [Benchmark] - public void AddsAndReplacements() - { - using var source = new Subject>(); - - using var subscription = source - .Sum(static item => item.Value) - .Subscribe(); - - foreach (var changeSet in _addChangeSets) - source.OnNext(changeSet); - - foreach (var changeSet in _replaceChangeSets) - source.OnNext(changeSet); - - source.OnCompleted(); - } + public void Replaces() => Run(_replaceChangeSets); [Benchmark] - public void AddsAndRefreshes() - { - using var source = new Subject>(); - - using var subscription = source - .Sum(static item => item.Value) - .Subscribe(); - - foreach (var changeSet in _addChangeSets) - source.OnNext(changeSet); - - foreach (var changeSet in _refreshChangeSets) - source.OnNext(changeSet); - - source.OnCompleted(); - } + public void Refreshes() => Run(_refreshChangeSets); [Benchmark] - public void AddsAndRemoves() - { - using var source = new Subject>(); - - using var subscription = source - .Sum(static item => item.Value) - .Subscribe(); - - foreach (var changeSet in _addChangeSets) - source.OnNext(changeSet); - - foreach (var changeSet in _removeChangeSets) - source.OnNext(changeSet); - - source.OnCompleted(); - } + public void Removes() => Run(_removeChangeSets); - [Benchmark] - public void AddsReplacementsAndRemoves() + private static void Run(IReadOnlyList> changeSets) { using var source = new Subject>(); @@ -142,13 +88,7 @@ public void AddsReplacementsAndRemoves() .Sum(static item => item.Value) .Subscribe(); - foreach (var changeSet in _addChangeSets) - source.OnNext(changeSet); - - foreach (var changeSet in _replaceChangeSets) - source.OnNext(changeSet); - - foreach (var changeSet in _removeChangeSets) + foreach (var changeSet in changeSets) source.OnNext(changeSet); source.OnCompleted();