Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions src/DynamicData.Benchmarks/Cache/Sum_Cache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class Sum_Cache

private IChangeSet<Item, int> _seedAfterAdds = null!;
private IChangeSet<Item, int> _seedAfterReplaces = null!;
private IChangeSet<Item, int> _seedAfterRefreshes = null!;

[Params(100, 500, 1_000, 10_000)]
public int Count { get; set; }
Expand All @@ -45,6 +46,11 @@ public void Setup()

var addedItems = (Item[])items.Clone();

// Each non-add benchmark only forms a valid sequence for a stateful operator after it has seen
// the preceding population. Seed snapshots are collapsed into one change set so their cost stays
// outside the measured sequence as far as possible.
_seedAfterAdds = BuildSeed(addedItems);

var replaceChangeSets = new List<IChangeSet<Item, int>>(capacity: Count);
for (var id = 1; id <= Count; ++id)
{
Expand All @@ -58,6 +64,7 @@ public void Setup()
replaceChangeSets.Add(source.CaptureChanges());
}
_replaceChangeSets = replaceChangeSets;
_seedAfterReplaces = BuildSeed(items);

var refreshChangeSets = new List<IChangeSet<Item, int>>(capacity: Count);
for (var id = 1; id <= Count; ++id)
Expand All @@ -68,6 +75,7 @@ public void Setup()
refreshChangeSets.Add(source.CaptureChanges());
}
_refreshChangeSets = refreshChangeSets;
_seedAfterRefreshes = BuildSeed(items);

var removeChangeSets = new List<IChangeSet<Item, int>>(capacity: Count);
for (var id = 1; id <= Count; ++id)
Expand All @@ -76,14 +84,6 @@ 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]
Expand All @@ -96,14 +96,18 @@ public void Setup()
public void Refreshes() => Run(_seedAfterReplaces, _refreshChangeSets);

[Benchmark]
public void Removes() => Run(_seedAfterReplaces, _removeChangeSets);
public void Removes() => Run(_seedAfterRefreshes, _removeChangeSets);

private static IChangeSet<Item, int> BuildSeed(Item[] items)
{
var seed = new ChangeAwareCache<Item, int>(capacity: items.Length - 1);

for (var id = 1; id < items.Length; ++id)
seed.Add(items[id], key: id);
seed.Add(new Item()
{
Id = items[id].Id,
Value = items[id].Value
}, key: id);

return seed.CaptureChanges();
}
Expand Down
27 changes: 17 additions & 10 deletions src/DynamicData.Benchmarks/List/Sum_List.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class Sum_List

private IChangeSet<Item> _seedAfterAdds = null!;
private IChangeSet<Item> _seedAfterReplaces = null!;
private IChangeSet<Item> _seedAfterRefreshes = null!;

[Params(100, 500, 1_000, 10_000)]
public int Count { get; set; }
Expand All @@ -44,6 +45,11 @@ public void Setup()

var addedItems = (Item[])items.Clone();

// Each non-add benchmark only forms a valid sequence for a stateful operator after it has seen
// the preceding population. Seed snapshots are collapsed into one change set so their cost stays
// outside the measured sequence as far as possible.
_seedAfterAdds = BuildSeed(addedItems);

var replaceChangeSets = new List<IChangeSet<Item>>(capacity: Count);
for (var index = 0; index < Count; ++index)
{
Expand All @@ -56,6 +62,7 @@ public void Setup()
replaceChangeSets.Add(source.CaptureChanges());
}
_replaceChangeSets = replaceChangeSets;
_seedAfterReplaces = BuildSeed(items);

var refreshChangeSets = new List<IChangeSet<Item>>(capacity: Count);
for (var index = 0; index < Count; ++index)
Expand All @@ -66,6 +73,7 @@ public void Setup()
refreshChangeSets.Add(source.CaptureChanges());
}
_refreshChangeSets = refreshChangeSets;
_seedAfterRefreshes = BuildSeed(items);

var removeChangeSets = new List<IChangeSet<Item>>(capacity: Count);
for (var id = 1; id <= Count; ++id)
Expand All @@ -74,14 +82,6 @@ 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]
Expand All @@ -94,13 +94,20 @@ public void Setup()
public void Refreshes() => Run(_seedAfterReplaces, _refreshChangeSets);

[Benchmark]
public void Removes() => Run(_seedAfterReplaces, _removeChangeSets);
public void Removes() => Run(_seedAfterRefreshes, _removeChangeSets);

