Skip to content

Commit 9e47bc4

Browse files
authored
Merge branch 'main' into maint/list_ext_breakup
2 parents 572ddba + 46ccd1a commit 9e47bc4

52 files changed

Lines changed: 5399 additions & 559 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/release.yml

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,16 @@ jobs:
131131
id: changelog
132132

133133
- name: Create GitHub Release
134-
uses: softprops/action-gh-release@v2.6.2
135-
with:
136-
tag_name: ${{ steps.nbgv.outputs.SemVer2 }}
137-
name: ${{ steps.nbgv.outputs.SemVer2 }}
138-
prerelease: ${{ steps.nbgv.outputs.PrereleaseVersion != '' }}
139-
body: |
140-
${{ steps.changelog.outputs.commitLog }}
134+
env:
135+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
136+
TAG: ${{ steps.nbgv.outputs.SemVer2 }}
137+
IS_PRERELEASE: ${{ steps.nbgv.outputs.PrereleaseVersion != '' }}
138+
BODY: ${{ steps.changelog.outputs.commitLog }}
139+
shell: pwsh
140+
run: |
141+
$ErrorActionPreference = 'Stop'
142+
$notesPath = Join-Path $env:RUNNER_TEMP 'release-notes.md'
143+
Set-Content -Path $notesPath -Value $env:BODY -Encoding utf8 -NoNewline
144+
$cmd = @('release', 'create', $env:TAG, '--title', $env:TAG, '--notes-file', $notesPath, '--target', $env:GITHUB_SHA)
145+
if ($env:IS_PRERELEASE -eq 'true') { $cmd += '--prerelease' }
146+
gh @cmd
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
// Copyright (c) 2011-2025 Roland Pheasant. All rights reserved.
2+
// Roland Pheasant licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for full license information.
4+
5+
using System;
6+
using System.Linq;
7+
using System.Reactive.Linq;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
11+
using BenchmarkDotNet.Attributes;
12+
13+
using DynamicData.Binding;
14+
15+
namespace DynamicData.Benchmarks.Cache;
16+
17+
/// <summary>
18+
/// Multi-threaded SourceCache contention benchmarks. Measures aggregate throughput
19+
/// when N threads write concurrently with varying subscriber complexity.
20+
/// Exercises the DeliveryQueue path (ObservableCache).
21+
/// </summary>
22+
[MemoryDiagnoser]
23+
[MarkdownExporterAttribute.GitHub]
24+
public class ContentionBenchmarks
25+
{
26+
private SourceCache<ContentionItem, int> _cache = null!;
27+
private IDisposable? _subscription;
28+
29+
[Params(1, 2, 4)]
30+
public int ThreadCount;
31+
32+
[Params("None", "Sort", "Chain")]
33+
public string SubscriberWork = "None";
34+
35+
private const int ItemsPerThread = 1_000;
36+
37+
[GlobalSetup]
38+
public void GlobalSetup()
39+
{
40+
_cache = new SourceCache<ContentionItem, int>(i => i.Id);
41+
}
42+
43+
[GlobalCleanup]
44+
public void GlobalCleanup()
45+
{
46+
_subscription?.Dispose();
47+
_cache.Dispose();
48+
}
49+
50+
[IterationSetup]
51+
public void IterationSetup()
52+
{
53+
_subscription?.Dispose();
54+
_cache.Clear();
55+
56+
_subscription = SubscriberWork switch
57+
{
58+
"Sort" => _cache.Connect()
59+
.Sort(SortExpressionComparer<ContentionItem>.Ascending(i => i.Name))
60+
.Subscribe(_ => { }),
61+
62+
"Chain" => _cache.Connect()
63+
.Filter(i => i.Price > 0)
64+
.Sort(SortExpressionComparer<ContentionItem>.Ascending(i => i.Name))
65+
.Transform(i => new ContentionItemVm(i))
66+
.Subscribe(_ => { }),
67+
68+
_ => _cache.Connect().Subscribe(_ => { }),
69+
};
70+
}
71+
72+
[Benchmark]
73+
public void ConcurrentAddOrUpdate()
74+
{
75+
if (ThreadCount == 1)
76+
{
77+
for (var i = 0; i < ItemsPerThread; i++)
78+
_cache.AddOrUpdate(new ContentionItem(i, $"Item_{i}", i * 0.1m));
79+
}
80+
else
81+
{
82+
var barrier = new Barrier(ThreadCount);
83+
var tasks = new Task[ThreadCount];
84+
85+
for (var t = 0; t < ThreadCount; t++)
86+
{
87+
var threadId = t;
88+
tasks[t] = Task.Run(() =>
89+
{
90+
barrier.SignalAndWait();
91+
for (var i = 0; i < ItemsPerThread; i++)
92+
{
93+
var id = (threadId * ItemsPerThread) + i;
94+
_cache.AddOrUpdate(new ContentionItem(id, $"Item_{id}", id * 0.1m));
95+
}
96+
});
97+
}
98+
99+
Task.WaitAll(tasks);
100+
barrier.Dispose();
101+
}
102+
}
103+
104+
public sealed record ContentionItem(int Id, string Name, decimal Price);
105+
public sealed record ContentionItemVm(ContentionItem Source);
106+
}
107+
108+
/// <summary>
109+
/// MergeManyChangeSets contention benchmark. Multiple threads mutating child
110+
/// SourceCaches while a CPS pipeline is subscribed. Uses SourceCache (not
111+
/// SourceList) for children so the full path exercises DeliveryQueue + SDQ.
112+
/// </summary>
113+
[MemoryDiagnoser]
114+
[MarkdownExporterAttribute.GitHub]
115+
public class MmcsContentionBenchmarks
116+
{
117+
private SourceCache<MmcsParent, int> _parents = null!;
118+
private MmcsParent[] _parentItems = null!;
119+
private IDisposable? _subscription;
120+
121+
[Params(1, 2, 4)]
122+
public int ThreadCount;
123+
124+
[Params("None", "Sort", "Transform")]
125+
public string SubscriberWork = "None";
126+
127+
private const int ParentCount = 50;
128+
private const int ChildOpsPerThread = 200;
129+
130+
[GlobalSetup]
131+
public void GlobalSetup()
132+
{
133+
_parents = new SourceCache<MmcsParent, int>(p => p.Id);
134+
_parentItems = Enumerable.Range(0, ParentCount).Select(i =>
135+
{
136+
var p = new MmcsParent(i);
137+
p.Children.Edit(u =>
138+
{
139+
for (var j = 0; j < 10; j++)
140+
u.AddOrUpdate(new MmcsChild(i * 100 + j, $"Child_{i}_{j}"));
141+
});
142+
return p;
143+
}).ToArray();
144+
}
145+
146+
[GlobalCleanup]
147+
public void GlobalCleanup()
148+
{
149+
_subscription?.Dispose();
150+
foreach (var p in _parentItems) p.Dispose();
151+
_parents.Dispose();
152+
}
153+
154+
[IterationSetup]
155+
public void IterationSetup()
156+
{
157+
_subscription?.Dispose();
158+
_parents.Clear();
159+
_parents.AddOrUpdate(_parentItems);
160+
161+
var pipeline = _parents.Connect()
162+
.MergeManyChangeSets(p => p.Children.Connect());
163+
164+
_subscription = SubscriberWork switch
165+
{
166+
"Sort" => pipeline
167+
.Sort(SortExpressionComparer<MmcsChild>.Ascending(c => c.Name))
168+
.Bind(out _)
169+
.Subscribe(_ => { }),
170+
171+
"Transform" => pipeline
172+
.Transform(c => new MmcsChildVm(c))
173+
.Subscribe(_ => { }),
174+
175+
_ => pipeline.Subscribe(_ => { }),
176+
};
177+
}
178+
179+
[Benchmark]
180+
public void ConcurrentChildMutations()
181+
{
182+
if (ThreadCount == 1)
183+
{
184+
MutateChildren(0);
185+
}
186+
else
187+
{
188+
var barrier = new Barrier(ThreadCount);
189+
var tasks = new Task[ThreadCount];
190+
for (var t = 0; t < ThreadCount; t++)
191+
{
192+
var threadId = t;
193+
tasks[t] = Task.Run(() =>
194+
{
195+
barrier.SignalAndWait();
196+
MutateChildren(threadId);
197+
});
198+
}
199+
Task.WaitAll(tasks);
200+
barrier.Dispose();
201+
}
202+
}
203+
204+
private void MutateChildren(int threadId)
205+
{
206+
for (var i = 0; i < ChildOpsPerThread; i++)
207+
{
208+
var parentIdx = (threadId * ChildOpsPerThread + i) % ParentCount;
209+
var parent = _parentItems[parentIdx];
210+
var childId = threadId * 100_000 + i;
211+
parent.Children.AddOrUpdate(new MmcsChild(childId, $"New_{childId}"));
212+
if (parent.Children.Count > 15)
213+
parent.Children.RemoveKey(parent.Children.Keys.First());
214+
}
215+
}
216+
217+
public sealed record MmcsChild(int Id, string Name);
218+
public sealed record MmcsChildVm(MmcsChild Source);
219+
220+
public sealed class MmcsParent(int id) : IDisposable
221+
{
222+
public int Id { get; } = id;
223+
public SourceCache<MmcsChild, int> Children { get; } = new(c => c.Id);
224+
public void Dispose() => Children.Dispose();
225+
}
226+
}

src/DynamicData.Benchmarks/DynamicData.Benchmarks.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFramework>net8.0-windows</TargetFramework>
5+
<TargetFramework>net9.0</TargetFramework>
66
<PlatformTarget>AnyCPU</PlatformTarget>
77
<IsPackable>false</IsPackable>
88
<NoWarn>;1591;1701;1702;1705;CA1822;CA1001</NoWarn>

0 commit comments

Comments
 (0)