Skip to content

Commit 37ffe9d

Browse files
authored
Added new Filter operators that utilize a predicate state stream, to help avoid unneccessary allocations of a new filter predicate delegate, every time the consumer desires to change filtering logic. (#941)
1 parent dd1f54d commit 37ffe9d

14 files changed

+3987
-19
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
using System;
2+
using System.Collections.Immutable;
3+
using System.Linq;
4+
using System.Reactive.Subjects;
5+
6+
using BenchmarkDotNet.Attributes;
7+
8+
using Bogus;
9+
10+
namespace DynamicData.Benchmarks.Cache;
11+
12+
[MemoryDiagnoser]
13+
[MarkdownExporterAttribute.GitHub]
14+
public class Filter_Cache_WithPredicateState
15+
{
16+
public Filter_Cache_WithPredicateState()
17+
{
18+
// Not exercising Moved, since ChangeAwareCache<> doesn't support it, and I'm too lazy to implement it by hand.
19+
var changeReasons = new[]
20+
{
21+
ChangeReason.Add,
22+
ChangeReason.Refresh,
23+
ChangeReason.Remove,
24+
ChangeReason.Update
25+
};
26+
27+
// Weights are chosen to make the cache size likely to grow over time,
28+
// exerting more pressure on the system the longer the benchmark runs.
29+
// Also, to prevent bogus operations (E.G. you can't remove an item from an empty cache).
30+
var changeReasonWeightsWhenCountIs0 = new[]
31+
{
32+
1f, // Add
33+
0f, // Refresh
34+
0f, // Remove
35+
0f // Update
36+
};
37+
38+
var changeReasonWeightsOtherwise = new[]
39+
{
40+
0.30f, // Add
41+
0.25f, // Refresh
42+
0.20f, // Remove
43+
0.25f // Update
44+
};
45+
46+
var maxChangeCount = 20;
47+
48+
var randomizer = new Randomizer(0x1234567);
49+
50+
var changeSets = ImmutableArray.CreateBuilder<IChangeSet<Item, int>>(initialCapacity: 5_000);
51+
var nextItemId = 1;
52+
var items = new ChangeAwareCache<Item, int>();
53+
while (changeSets.Count < changeSets.Capacity)
54+
{
55+
var changeCount = randomizer.Int(1, maxChangeCount);
56+
for (var i = 0; i < changeCount; ++i)
57+
{
58+
var changeReason = randomizer.WeightedRandom(changeReasons, items.Count switch
59+
{
60+
0 => changeReasonWeightsWhenCountIs0,
61+
_ => changeReasonWeightsOtherwise
62+
});
63+
64+
switch (changeReason)
65+
{
66+
case ChangeReason.Add:
67+
items.AddOrUpdate(
68+
item: new Item()
69+
{
70+
Id = nextItemId,
71+
IsIncluded = randomizer.Bool()
72+
},
73+
key: nextItemId);
74+
++nextItemId;
75+
break;
76+
77+
case ChangeReason.Refresh:
78+
items.Refresh(items.Keys.ElementAt(randomizer.Int(0, items.Count - 1)));
79+
break;
80+
81+
case ChangeReason.Remove:
82+
items.Remove(items.Keys.ElementAt(randomizer.Int(0, items.Count - 1)));
83+
break;
84+
85+
case ChangeReason.Update:
86+
var id = items.Keys.ElementAt(randomizer.Int(0, items.Count - 1));
87+
items.AddOrUpdate(
88+
item: new Item()
89+
{
90+
Id = id,
91+
IsIncluded = randomizer.Bool()
92+
},
93+
key: id);
94+
break;
95+
}
96+
}
97+
98+
changeSets.Add(items.CaptureChanges());
99+
}
100+
_changeSets = changeSets.MoveToImmutable();
101+
102+
103+
var predicateStates = ImmutableArray.CreateBuilder<int>(initialCapacity: 5_000);
104+
while (predicateStates.Count < predicateStates.Capacity)
105+
predicateStates.Add(randomizer.Int());
106+
_predicateStates = predicateStates.MoveToImmutable();
107+
}
108+
109+
[Benchmark(Baseline = true)]
110+
public void RandomizedEditsAndStateChanges()
111+
{
112+
using var source = new Subject<IChangeSet<Item, int>>();
113+
using var predicateState = new Subject<int>();
114+
115+
using var subscription = source
116+
.Filter(
117+
predicateState: predicateState,
118+
predicate: Item.FilterByIdInclusionMask)
119+
.Subscribe();
120+
121+
PublishNotifications(source, predicateState);
122+
123+
subscription.Dispose();
124+
}
125+
126+
private void PublishNotifications(
127+
IObserver<IChangeSet<Item, int>> source,
128+
IObserver<int> predicateState)
129+
{
130+
int i;
131+
for (i = 0; (i < _changeSets.Length) && (i < _predicateStates.Length); ++i)
132+
{
133+
source.OnNext(_changeSets[i]);
134+
predicateState.OnNext(_predicateStates[i]);
135+
}
136+
137+
for (; i < _changeSets.Length; ++i)
138+
source.OnNext(_changeSets[i]);
139+
140+
for (; i < _predicateStates.Length; ++i)
141+
predicateState.OnNext(_predicateStates[i]);
142+
}
143+
144+
private readonly ImmutableArray<IChangeSet<Item, int>> _changeSets;
145+
private readonly ImmutableArray<int> _predicateStates;
146+
147+
public class Item
148+
{
149+
public static bool FilterByIdInclusionMask(
150+
int idInclusionMask,
151+
Item item)
152+
=> ((item.Id & idInclusionMask) == 0) && item.IsIncluded;
153+
154+
public required int Id { get; init; }
155+
156+
public bool IsIncluded { get; set; }
157+
158+
public override string ToString()
159+
=> $"{{ Id = {Id}, IsIncluded = {IsIncluded} }}";
160+
}
161+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
using System;
2+
using System.Collections.Immutable;
3+
using System.Linq;
4+
using System.Reactive.Subjects;
5+
6+
using BenchmarkDotNet.Attributes;
7+
8+
using Bogus;
9+
10+
namespace DynamicData.Benchmarks.List;
11+
12+
[MemoryDiagnoser]
13+
[MarkdownExporterAttribute.GitHub]
14+
public class Filter_List_WithPredicateState
15+
{
16+
public Filter_List_WithPredicateState()
17+
{
18+
var randomizer = new Randomizer(0x1234567);
19+
20+
_changeSets = GenerateStressItemsAndChangeSets(
21+
editCount: 5_000,
22+
maxChangeCount: 20,
23+
maxRangeSize: 10,
24+
randomizer: randomizer);
25+
26+
_predicateStates = GenerateRandomPredicateStates(
27+
valueCount: 5_000,
28+
randomizer: randomizer);
29+
}
30+
31+
[Params(ListFilterPolicy.CalculateDiff, ListFilterPolicy.ClearAndReplace)]
32+
public ListFilterPolicy FilterPolicy { get; set; }
33+
34+
[Benchmark(Baseline = true)]
35+
public void RandomizedEditsAndStateChanges()
36+
{
37+
using var source = new Subject<IChangeSet<Item>>();
38+
using var predicateState = new Subject<int>();
39+
40+
using var subscription = source
41+
.Filter(
42+
predicateState: predicateState,
43+
predicate: Item.FilterByIdInclusionMask,
44+
filterPolicy: FilterPolicy)
45+
.Subscribe();
46+
47+
PublishNotifications(source, predicateState);
48+
49+
subscription.Dispose();
50+
}
51+
52+
private static ImmutableArray<IChangeSet<Item>> GenerateStressItemsAndChangeSets(
53+
int editCount,
54+
int maxChangeCount,
55+
int maxRangeSize,
56+
Randomizer randomizer)
57+
{
58+
var changeReasons = new[]
59+
{
60+
ListChangeReason.Add,
61+
ListChangeReason.AddRange,
62+
ListChangeReason.Clear,
63+
ListChangeReason.Moved,
64+
ListChangeReason.Refresh,
65+
ListChangeReason.Remove,
66+
ListChangeReason.RemoveRange,
67+
ListChangeReason.Replace
68+
};
69+
70+
// Weights are chosen to make the cache size likely to grow over time,
71+
// exerting more pressure on the system the longer the benchmark runs.
72+
// Also, to prevent bogus operations (E.G. you can't remove an item from an empty cache).
73+
var changeReasonWeightsWhenCountIs0 = new[]
74+
{
75+
0.5f, // Add
76+
0.5f, // AddRange
77+
0.0f, // Clear
78+
0.0f, // Moved
79+
0.0f, // Refresh
80+
0.0f, // Remove
81+
0.0f, // RemoveRange
82+
0.0f // Replace
83+
};
84+
85+
var changeReasonWeightsWhenCountIs1 = new[]
86+
{
87+
0.400f, // Add
88+
0.400f, // AddRange
89+
0.001f, // Clear
90+
0.000f, // Moved
91+
0.000f, // Refresh
92+
0.199f, // Remove
93+
0.000f, // RemoveRange
94+
0.000f // Replace
95+
};
96+
97+
var changeReasonWeightsOtherwise = new[]
98+
{
99+
0.250f, // Add
100+
0.250f, // AddRange
101+
0.001f, // Clear
102+
0.100f, // Moved
103+
0.099f, // Refresh
104+
0.100f, // Remove
105+
0.100f, // RemoveRange
106+
0.100f // Replace
107+
};
108+
109+
var nextItemId = 1;
110+
111+
var changeSets = ImmutableArray.CreateBuilder<IChangeSet<Item>>(initialCapacity: editCount);
112+
113+
var items = new ChangeAwareList<Item>();
114+
115+
while (changeSets.Count < changeSets.Capacity)
116+
{
117+
var changeCount = randomizer.Int(1, maxChangeCount);
118+
for (var i = 0; i < changeCount; ++i)
119+
{
120+
var changeReason = randomizer.WeightedRandom(changeReasons, items.Count switch
121+
{
122+
0 => changeReasonWeightsWhenCountIs0,
123+
1 => changeReasonWeightsWhenCountIs1,
124+
_ => changeReasonWeightsOtherwise
125+
});
126+
127+
switch (changeReason)
128+
{
129+
case ListChangeReason.Add:
130+
items.Add(new Item()
131+
{
132+
Id = nextItemId++,
133+
IsIncluded = randomizer.Bool()
134+
});
135+
break;
136+
137+
case ListChangeReason.AddRange:
138+
items.AddRange(Enumerable.Repeat(0, randomizer.Int(1, maxRangeSize))
139+
.Select(_ => new Item()
140+
{
141+
Id = nextItemId++,
142+
IsIncluded = randomizer.Bool()
143+
}));
144+
break;
145+
146+
case ListChangeReason.Clear:
147+
items.Clear();
148+
break;
149+
150+
case ListChangeReason.Moved:
151+
items.Move(
152+
original: randomizer.Int(0, items.Count - 1),
153+
destination: randomizer.Int(0, items.Count - 1));
154+
break;
155+
156+
case ListChangeReason.Refresh:
157+
items.RefreshAt(randomizer.Int(0, items.Count - 1));
158+
break;
159+
160+
case ListChangeReason.Remove:
161+
items.RemoveAt(randomizer.Int(0, items.Count - 1));
162+
break;
163+
164+
case ListChangeReason.RemoveRange:
165+
{
166+
var rangeStartIndex = randomizer.Int(0, items.Count - 1);
167+
168+
items.RemoveRange(
169+
index: rangeStartIndex,
170+
count: Math.Min(items.Count - rangeStartIndex, randomizer.Int(1, maxRangeSize)));
171+
}
172+
break;
173+
174+
case ListChangeReason.Replace:
175+
items[randomizer.Int(0, items.Count - 1)] = new Item()
176+
{
177+
Id = nextItemId++,
178+
IsIncluded = randomizer.Bool()
179+
};
180+
break;
181+
}
182+
}
183+
184+
changeSets.Add(items.CaptureChanges());
185+
}
186+
187+
return changeSets.MoveToImmutable();
188+
}
189+
190+
private static ImmutableArray<int> GenerateRandomPredicateStates(
191+
int valueCount,
192+
Randomizer randomizer)
193+
{
194+
var values = ImmutableArray.CreateBuilder<int>(initialCapacity: valueCount);
195+
196+
while (values.Count < valueCount)
197+
values.Add(randomizer.Int());
198+
199+
return values.MoveToImmutable();
200+
}
201+
202+
private void PublishNotifications(
203+
IObserver<IChangeSet<Item>> source,
204+
IObserver<int> predicateState)
205+
{
206+
int i;
207+
for (i = 0; (i < _changeSets.Length) && (i < _predicateStates.Length); ++i)
208+
{
209+
source.OnNext(_changeSets[i]);
210+
predicateState.OnNext(_predicateStates[i]);
211+
}
212+
213+
for (; i < _changeSets.Length; ++i)
214+
source.OnNext(_changeSets[i]);
215+
216+
for (; i < _predicateStates.Length; ++i)
217+
predicateState.OnNext(_predicateStates[i]);
218+
}
219+
220+
private readonly ImmutableArray<IChangeSet<Item>> _changeSets;
221+
private readonly ImmutableArray<int> _predicateStates;
222+
223+
public class Item
224+
{
225+
public static bool FilterByIdInclusionMask(
226+
int idInclusionMask,
227+
Item item)
228+
=> ((item.Id & idInclusionMask) == 0) && item.IsIncluded;
229+
230+
public required int Id { get; init; }
231+
232+
public bool IsIncluded { get; set; }
233+
234+
public override string ToString()
235+
=> $"{{ Id = {Id}, IsIncluded = {IsIncluded} }}";
236+
}
237+
}

0 commit comments

Comments
 (0)