Skip to content

Commit 6f28cff

Browse files
author
Mykyta Zotov
committed
benhcmark: enhance Rebalance Flow Benchmarks with detailed documentation and improved configuration for span behavior and storage strategy, ensuring deterministic workload generation and isolation of rebalance costs.
1 parent 5aaf15d commit 6f28cff

3 files changed

Lines changed: 193 additions & 113 deletions

File tree

tests/SlidingWindowCache.Benchmarks/Benchmarks/RebalanceFlowBenchmarks.cs

Lines changed: 185 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -9,168 +9,248 @@
99
namespace SlidingWindowCache.Benchmarks.Benchmarks;
1010

1111
/// <summary>
12-
/// Rebalance/Maintenance Flow Benchmarks
13-
/// Measures ONLY window maintenance and rebalance operation costs.
14-
/// Uses zero-latency SynchronousDataSource to isolate cache mechanics from I/O.
12+
/// Rebalance Flow Benchmarks
13+
/// Behavior-driven benchmarking suite focused exclusively on rebalance mechanics and storage rematerialization cost.
1514
///
16-
/// EXECUTION FLOW: Trigger mutation → WaitForIdleAsync → Measure rebalance cost
15+
/// BENCHMARK PHILOSOPHY:
16+
/// This suite models system behavior through three orthogonal axes:
17+
/// ✔ RequestedRange Span Behavior (Fixed/Growing/Shrinking) - models requested range span dynamics
18+
/// ✔ Storage Strategy (Snapshot/CopyOnRead) - measures rematerialization tradeoffs
19+
/// ✔ Base RequestedRange Span Size (100/1000/10000) - tests scaling behavior
20+
///
21+
/// PERFORMANCE MODEL:
22+
/// Rebalance cost depends primarily on:
23+
/// ✔ Span stability/volatility (behavior axis)
24+
/// ✔ Buffer reuse feasibility (storage axis)
25+
/// ✔ Capacity growth patterns (size axis)
26+
///
27+
/// NOT on:
28+
/// ✖ Cache hit/miss classification (irrelevant for rebalance cost)
29+
/// ✖ DataSource performance (isolated via SynchronousDataSource)
30+
/// ✖ Decision logic (covered by tests, not benchmarked)
31+
///
32+
/// EXECUTION MODEL: Deterministic multi-request sequence → Measure cumulative rebalance cost
1733
///
1834
/// Methodology:
1935
/// - Fresh cache per iteration
20-
/// - SynchronousDataSource (zero latency) isolates cache mechanics
21-
/// - Trigger rebalance by moving outside thresholds
22-
/// - WaitForIdleAsync INSIDE benchmark methods (measuring rebalance)
23-
/// - Aggressive thresholds ensure rebalancing occurs
36+
/// - Zero-latency SynchronousDataSource isolates cache mechanics
37+
/// - Deterministic request sequence precomputed in IterationSetup (RequestsPerInvocation = 10)
38+
/// - Each request guarantees rebalance via range shift and aggressive thresholds
39+
/// - WaitForIdleAsync after EACH request (measuring rebalance completion)
40+
/// - Benchmark method contains ZERO workload logic, ZERO branching, ZERO allocations
41+
///
42+
/// Workload Generation:
43+
/// - ALL span calculations occur in BuildRequestSequence()
44+
/// - ALL branching occurs in BuildRequestSequence()
45+
/// - Benchmark method only iterates precomputed array and awaits results
46+
///
47+
/// EXPECTED BEHAVIOR:
48+
/// - Fixed RequestedRange Span: CopyOnRead optimal (buffer reuse), Snapshot consistent (always allocates)
49+
/// - Growing RequestedRange Span: CopyOnRead capacity growth penalty, Snapshot stable cost
50+
/// - Shrinking RequestedRange Span: Both strategies handle well, CopyOnRead may over-allocate
2451
/// </summary>
2552
[MemoryDiagnoser]
2653
[MarkdownExporter]
27-
[GroupBenchmarksBy(BenchmarkDotNet.Configs.BenchmarkLogicalGroupRule.ByCategory)]
2854
public class RebalanceFlowBenchmarks
2955
{
30-
private WindowCache<int, int, IntegerFixedStepDomain>? _snapshotCache;
31-
private WindowCache<int, int, IntegerFixedStepDomain>? _copyOnReadCache;
32-
private SynchronousDataSource _dataSource = default!;
33-
private IntegerFixedStepDomain _domain = default!;
56+
/// <summary>
57+
/// RequestedRange Span behavior model: Fixed (stable), Growing (increasing), Shrinking (decreasing)
58+
/// </summary>
59+
public enum SpanBehavior
60+
{
61+
Fixed,
62+
Growing,
63+
Shrinking
64+
}
65+
66+
/// <summary>
67+
/// Storage strategy: Snapshot (array-based) vs CopyOnRead (list-based)
68+
/// </summary>
69+
public enum StorageStrategy
70+
{
71+
Snapshot,
72+
CopyOnRead
73+
}
74+
75+
// Benchmark Parameters - 3 Orthogonal Axes
3476

3577
/// <summary>
36-
/// Requested range size - varies from small (100) to very large (1,000,000) to test rebalance scaling behavior.
78+
/// RequestedRange Span behavior model determining how requested range span evolves across iterations
79+
/// </summary>
80+
[Params(SpanBehavior.Fixed, SpanBehavior.Growing, SpanBehavior.Shrinking)]
81+
public SpanBehavior Behavior { get; set; }
82+
83+
/// <summary>
84+
/// Storage strategy for cache rematerialization
85+
/// </summary>
86+
[Params(StorageStrategy.Snapshot, StorageStrategy.CopyOnRead)]
87+
public StorageStrategy Strategy { get; set; }
88+
89+
/// <summary>
90+
/// Base span size for requested ranges - tests scaling behavior from small to large data volumes
3791
/// </summary>
3892
[Params(100, 1_000, 10_000)]
39-
public int RangeSpan { get; set; }
93+
public int BaseSpanSize { get; set; }
94+
95+
// Configuration Constants
4096

4197
/// <summary>
42-
/// Cache coefficient size for left/right prefetch - varies from minimal (1) to aggressive (1,000).
43-
/// Combined with RangeSpan, determines total materialized cache size during rebalance.
98+
/// Cache coefficient for left/right prefetch - fixed to isolate span behavior effects
4499
/// </summary>
45-
[Params(1, 10, 100)]
46-
public int CacheCoefficientSize { get; set; }
100+
private const int CacheCoefficientSize = 10;
47101

48-
private int InitialStart => 10000;
49-
private int InitialEnd => InitialStart + RangeSpan;
102+
/// <summary>
103+
/// Growth factor per iteration for Growing RequestedRange span behavior
104+
/// </summary>
105+
private const int GrowthFactor = 100;
50106

51-
private Range<int> InitialCacheRange =>
52-
Intervals.NET.Factories.Range.Closed<int>(InitialStart, InitialEnd);
107+
/// <summary>
108+
/// Shrink factor per iteration for Shrinking RequestedRange span behavior
109+
/// </summary>
110+
private const int ShrinkFactor = 100;
111+
112+
/// <summary>
113+
/// Initial range start position - arbitrary but consistent across all benchmarks
114+
/// </summary>
115+
private const int InitialStart = 10000;
116+
117+
/// <summary>
118+
/// Number of requests executed per benchmark invocation - deterministic workload size
119+
/// </summary>
120+
private const int RequestsPerInvocation = 10;
53121

54-
private Range<int> InitialCacheRangeAfterRebalance => InitialCacheRange
55-
.ExpandByRatio(_domain, CacheCoefficientSize, CacheCoefficientSize);
122+
// Infrastructure
56123

57-
private Range<int> PartialHitRange => InitialCacheRangeAfterRebalance
58-
.Shift(_domain, InitialCacheRangeAfterRebalance.Span(_domain).Value / 2);
124+
private WindowCache<int, int, IntegerFixedStepDomain>? _cache;
125+
private SynchronousDataSource _dataSource = null!;
126+
private IntegerFixedStepDomain _domain;
127+
private WindowCacheOptions _options = null!;
59128

60-
private Range<int> FullMissRange => InitialCacheRangeAfterRebalance
61-
.Shift(_domain, InitialCacheRangeAfterRebalance.Span(_domain).Value * 3);
129+
// Deterministic Workload Storage
62130

63-
private Range<int> _partialHitRange;
64-
private Range<int> _fullMissRange;
65-
private WindowCacheOptions _snapshotOptions;
66-
private WindowCacheOptions _copyOnReadOptions;
131+
/// <summary>
132+
/// Precomputed request sequence for current iteration - generated in IterationSetup.
133+
/// Contains EXACTLY RequestsPerInvocation ranges with all span calculations completed.
134+
/// Benchmark methods iterate through this array without any workload logic.
135+
/// </summary>
136+
private Range<int>[] _requestSequence = null!;
67137

68138
[GlobalSetup]
69139
public void GlobalSetup()
70140
{
71141
_domain = new IntegerFixedStepDomain();
72142
_dataSource = new SynchronousDataSource(_domain);
73143

74-
// Pre-calculate rebalance triggering ranges
75-
_partialHitRange = PartialHitRange;
76-
77-
_fullMissRange = FullMissRange;
78-
79-
_snapshotOptions = new WindowCacheOptions(
80-
leftCacheSize: CacheCoefficientSize,
81-
rightCacheSize: CacheCoefficientSize,
82-
UserCacheReadMode.Snapshot,
83-
leftThreshold: 0,
84-
rightThreshold: 0
85-
);
144+
// Configure cache with aggressive thresholds to guarantee rebalancing
145+
// leftThreshold=0, rightThreshold=0 means any request outside current window triggers rebalance
146+
var readMode = Strategy switch
147+
{
148+
StorageStrategy.Snapshot => UserCacheReadMode.Snapshot,
149+
StorageStrategy.CopyOnRead => UserCacheReadMode.CopyOnRead,
150+
_ => throw new ArgumentOutOfRangeException(nameof(Strategy))
151+
};
86152

87-
_copyOnReadOptions = new WindowCacheOptions(
153+
_options = new WindowCacheOptions(
88154
leftCacheSize: CacheCoefficientSize,
89155
rightCacheSize: CacheCoefficientSize,
90-
UserCacheReadMode.CopyOnRead,
91-
leftThreshold: 0,
156+
readMode: readMode,
157+
leftThreshold: 1, // Set to 1 (100%) to ensure any request even the same range as previous triggers rebalance, isolating rebalance cost
92158
rightThreshold: 0
93159
);
94160
}
95161

96162
[IterationSetup]
97163
public void IterationSetup()
98164
{
99-
_snapshotCache = new WindowCache<int, int, IntegerFixedStepDomain>(
165+
// Create fresh cache for this iteration
166+
_cache = new WindowCache<int, int, IntegerFixedStepDomain>(
100167
_dataSource,
101168
_domain,
102-
_snapshotOptions
169+
_options
103170
);
104171

105-
_copyOnReadCache = new WindowCache<int, int, IntegerFixedStepDomain>(
106-
_dataSource,
107-
_domain,
108-
_copyOnReadOptions
109-
);
172+
// Compute initial range for priming the cache
173+
var initialRange = Intervals.NET.Factories.Range.Closed<int>(InitialStart, InitialStart + BaseSpanSize - 1);
110174

111-
// Prime both caches with initial window
112-
var initialRange = Intervals.NET.Factories.Range.Closed<int>(InitialStart, InitialEnd);
113-
_snapshotCache.GetDataAsync(initialRange, CancellationToken.None).GetAwaiter().GetResult();
114-
_copyOnReadCache.GetDataAsync(initialRange, CancellationToken.None).GetAwaiter().GetResult();
175+
// Prime cache with initial window
176+
_cache.GetDataAsync(initialRange, CancellationToken.None).GetAwaiter().GetResult();
177+
_cache.WaitForIdleAsync().GetAwaiter().GetResult();
115178

116-
// Wait for initial rebalancing to complete
117-
_snapshotCache.WaitForIdleAsync().GetAwaiter().GetResult();
118-
_copyOnReadCache.WaitForIdleAsync().GetAwaiter().GetResult();
179+
// Build deterministic request sequence with all workload logic
180+
_requestSequence = BuildRequestSequence(initialRange);
119181
}
120182

121-
[IterationCleanup]
122-
public void IterationCleanup()
183+
/// <summary>
184+
/// Builds a deterministic request sequence based on the configured span behavior.
185+
/// This method contains ALL workload generation logic, span calculations, and branching.
186+
/// The benchmark method will execute this precomputed sequence with zero overhead.
187+
/// </summary>
188+
/// <param name="initialRange">The initial primed range used to seed the sequence</param>
189+
/// <returns>Array of EXACTLY RequestsPerInvocation ranges, precomputed and ready to execute</returns>
190+
private Range<int>[] BuildRequestSequence(Range<int> initialRange)
123191
{
124-
// Final stabilization before next iteration
125-
_snapshotCache?.WaitForIdleAsync(TimeSpan.FromSeconds(5)).GetAwaiter().GetResult();
126-
_copyOnReadCache?.WaitForIdleAsync(TimeSpan.FromSeconds(5)).GetAwaiter().GetResult();
127-
}
192+
var sequence = new Range<int>[RequestsPerInvocation];
128193

129-
[Benchmark(Baseline = true)]
130-
[BenchmarkCategory("PartialHit")]
131-
public async Task Rebalance_AfterPartialHit_Snapshot()
132-
{
133-
// Trigger rebalance with partial overlap [1500, 2500] vs cached [1000, 2000]
134-
await _snapshotCache!.GetDataAsync(_partialHitRange, CancellationToken.None);
194+
for (var i = 0; i < RequestsPerInvocation; i++)
195+
{
196+
Range<int> requestRange;
135197

136-
// Explicitly measure rebalance cycle completion
137-
// This is the cost center we're measuring
138-
await _snapshotCache.WaitForIdleAsync(timeout: TimeSpan.FromSeconds(10));
139-
}
198+
switch (Behavior)
199+
{
200+
case SpanBehavior.Fixed:
201+
// Fixed: Span remains constant, position shifts by +1 each request
202+
requestRange = initialRange.Shift(_domain, i + 1);
203+
break;
140204

141-
[Benchmark]
142-
[BenchmarkCategory("PartialHit")]
143-
public async Task Rebalance_AfterPartialHit_CopyOnRead()
144-
{
145-
// Trigger rebalance with partial overlap [1500, 2500] vs cached [1000, 2000]
146-
await _copyOnReadCache!.GetDataAsync(_partialHitRange, CancellationToken.None);
205+
case SpanBehavior.Growing:
206+
// Growing: Span increases deterministically, position shifts slightly
207+
var spanGrow = i * GrowthFactor;
208+
requestRange = initialRange.Shift(_domain, i + 1).Expand(_domain, 0, spanGrow);
209+
break;
210+
211+
case SpanBehavior.Shrinking:
212+
// Shrinking: Span decreases deterministically, respecting minimum
213+
var spanShrink = i * ShrinkFactor;
214+
var bigInitialRange = initialRange.Expand(_domain, 0, RequestsPerInvocation * ShrinkFactor); // Ensure we have room to shrink
215+
requestRange = bigInitialRange.Shift(_domain, i + 1).Expand(_domain, 0, -spanShrink);
216+
break;
217+
218+
default:
219+
throw new ArgumentOutOfRangeException(nameof(Behavior), Behavior, "Unsupported span behavior");
220+
}
221+
222+
sequence[i] = requestRange;
223+
}
147224

148-
// Explicitly measure rebalance cycle completion
149-
// This is the cost center we're measuring
150-
await _copyOnReadCache.WaitForIdleAsync(timeout: TimeSpan.FromSeconds(10));
225+
return sequence;
151226
}
152227

153-
[Benchmark]
154-
[BenchmarkCategory("FullMiss")]
155-
public async Task Rebalance_AfterFullMiss_Snapshot()
228+
[IterationCleanup]
229+
public void IterationCleanup()
156230
{
157-
// Trigger rebalance with no overlap [5000, 6000] vs cached [1000, 2000]
158-
await _snapshotCache!.GetDataAsync(_fullMissRange, CancellationToken.None);
159-
160-
// Explicitly measure rebalance cycle completion
161-
// Full cache replacement cost
162-
await _snapshotCache.WaitForIdleAsync(timeout: TimeSpan.FromSeconds(10));
231+
// Ensure cache is idle before next iteration
232+
_cache?.WaitForIdleAsync(TimeSpan.FromSeconds(5)).GetAwaiter().GetResult();
163233
}
164234

235+
/// <summary>
236+
/// Measures rebalance rematerialization cost for the configured span behavior and storage strategy.
237+
/// Executes a deterministic sequence of requests, each followed by rebalance completion.
238+
/// This benchmark measures ONLY the rebalance path - decision logic is excluded.
239+
/// Contains ZERO workload logic, ZERO branching, ZERO span calculations.
240+
/// </summary>
165241
[Benchmark]
166-
[BenchmarkCategory("FullMiss")]
167-
public async Task Rebalance_AfterFullMiss_CopyOnRead()
242+
public async Task Rebalance()
168243
{
169-
// Trigger rebalance with no overlap [5000, 6000] vs cached [1000, 2000]
170-
await _copyOnReadCache!.GetDataAsync(_fullMissRange, CancellationToken.None);
171-
172-
// Explicitly measure rebalance cycle completion
173-
// Full cache replacement cost
174-
await _copyOnReadCache.WaitForIdleAsync(timeout: TimeSpan.FromSeconds(10));
244+
// Execute precomputed request sequence
245+
// Each request triggers rebalance (guaranteed by leftThreshold=1 and range shift)
246+
// Measure complete rebalance cycle for each request
247+
foreach (var requestRange in _requestSequence)
248+
{
249+
await _cache!.GetDataAsync(requestRange, CancellationToken.None);
250+
251+
// Explicitly measure rebalance cycle completion
252+
// This captures the rematerialization cost we're benchmarking
253+
await _cache.WaitForIdleAsync(timeout: TimeSpan.FromSeconds(10));
254+
}
175255
}
176-
}
256+
}

tests/SlidingWindowCache.Benchmarks/Benchmarks/ScenarioBenchmarks.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ namespace SlidingWindowCache.Benchmarks.Benchmarks;
2525
[GroupBenchmarksBy(BenchmarkDotNet.Configs.BenchmarkLogicalGroupRule.ByCategory)]
2626
public class ScenarioBenchmarks
2727
{
28-
private SynchronousDataSource _dataSource = default!;
29-
private IntegerFixedStepDomain _domain = default!;
28+
private SynchronousDataSource _dataSource = null!;
29+
private IntegerFixedStepDomain _domain;
3030
private WindowCache<int, int, IntegerFixedStepDomain>? _snapshotCache;
3131
private WindowCache<int, int, IntegerFixedStepDomain>? _copyOnReadCache;
32-
private WindowCacheOptions _snapshotOptions = default!;
33-
private WindowCacheOptions _copyOnReadOptions = default!;
34-
private List<Range<int>> _sequentialRanges = default!;
32+
private WindowCacheOptions _snapshotOptions = null!;
33+
private WindowCacheOptions _copyOnReadOptions = null!;
34+
private List<Range<int>> _sequentialRanges = null!;
3535
private Range<int> _coldStartRange;
3636

3737
/// <summary>
@@ -157,7 +157,7 @@ public void LocalityIterationSetup()
157157

158158
var localityCopyOnReadOptions = new WindowCacheOptions(
159159
leftCacheSize: CacheCoefficientSize,
160-
rightCacheSize: CacheCoefficientSize * 10, // Moderate prefetch for sequential access
160+
rightCacheSize: CacheCoefficientSize * 10, // Aggressive prefetch for sequential access
161161
UserCacheReadMode.CopyOnRead,
162162
leftThreshold: 0,
163163
rightThreshold: 0

tests/SlidingWindowCache.Benchmarks/Benchmarks/UserFlowBenchmarks.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ public class UserFlowBenchmarks
2929
{
3030
private WindowCache<int, int, IntegerFixedStepDomain>? _snapshotCache;
3131
private WindowCache<int, int, IntegerFixedStepDomain>? _copyOnReadCache;
32-
private SynchronousDataSource _dataSource = default!;
33-
private IntegerFixedStepDomain _domain = default!;
32+
private SynchronousDataSource _dataSource = null!;
33+
private IntegerFixedStepDomain _domain;
3434

3535
/// <summary>
3636
/// Requested range size - varies from small (100) to very large (1,000,000) to test scaling behavior.

0 commit comments

Comments
 (0)