99namespace 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 ) ]
2854public 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+ }
0 commit comments