Skip to content

Commit 021a933

Browse files
Merge pull request #2 from engineering87/develop
Add .NET 10 Support, New Temporal Data Structures & General Improvements
2 parents 0e908d1 + 547005b commit 021a933

29 files changed

+3301
-402
lines changed

README.md

Lines changed: 48 additions & 42 deletions
Large diffs are not rendered by default.

src/TemporalCollections.PerformanceTests/Benchmarks/AllBenchmarks.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ public static void RunAll()
1717
BenchmarkRunner.Run<TemporalStackBenchmarks>();
1818
BenchmarkRunner.Run<TemporalSlidingWindowSetBenchmarks>();
1919
BenchmarkRunner.Run<TemporalCircularBufferBenchmarks>();
20+
BenchmarkRunner.Run<TemporalSegmentedArrayBenchmarks>();
21+
BenchmarkRunner.Run<TemporalMultimapBenchmarks>();
2022
}
2123
}
2224
}
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
// (c) 2025 Francesco Del Re <francesco.delre.87@gmail.com>
2+
// This code is licensed under MIT license (see LICENSE.txt for details)
3+
using BenchmarkDotNet.Attributes;
4+
using TemporalCollections.Collections;
5+
using TemporalCollections.Models;
6+
7+
namespace TemporalCollections.PerformanceTests.Benchmarks
8+
{
9+
/// <summary>
10+
/// BenchmarkDotNet benchmarks for TemporalMultimap<TKey, TValue>.
11+
///
12+
/// Scenarios covered:
13+
/// - Adds: AddValue, Add (pre-built items), AddRange(values), AddRange(items)
14+
/// - Per-key queries: GetValuesInRange
15+
/// - Global ITimeQueryable queries: GetInRange, GetBefore, GetAfter, CountInRange, CountSince, GetNearest
16+
/// - Extremes & span: GetLatest/GetEarliest/GetTimeSpan
17+
/// - Retention: RemoveOlderThan (per-key & global), RemoveRange (per-key & global), RemoveKey, Clear
18+
///
19+
/// Notes:
20+
/// - We build fresh instances in IterationSetup to avoid cross-benchmark interference.
21+
/// - Time-window queries use "last N minutes/seconds" relative to now (creation time is during setup).
22+
/// </summary>
23+
[MemoryDiagnoser]
24+
public class TemporalMultimapBenchmarks
25+
{
26+
// ---------- Parameters ----------
27+
28+
/// <summary>Number of distinct keys in datasets.</summary>
29+
[Params(10, 100)]
30+
public int KeyCount { get; set; }
31+
32+
/// <summary>Number of values per key.</summary>
33+
[Params(1_000)]
34+
public int ValuesPerKey { get; set; }
35+
36+
// ---------- Prepared data ----------
37+
38+
private string[] _keys = default!;
39+
private (string Key, int Value)[] _kvData = default!;
40+
private TemporalItem<KeyValuePair<string, int>>[] _prebuiltItems = default!;
41+
42+
// ---------- Maps per scenario (fresh each iteration) ----------
43+
44+
private TemporalMultimap<string, int> _mapForAdds = default!;
45+
private TemporalMultimap<string, int> _mapForQueries = default!;
46+
private TemporalMultimap<string, int> _mapForPerKeyRetention = default!;
47+
private TemporalMultimap<string, int> _mapForGlobalRetention = default!;
48+
49+
// ---------- Setup ----------
50+
51+
/// <summary>
52+
/// Prepare deterministic keys and data layouts that are reused across iterations.
53+
/// Also prepares a set of pre-built TemporalItems for benchmarking Add(items)/AddRange(items).
54+
/// </summary>
55+
[GlobalSetup]
56+
public void GlobalSetup()
57+
{
58+
_keys = Enumerable.Range(0, KeyCount).Select(i => $"K{i}").ToArray();
59+
60+
// Flattened (key,value) array
61+
_kvData = new (string, int)[KeyCount * ValuesPerKey];
62+
int p = 0;
63+
for (int i = 0; i < KeyCount; i++)
64+
for (int v = 0; v < ValuesPerKey; v++)
65+
_kvData[p++] = (_keys[i], v);
66+
67+
// Prebuild TemporalItems with strictly increasing timestamps
68+
// (use a base time and tick increments to avoid Create() during the benchmark body)
69+
var baseTime = DateTimeOffset.UtcNow.AddMinutes(-30);
70+
_prebuiltItems = new TemporalItem<KeyValuePair<string, int>>[_kvData.Length];
71+
long tick = 0;
72+
for (int i = 0; i < _kvData.Length; i++)
73+
{
74+
var (k, v) = _kvData[i];
75+
var ts = baseTime.AddTicks(tick++);
76+
_prebuiltItems[i] = new TemporalItem<KeyValuePair<string, int>>(new KeyValuePair<string, int>(k, v), ts);
77+
}
78+
}
79+
80+
/// <summary>
81+
/// Build fresh maps for each iteration and load the query/retention datasets.
82+
/// </summary>
83+
[IterationSetup]
84+
public void IterationSetup()
85+
{
86+
_mapForAdds = new TemporalMultimap<string, int>();
87+
88+
_mapForQueries = new TemporalMultimap<string, int>();
89+
_mapForPerKeyRetention = new TemporalMultimap<string, int>();
90+
_mapForGlobalRetention = new TemporalMultimap<string, int>();
91+
92+
// Preload maps that are read/modified by query/retention benchmarks
93+
for (int i = 0; i < _kvData.Length; i++)
94+
{
95+
var (k, v) = _kvData[i];
96+
_mapForQueries.AddValue(k, v);
97+
_mapForPerKeyRetention.AddValue(k, v);
98+
_mapForGlobalRetention.AddValue(k, v);
99+
}
100+
}
101+
102+
// ---------- Adds ----------
103+
104+
/// <summary>Bulk insert all (key,value) pairs into an empty map via AddValue.</summary>
105+
[Benchmark(Description = "AddValue: insert N×M items across keys")]
106+
public void Add_AllItems_AddValue()
107+
{
108+
var map = _mapForAdds;
109+
for (int i = 0; i < _kvData.Length; i++)
110+
{
111+
var (k, v) = _kvData[i];
112+
map.AddValue(k, v);
113+
}
114+
}
115+
116+
/// <summary>Bulk insert using pre-built TemporalItem&lt;KeyValuePair&lt;string,int&gt;&gt; via Add(item).</summary>
117+
[Benchmark(Description = "Add(item): insert pre-built temporal items")]
118+
public void Add_AllItems_PreBuilt()
119+
{
120+
var map = _mapForAdds;
121+
for (int i = 0; i < _prebuiltItems.Length; i++)
122+
{
123+
map.Add(_prebuiltItems[i]);
124+
}
125+
}
126+
127+
/// <summary>Bulk insert per key using AddRange(values).</summary>
128+
[Benchmark(Description = "AddRange(values): insert per-key batches")]
129+
public void AddRange_Values()
130+
{
131+
var map = _mapForAdds;
132+
foreach (var k in _keys)
133+
{
134+
// Reuse a slice [0..ValuesPerKey) for simplicity
135+
map.AddRange(k, Enumerable.Range(0, ValuesPerKey));
136+
}
137+
}
138+
139+
/// <summary>Bulk insert using AddRange(items) with pre-built items.</summary>
140+
[Benchmark(Description = "AddRange(items): insert pre-built temporal items")]
141+
public void AddRange_Items()
142+
{
143+
var map = _mapForAdds;
144+
map.AddRange(_prebuiltItems);
145+
}
146+
147+
// ---------- Per-key query ----------
148+
149+
/// <summary>Per-key inclusive range query over the last 2 minutes.</summary>
150+
[Benchmark(Description = "Per-key query: GetValuesInRange(last 2 minutes)")]
151+
public void PerKey_GetValuesInRange_Last2Minutes()
152+
{
153+
string key = _keys[_keys.Length / 2];
154+
var to = DateTimeOffset.UtcNow;
155+
var from = to.AddMinutes(-2);
156+
var _ = _mapForQueries.GetValuesInRange(key, from, to);
157+
}
158+
159+
// ---------- Global queries (ITimeQueryable) ----------
160+
161+
/// <summary>Global inclusive range query (last 2 minutes).</summary>
162+
[Benchmark(Description = "Global query: GetInRange(last 2 minutes)")]
163+
public void Global_GetInRange_Last2Minutes()
164+
{
165+
var to = DateTimeOffset.UtcNow;
166+
var from = to.AddMinutes(-2);
167+
var _ = _mapForQueries.GetInRange(from, to);
168+
}
169+
170+
/// <summary>Global strictly-before query using a midpoint cutoff.</summary>
171+
[Benchmark(Description = "Global query: GetBefore(midpoint cutoff)")]
172+
public void Global_GetBefore_Midpoint()
173+
{
174+
// Use two known items to craft a midpoint
175+
var to = DateTimeOffset.UtcNow;
176+
var from = to.AddMinutes(-5);
177+
var window = _mapForQueries.GetInRange(from, to).ToArray();
178+
if (window.Length < 2) return;
179+
var cutoff = Mid(window[0].Timestamp, window[^1].Timestamp);
180+
var _ = _mapForQueries.GetBefore(cutoff);
181+
}
182+
183+
/// <summary>Global strictly-after query using a midpoint cutoff.</summary>
184+
[Benchmark(Description = "Global query: GetAfter(midpoint cutoff)")]
185+
public void Global_GetAfter_Midpoint()
186+
{
187+
var to = DateTimeOffset.UtcNow;
188+
var from = to.AddMinutes(-5);
189+
var window = _mapForQueries.GetInRange(from, to).ToArray();
190+
if (window.Length < 2) return;
191+
var cutoff = Mid(window[0].Timestamp, window[^1].Timestamp);
192+
var _ = _mapForQueries.GetAfter(cutoff);
193+
}
194+
195+
/// <summary>Global inclusive count in a 2-minute window.</summary>
196+
[Benchmark(Description = "Global query: CountInRange(last 2 minutes)")]
197+
public int Global_CountInRange_Last2Minutes()
198+
{
199+
var to = DateTimeOffset.UtcNow;
200+
var from = to.AddMinutes(-2);
201+
return _mapForQueries.CountInRange(from, to);
202+
}
203+
204+
/// <summary>Global count since (>=) a moving cutoff (~last minute).</summary>
205+
[Benchmark(Description = "Global query: CountSince(last 1 minute)")]
206+
public int Global_CountSince_Last1Minute()
207+
{
208+
var from = DateTimeOffset.UtcNow.AddMinutes(-1);
209+
return _mapForQueries.CountSince(from);
210+
}
211+
212+
/// <summary>Global nearest-to-time (use midpoint of a recent window).</summary>
213+
[Benchmark(Description = "Global query: GetNearest(midpoint)")]
214+
public TemporalItem<KeyValuePair<string, int>>? Global_GetNearest_Midpoint()
215+
{
216+
var to = DateTimeOffset.UtcNow;
217+
var from = to.AddMinutes(-5);
218+
var window = _mapForQueries.GetInRange(from, to).ToArray();
219+
if (window.Length < 2) return null;
220+
var mid = Mid(window[0].Timestamp, window[^1].Timestamp);
221+
return _mapForQueries.GetNearest(mid);
222+
}
223+
224+
/// <summary>Fetch extremes and span in a single call group.</summary>
225+
[Benchmark(Description = "Global query: GetLatest/GetEarliest/GetTimeSpan")]
226+
public (TemporalItem<KeyValuePair<string, int>>? latest,
227+
TemporalItem<KeyValuePair<string, int>>? earliest,
228+
TimeSpan span) Global_Extremes_And_Span()
229+
{
230+
var latest = _mapForQueries.GetLatest();
231+
var earliest = _mapForQueries.GetEarliest();
232+
var span = _mapForQueries.GetTimeSpan();
233+
return (latest, earliest, span);
234+
}
235+
236+
// ---------- Retention ----------
237+
238+
/// <summary>Per-key RemoveOlderThan with cutoff = now - 1 minute.</summary>
239+
[Benchmark(Description = "Per-key retention: RemoveOlderThan(key, now-1m)")]
240+
public void PerKey_RemoveOlderThan()
241+
{
242+
string key = _keys[_keys.Length / 2];
243+
var cutoff = DateTimeOffset.UtcNow.AddMinutes(-1);
244+
_mapForPerKeyRetention.RemoveOlderThan(key, cutoff);
245+
}
246+
247+
/// <summary>Per-key RemoveRange over [now-90s .. now-30s].</summary>
248+
[Benchmark(Description = "Per-key retention: RemoveRange(key, [now-90s..now-30s])")]
249+
public void PerKey_RemoveRange()
250+
{
251+
string key = _keys[_keys.Length / 2];
252+
var to = DateTimeOffset.UtcNow.AddSeconds(-30);
253+
var from = DateTimeOffset.UtcNow.AddSeconds(-90);
254+
_mapForPerKeyRetention.RemoveRange(key, from, to);
255+
}
256+
257+
/// <summary>RemoveKey for a middle key.</summary>
258+
[Benchmark(Description = "Per-key retention: RemoveKey(middle key)")]
259+
public void PerKey_RemoveKey()
260+
{
261+
string key = _keys[_keys.Length / 2];
262+
_mapForPerKeyRetention.RemoveKey(key);
263+
}
264+
265+
/// <summary>Global RemoveOlderThan with cutoff = now - 1 minute.</summary>
266+
[Benchmark(Description = "Global retention: RemoveOlderThan(now-1m)")]
267+
public void Global_RemoveOlderThan()
268+
{
269+
var cutoff = DateTimeOffset.UtcNow.AddMinutes(-1);
270+
_mapForGlobalRetention.RemoveOlderThan(cutoff);
271+
}
272+
273+
/// <summary>Global RemoveRange over [now-2m .. now-1m].</summary>
274+
[Benchmark(Description = "Global retention: RemoveRange([now-2m..now-1m])")]
275+
public void Global_RemoveRange()
276+
{
277+
var to = DateTimeOffset.UtcNow.AddMinutes(-1);
278+
var from = DateTimeOffset.UtcNow.AddMinutes(-2);
279+
_mapForGlobalRetention.RemoveRange(from, to);
280+
}
281+
282+
/// <summary>Global Clear of map.</summary>
283+
[Benchmark(Description = "Global retention: Clear()")]
284+
public void Global_Clear()
285+
{
286+
_mapForGlobalRetention.Clear();
287+
}
288+
289+
// ---------- Utility ----------
290+
291+
private static DateTimeOffset Mid(DateTimeOffset a, DateTimeOffset b)
292+
{
293+
long mid = (a.UtcTicks + b.UtcTicks) / 2;
294+
return new DateTimeOffset(mid, TimeSpan.Zero);
295+
}
296+
}
297+
}

0 commit comments

Comments
 (0)