|
| 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