From f20981481d197bd475705b1284ff7b991cd7e474 Mon Sep 17 00:00:00 2001 From: Alexandre Giard Date: Mon, 22 Jun 2026 20:34:20 -0400 Subject: [PATCH 1/4] 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 000000000..af3d5a8be --- /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 e1302ab3b896f1dd1b6124c3030c09fab402444e Mon Sep 17 00:00:00 2001 From: Alexandre Giard Date: Mon, 22 Jun 2026 20:42:24 -0400 Subject: [PATCH 2/4] 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 000000000..a27f54961 --- /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 5bf75a416e3e3f7399dbf83ac161e8191f0c02c9 Mon Sep 17 00:00:00 2001 From: Alexandre Giard Date: Mon, 22 Jun 2026 21:42:05 -0400 Subject: [PATCH 3/4] 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 af3d5a8be..b1aacae6d 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 a27f54961..74aa18f4c 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(); From dda390671a06534b97ec41b648f08c183691d8c1 Mon Sep 17 00:00:00 2001 From: Alexandre Giard Date: Thu, 6 Aug 2026 10:53:03 -0400 Subject: [PATCH 4/4] fix: consolidate seed creation --- src/DynamicData.Benchmarks/Cache/Sum_Cache.cs | 36 +++++++++++-- src/DynamicData.Benchmarks/List/Sum_List.cs | 52 ++++++++++++++----- 2 files changed, 71 insertions(+), 17 deletions(-) diff --git a/src/DynamicData.Benchmarks/Cache/Sum_Cache.cs b/src/DynamicData.Benchmarks/Cache/Sum_Cache.cs index b1aacae6d..008192d9b 100644 --- a/src/DynamicData.Benchmarks/Cache/Sum_Cache.cs +++ b/src/DynamicData.Benchmarks/Cache/Sum_Cache.cs @@ -17,6 +17,9 @@ public class Sum_Cache 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; } @@ -40,6 +43,8 @@ public void Setup() } _addChangeSets = addChangeSets; + var addedItems = (Item[])items.Clone(); + var replaceChangeSets = new List>(capacity: Count); for (var id = 1; id <= Count; ++id) { @@ -71,21 +76,39 @@ public void Setup() 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(_addChangeSets); + public void Adds() => Run(seed: null, _addChangeSets); [Benchmark] - public void Replaces() => Run(_replaceChangeSets); + public void Replaces() => Run(_seedAfterAdds, _replaceChangeSets); [Benchmark] - public void Refreshes() => Run(_refreshChangeSets); + public void Refreshes() => Run(_seedAfterReplaces, _refreshChangeSets); [Benchmark] - public void Removes() => Run(_removeChangeSets); + public void Removes() => Run(_seedAfterReplaces, _removeChangeSets); - private static void Run(IReadOnlyList> changeSets) + 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>(); @@ -93,6 +116,9 @@ private static void Run(IReadOnlyList> changeSets) .Sum(static item => item.Value) .Subscribe(); + if (seed is not null) + source.OnNext(seed); + foreach (var changeSet in changeSets) source.OnNext(changeSet); diff --git a/src/DynamicData.Benchmarks/List/Sum_List.cs b/src/DynamicData.Benchmarks/List/Sum_List.cs index 74aa18f4c..87a90c97e 100644 --- a/src/DynamicData.Benchmarks/List/Sum_List.cs +++ b/src/DynamicData.Benchmarks/List/Sum_List.cs @@ -17,6 +17,9 @@ public class Sum_List 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; } @@ -24,27 +27,32 @@ public class Sum_List public void Setup() { var source = new ChangeAwareList(capacity: Count); + var items = new Item[Count]; var addChangeSets = new List>(capacity: Count); - for (var id = 1; id <= Count; ++id) + for (var index = 0; index < Count; ++index) { - source.Add(new Item() + items[index] = new Item() { - Id = id, - Value = id - }); + 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) { - source[index] = new Item() + items[index] = new Item() { Id = index + 1, Value = (index + 1) * 2 }; + source[index] = items[index]; replaceChangeSets.Add(source.CaptureChanges()); } _replaceChangeSets = replaceChangeSets; @@ -53,7 +61,7 @@ public void Setup() for (var index = 0; index < Count; ++index) { // Mutate in place, then refresh - the scenario stateless aggregation cannot currently observe. - source[index].Value += 1; + items[index].Value += 1; source.RefreshAt(index); refreshChangeSets.Add(source.CaptureChanges()); } @@ -66,21 +74,38 @@ public void Setup() 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(_addChangeSets); + public void Adds() => Run(seed: null, _addChangeSets); [Benchmark] - public void Replaces() => Run(_replaceChangeSets); + public void Replaces() => Run(_seedAfterAdds, _replaceChangeSets); [Benchmark] - public void Refreshes() => Run(_refreshChangeSets); + public void Refreshes() => Run(_seedAfterReplaces, _refreshChangeSets); [Benchmark] - public void Removes() => Run(_removeChangeSets); + public void Removes() => Run(_seedAfterReplaces, _removeChangeSets); - private static void Run(IReadOnlyList> changeSets) + 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>(); @@ -88,6 +113,9 @@ private static void Run(IReadOnlyList> changeSets) .Sum(static item => item.Value) .Subscribe(); + if (seed is not null) + source.OnNext(seed); + foreach (var changeSet in changeSets) source.OnNext(changeSet);