private static IChangeSet<Item> BuildSeed(Item[] items)
{
var seed = new ChangeAwareList<Item>(capacity: items.Length);

seed.AddRange(items);
foreach (var item in items)
{
seed.Add(new Item()
{
Id = item.Id,
Value = item.Value
});
}

return seed.CaptureChanges();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,56 @@ namespace DynamicData.Aggregation
public static System.IObservable<long> Sum<TObject, TKey>(this System.IObservable<DynamicData.IChangeSet<TObject, TKey>> source, System.Func<TObject, long?> valueSelector)
where TObject : notnull
where TKey : notnull { }
public static System.IObservable<decimal> SumImmutable<T>(this System.IObservable<DynamicData.IChangeSet<T>> source, System.Func<T, decimal> valueSelector)
where T : notnull { }
public static System.IObservable<decimal> SumImmutable<T>(this System.IObservable<DynamicData.IChangeSet<T>> source, System.Func<T, decimal?> valueSelector)
where T : notnull { }
public static System.IObservable<double> SumImmutable<T>(this System.IObservable<DynamicData.IChangeSet<T>> source, System.Func<T, double> valueSelector)
where T : notnull { }
public static System.IObservable<double> SumImmutable<T>(this System.IObservable<DynamicData.IChangeSet<T>> source, System.Func<T, double?> valueSelector)
where T : notnull { }
public static System.IObservable<float> SumImmutable<T>(this System.IObservable<DynamicData.IChangeSet<T>> source, System.Func<T, float> valueSelector)
where T : notnull { }
public static System.IObservable<float> SumImmutable<T>(this System.IObservable<DynamicData.IChangeSet<T>> source, System.Func<T, float?> valueSelector)
where T : notnull { }
public static System.IObservable<int> SumImmutable<T>(this System.IObservable<DynamicData.IChangeSet<T>> source, System.Func<T, int> valueSelector)
where T : notnull { }
public static System.IObservable<int> SumImmutable<T>(this System.IObservable<DynamicData.IChangeSet<T>> source, System.Func<T, int?> valueSelector)
where T : notnull { }
public static System.IObservable<long> SumImmutable<T>(this System.IObservable<DynamicData.IChangeSet<T>> source, System.Func<T, long> valueSelector)
where T : notnull { }
public static System.IObservable<long> SumImmutable<T>(this System.IObservable<DynamicData.IChangeSet<T>> source, System.Func<T, long?> valueSelector)
where T : notnull { }
public static System.IObservable<decimal> SumImmutable<TObject, TKey>(this System.IObservable<DynamicData.IChangeSet<TObject, TKey>> source, System.Func<TObject, decimal> valueSelector)
where TObject : notnull
where TKey : notnull { }
public static System.IObservable<decimal> SumImmutable<TObject, TKey>(this System.IObservable<DynamicData.IChangeSet<TObject, TKey>> source, System.Func<TObject, decimal?> valueSelector)
where TObject : notnull
where TKey : notnull { }
public static System.IObservable<double> SumImmutable<TObject, TKey>(this System.IObservable<DynamicData.IChangeSet<TObject, TKey>> source, System.Func<TObject, double> valueSelector)
where TObject : notnull
where TKey : notnull { }
public static System.IObservable<double> SumImmutable<TObject, TKey>(this System.IObservable<DynamicData.IChangeSet<TObject, TKey>> source, System.Func<TObject, double?> valueSelector)
where TObject : notnull
where TKey : notnull { }
public static System.IObservable<float> SumImmutable<TObject, TKey>(this System.IObservable<DynamicData.IChangeSet<TObject, TKey>> source, System.Func<TObject, float> valueSelector)
where TObject : notnull
where TKey : notnull { }
public static System.IObservable<float> SumImmutable<TObject, TKey>(this System.IObservable<DynamicData.IChangeSet<TObject, TKey>> source, System.Func<TObject, float?> valueSelector)
where TObject : notnull
where TKey : notnull { }
public static System.IObservable<int> SumImmutable<TObject, TKey>(this System.IObservable<DynamicData.IChangeSet<TObject, TKey>> source, System.Func<TObject, int> valueSelector)
where TObject : notnull
where TKey : notnull { }
public static System.IObservable<int> SumImmutable<TObject, TKey>(this System.IObservable<DynamicData.IChangeSet<TObject, TKey>> source, System.Func<TObject, int?> valueSelector)
where TObject : notnull
where TKey : notnull { }
public static System.IObservable<long> SumImmutable<TObject, TKey>(this System.IObservable<DynamicData.IChangeSet<TObject, TKey>> source, System.Func<TObject, long> valueSelector)
where TObject : notnull
where TKey : notnull { }
public static System.IObservable<long> SumImmutable<TObject, TKey>(this System.IObservable<DynamicData.IChangeSet<TObject, TKey>> source, System.Func<TObject, long?> valueSelector)
where TObject : notnull
where TKey : notnull { }
}
}
namespace DynamicData.Alias
Expand Down
58 changes: 58 additions & 0 deletions src/DynamicData.Tests/AggregationTests/SumFixture.ForCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,64 @@ public void NullableValuesAreTreatedAsZero()
.Which.Should().Be(40, "null values should be treated as zero, so the sum should be 10 + 0 + 30 = 40");
}

[Fact]
public void ItemIsRefreshed_SumReflectsMutatedValue()
{
using var source = new TestSourceCache<Person, string>(p => p.Name);
var person = new Person("A", 10);

source.AddOrUpdate(person);

using var subscription = source.Connect()
.Sum(p => p.Age)
.RecordValues(out var results);

person.Age = 40;
source.Refresh(person);

results.RecordedValues.Should().Equal(10, 40);

source.Remove(person.Name);

results.RecordedValues[^1].Should().Be(0, "removal should subtract the value captured by the refresh");
}

[Fact]
public void NullableItemIsRefreshed_SumReflectsMutatedValue()
{
using var source = new TestSourceCache<Person, string>(p => p.Name);
var person = new Person("A", new int?(10));

source.AddOrUpdate(person);

using var subscription = source.Connect()
.Sum(p => p.AgeNullable)
.RecordValues(out var results);

person.AgeNullable = null;
source.Refresh(person);

results.RecordedValues.Should().Equal(10, 0);
}

[Fact]
public void ItemIsRefreshed_SumImmutableDoesNotReevaluateMutatedValue()
{
using var source = new TestSourceCache<Person, string>(p => p.Name);
var person = new Person("A", 10);

source.AddOrUpdate(person);

using var subscription = source.Connect()
.SumImmutable(p => p.Age)
.RecordValues(out var results);

person.Age = 40;
source.Refresh(person);

results.RecordedValues.Should().Equal(10, 10);
}

[Theory]
[InlineData(new[] { 10, 20, 30 }, 60)]
[InlineData(new[] { int.MaxValue }, int.MaxValue)]
Expand Down
61 changes: 61 additions & 0 deletions src/DynamicData.Tests/AggregationTests/SumFixture.ForList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Linq;

using DynamicData.Aggregation;
using DynamicData.Tests.Domain;
using DynamicData.Tests.Utilities;

using FluentAssertions;
Expand Down Expand Up @@ -265,6 +266,66 @@ public void SourceFailsImmediately_ErrorPropagates()
results.HasCompleted.Should().BeFalse("an error is not a completion");
}

[Fact]
public void ItemIsRefreshed_SumReflectsMutatedValue()
{
using var source = new TestSourceList<Person>();
var person = new Person("A", 10);

source.Add(person);

using var subscription = source.Connect()
.Sum(p => p.Age)
.RecordValues(out var results);

person.Age = 40;
source.Refresh(0);

results.RecordedValues.Should().Equal(10, 40);

source.RemoveAt(0);

results.RecordedValues[^1].Should().Be(0, "removal should subtract the value captured by the refresh");
}

[Fact]
public void ItemIsMoved_RefreshUsesItsNewIndex()
{
using var source = new TestSourceList<Person>();
var first = new Person("A", 10);
var second = new Person("B", 20);

source.AddRange(new[] { first, second });

using var subscription = source.Connect()
.Sum(p => p.Age)
.RecordValues(out var results);

source.Move(1, 0);
second.Age = 50;
source.Refresh(0);

results.RecordedValues.Should().Equal(30, 30, 60);
}

[Fact]
public void ItemIsRefreshed_SumImmutableDoesNotReevaluateMutatedValue()
{
using var source = new TestSourceList<Person>();
var person = new Person("A", 10);

source.Add(person);

using var subscription = source.Connect()
.SumImmutable(p => p.Age)
.RecordValues(out var results);

person.Age = 40;
source.Refresh(0);

results.RecordedValues.Should().Equal(10, 10);
}

[Theory]
[InlineData(new[] { 10, 20, 30 }, 60)]
[InlineData(new[] { int.MaxValue }, int.MaxValue)]
Expand Down
Loading
Loading