A read-only, range-based, sequential-optimized cache with decision-driven background rebalancing, smart eventual consistency, and intelligent work avoidance.
- Overview
- Key Features
- Decision-Driven Rebalance Execution
- Sliding Window Cache Concept
- Understanding the Sliding Window
- Materialization for Fast Access
- Usage Example
- Configuration
- Optional Diagnostics
- Documentation
- Performance Considerations
- CI/CD & Package Information
- Contributing & Feedback
- License
The Sliding Window Cache is a high-performance caching library designed for scenarios where data is accessed in sequential or predictable patterns across ranges. It automatically prefetches and maintains a "window" of data around the most recently requested range, significantly reducing the need for repeated data source queries.
- Automatic Prefetching: Intelligently prefetches data on both sides of requested ranges based on configurable coefficients
- Smart Eventual Consistency: Decision-driven rebalance execution with multi-stage analytical validation ensures the cache converges to optimal configuration while avoiding unnecessary work
- Work Avoidance Through Validation: Multi-stage decision pipeline (NoRebalanceRange containment, pending rebalance coverage, cache geometry analysis) prevents thrashing, reduces redundant I/O, and maintains system stability under rapidly changing access patterns
- Background Rebalancing: Asynchronously adjusts the cache window when validation confirms necessity, with debouncing to control convergence timing
- Opportunistic Execution: Rebalance operations may be skipped when validation determines they are unnecessary ( intent represents observed access, not mandatory work)
- Single-Writer Architecture: User Path is read-only; only Rebalance Execution mutates cache state, eliminating race conditions with cancellation support for coordination
- Range-Based Operations: Built on top of the
Intervals.NETlibrary for robust range handling - Configurable Read Modes: Choose between different materialization strategies based on your performance requirements
- Optional Diagnostics: Built-in instrumentation for monitoring cache behavior and validating system invariants
- Full Cancellation Support: User-provided
CancellationTokenpropagates through the async pipeline; rebalance operations support cancellation at all stages
The cache uses a sophisticated decision-driven model where rebalance necessity is determined by analytical validation rather than blindly executing every user request. This prevents thrashing, reduces unnecessary I/O, and maintains stability under rapid access pattern changes.
Visual Flow:
User Request
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β User Path (User Thread - Synchronous) β
β β’ Read from cache or fetch missing data β
β β’ Return data immediately to user β
β β’ Publish intent with delivered data β
ββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β Decision Engine (User Thread - CPU-only) β
β Stage 1: NoRebalanceRange check β
β Stage 2: Pending coverage check β
β Stage 3: Desired == Current check β
β β Decision: SKIP or SCHEDULE β
ββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ
β
ββββ If SKIP: return (work avoidance) β
β
ββββ If SCHEDULE:
β
βΌ
βββββββββββββββββββββββββββββββββββββββ
β Background Rebalance (ThreadPool) β
β β’ Debounce delay β
β β’ Fetch missing data (I/O) β
β β’ Normalize cache to desired range β
β β’ Update cache state atomically β
βββββββββββββββββββββββββββββββββββββββ
Key Points:
- User requests never block - data returned immediately, rebalance happens later
- Decision happens synchronously - validation is CPU-only (microseconds), happens in user thread before scheduling
- Work avoidance prevents thrashing - validation may skip rebalance entirely if unnecessary
- Only I/O happens in background - debounce + data fetching + cache updates run asynchronously
- Smart eventual consistency - cache converges to optimal state while avoiding unnecessary operations
Why This Matters:
- Handles request bursts correctly: First request schedules rebalance, subsequent requests validate and skip if pending rebalance covers them
- No background queue buildup: Decisions made immediately, not queued
- Prevents oscillation: Stage 2 validation checks if pending rebalance will satisfy request
- Lightweight: Decision logic is pure CPU (math, conditions), no I/O blocking
For complete architectural details, see:
- Concurrency Model - Smart eventual consistency and synchronous decision execution
- Invariants - Multi-stage validation pipeline specification (Section D)
- Scenario Model - Temporal behavior and decision scenarios
Traditional caches work with individual keys. A sliding window cache, in contrast, operates on continuous ranges of data:
- User requests a range (e.g., records 100-200)
- Cache fetches more than requested (e.g., records 50-300) based on configured left/right cache coefficients
- Subsequent requests within the window are served instantly from materialized data
- Window automatically rebalances when the user moves outside threshold boundaries
This pattern is ideal for:
- Time-series data (sensor readings, logs, metrics)
- Paginated datasets with forward/backward navigation
- Sequential data processing (video frames, audio samples)
- Any scenario with high spatial or temporal locality of access
When you request a range, the cache actually fetches and stores a larger window:
Requested Range (what user asks for):
[======== USER REQUEST ========]
Actual Cache Window (what cache stores):
[=== LEFT BUFFER ===][======== USER REQUEST ========][=== RIGHT BUFFER ===]
β leftCacheSize requestedRange size rightCacheSize β
The left and right buffers are calculated as multiples of the requested range size using the leftCacheSize and
rightCacheSize coefficients.
Rebalancing occurs when a new request moves outside the threshold boundaries:
Current Cache Window:
[========*===================== CACHE ======================*=======]
β β
Left Threshold (20%) Right Threshold (20%)
Scenario 1: Request within thresholds β No rebalance
[========*===================== CACHE ======================*=======]
[---- new request ----] β Served from cache
Scenario 2: Request outside threshold β Rebalance triggered
[========*===================== CACHE ======================*=======]
[---- new request ----]
β
π Rebalance: Shift window right
How coefficients control the cache window size:
Example: User requests range of size 100
leftCacheSize = 1.0, rightCacheSize = 2.0
[==== 100 ====][======= 100 =======][============ 200 ============]
Left Buffer Requested Range Right Buffer
Total Cache Window = 100 + 100 + 200 = 400 items
leftThreshold = 0.2 (20% of 400 = 80 items)
rightThreshold = 0.2 (20% of 400 = 80 items)
Key insight: Threshold percentages are calculated based on the total cache window size, not individual buffer sizes.
The cache always materializes the data it fetches, meaning it stores the data in memory in a directly accessible format (arrays or lists) rather than keeping lazy enumerables. This design choice ensures:
- Fast, predictable read performance: No deferred execution chains on the hot path
- Multiple reads without re-enumeration: The same data can be read many times at zero cost (in Snapshot mode)
- Clean separation of concerns: Data fetching (I/O-bound) is decoupled from data serving (CPU-bound)
The cache supports two materialization strategies, configured at creation time via the UserCacheReadMode enum:
Storage: Contiguous array (TData[])
Read behavior: Returns ReadOnlyMemory<TData> pointing directly to internal array
Rebalance behavior: Always allocates a new array
Advantages:
- β Zero allocations on read β no memory overhead per request
- β Fastest read performance β direct memory view
- β Ideal for read-heavy scenarios with frequent access to cached data
Disadvantages:
- β Expensive rebalancing β always allocates a new array, even if size is unchanged
- β Large Object Heap (LOH) pressure β arrays β₯85,000 bytes go to LOH, which can cause fragmentation
- β Higher memory usage during rebalance (old + new arrays temporarily coexist)
Best for:
- Applications that read the same data many times
- Scenarios where cache updates are infrequent relative to reads
- Systems with ample memory and minimal LOH concerns
Storage: Growable list (List<TData>)
Read behavior: Allocates a new array and copies the requested range
Rebalance behavior: Uses List<T> operations (Clear + AddRange)
Advantages:
- β
Cheaper rebalancing β
List<T>can grow without always allocating large arrays - β Reduced LOH pressure β avoids large contiguous allocations in most cases
- β Ideal for memory-sensitive scenarios or when rebalancing is frequent
Disadvantages:
- β Allocates on every read β new array per request
- β Copy overhead β data must be copied from list to array
- β Slower read performance compared to Snapshot mode
Best for:
- Applications with frequent cache rebalancing
- Memory-constrained environments
- Scenarios where each range is typically read once or twice
- Systems sensitive to LOH fragmentation
Quick Decision Guide:
| Your Scenario | Recommended Mode | Why |
|---|---|---|
| Read data many times | Snapshot | Zero-allocation reads |
| Frequent rebalancing | CopyOnRead | Cheaper cache updates |
| Large cache (>85KB) | CopyOnRead | Avoid LOH pressure |
| Memory constrained | CopyOnRead | Better memory behavior |
| Read-once patterns | CopyOnRead | Copy cost already paid |
| Read-heavy workload | Snapshot | Direct memory access |
For detailed comparison, performance benchmarks, multi-level cache composition patterns, and staging buffer implementation details, see Storage Strategies Guide.
using SlidingWindowCache;
using SlidingWindowCache.Configuration;
using Intervals.NET;
using Intervals.NET.Domain.Default.Numeric;
// Configure the cache behavior
var options = new WindowCacheOptions(
leftCacheSize: 1.0, // Cache 100% of requested range size to the left
rightCacheSize: 2.0, // Cache 200% of requested range size to the right
leftThreshold: 0.2, // Rebalance if <20% left buffer remains
rightThreshold: 0.2 // Rebalance if <20% right buffer remains
);
// Create cache with Snapshot mode (zero-allocation reads)
var cache = WindowCache<int, string, IntegerFixedStepDomain>.Create(
dataSource: myDataSource,
domain: new IntegerFixedStepDomain(),
options: options,
readMode: UserCacheReadMode.Snapshot
);
// Request data - returns ReadOnlyMemory<string>
var data = await cache.GetDataAsync(
Range.Closed(100, 200),
cancellationToken
);
// Access the data
foreach (var item in data.Span)
{
Console.WriteLine(item);
}The WindowCacheOptions class provides fine-grained control over cache behavior. Understanding these parameters is
essential for optimal performance.
leftCacheSize (double, default: 1.0)
- Definition: Multiplier applied to the requested range size to determine the left buffer size
- Practical meaning: How much data to prefetch before the requested range
- Example: If user requests 100 items and
leftCacheSize = 1.5, the cache prefetches 150 items to the left - Typical values: 0.5 to 2.0 (depending on backward navigation patterns)
rightCacheSize (double, default: 2.0)
- Definition: Multiplier applied to the requested range size to determine the right buffer size
- Practical meaning: How much data to prefetch after the requested range
- Example: If user requests 100 items and
rightCacheSize = 2.0, the cache prefetches 200 items to the right - Typical values: 1.0 to 3.0 (higher for forward-scrolling scenarios)
leftThreshold (double, default: 0.2)
- Definition: Percentage of the total cache window size that triggers rebalancing when crossed on the left
- Calculation:
leftThreshold Γ (Left Buffer + Requested Range + Right Buffer) - Example: With total window of 400 items and
leftThreshold = 0.2, rebalance triggers when user moves within 80 items of the left edge - Typical values: 0.15 to 0.3 (lower = more aggressive rebalancing)
rightThreshold (double, default: 0.2)
- Definition: Percentage of the total cache window size that triggers rebalancing when crossed on the right
- Calculation:
rightThreshold Γ (Left Buffer + Requested Range + Right Buffer) - Example: With total window of 400 items and
rightThreshold = 0.2, rebalance triggers when user moves within 80 items of the right edge - Typical values: 0.15 to 0.3 (lower = more aggressive rebalancing)
debounceDelay (TimeSpan, default: 50ms)
- Definition: Minimum time delay before executing a rebalance operation after it's triggered
- Purpose: Prevents cache thrashing when user rapidly changes access patterns
- Behavior: If multiple rebalance requests occur within the debounce window, only the last one executes
- Typical values: 20ms to 200ms (depending on data source latency)
- Trade-off: Higher values reduce rebalance frequency but may delay cache optimization
Forward-heavy scrolling (e.g., log viewer, video player):
var options = new WindowCacheOptions(
leftCacheSize: 0.5, // Minimal backward buffer
rightCacheSize: 3.0, // Aggressive forward prefetching
leftThreshold: 0.25,
rightThreshold: 0.15 // Trigger rebalance earlier when moving forward
);Bidirectional navigation (e.g., paginated data grid):
var options = new WindowCacheOptions(
leftCacheSize: 1.5, // Balanced backward buffer
rightCacheSize: 1.5, // Balanced forward buffer
leftThreshold: 0.2,
rightThreshold: 0.2
);Aggressive prefetching with stability (e.g., high-latency data source):
var options = new WindowCacheOptions(
leftCacheSize: 2.0,
rightCacheSize: 3.0,
leftThreshold: 0.1, // Rebalance early to maintain large buffers
rightThreshold: 0.1,
debounceDelay: TimeSpan.FromMilliseconds(100) // Wait for access pattern to stabilize
);The cache supports optional diagnostics for monitoring behavior, measuring performance, and validating system invariants. This is useful for:
- Testing and validation: Verify cache behavior meets expected patterns
- Performance monitoring: Track cache hit/miss ratios and rebalance frequency
- Debugging: Understand cache lifecycle events in development
- Production observability: Optional instrumentation for metrics collection
You MUST handle the RebalanceExecutionFailed event in production applications.
Rebalance operations run in fire-and-forget background tasks. When exceptions occur, they are silently swallowed to
prevent application crashes. Without proper handling of RebalanceExecutionFailed:
- β Silent failures in background operations
- β Cache stops rebalancing with no indication
- β Degraded performance with no diagnostics
- β Data source errors go unnoticed
Minimum requirement: Log all failures
public class LoggingCacheDiagnostics : ICacheDiagnostics
{
private readonly ILogger<LoggingCacheDiagnostics> _logger;
public LoggingCacheDiagnostics(ILogger<LoggingCacheDiagnostics> logger)
{
_logger = logger;
}
public void RebalanceExecutionFailed(Exception ex)
{
// CRITICAL: Always log rebalance failures
_logger.LogError(ex, "Cache rebalance execution failed. Cache may not be optimally sized.");
}
// ...implement other methods (can be no-op if you only care about failures)...
}For production systems, consider:
- Alerting: Trigger alerts after N consecutive failures
- Metrics: Track failure rate and exception types
- Circuit breaker: Disable rebalancing after repeated failures
- Structured logging: Include cache state and requested range context
using SlidingWindowCache.Infrastructure.Instrumentation;
// Create diagnostics instance
var diagnostics = new EventCounterCacheDiagnostics();
// Pass to cache constructor
var cache = new WindowCache<int, string, IntegerFixedStepDomain>(
dataSource: myDataSource,
domain: new IntegerFixedStepDomain(),
options: options,
cacheDiagnostics: diagnostics // Optional parameter
);
// Access diagnostic counters
Console.WriteLine($"Full cache hits: {diagnostics.UserRequestFullCacheHit}");
Console.WriteLine($"Rebalances completed: {diagnostics.RebalanceExecutionCompleted}");If no diagnostics instance is provided (default), the cache uses NoOpDiagnostics - a zero-overhead implementation with
empty method bodies that the JIT compiler can optimize away completely. This ensures diagnostics add zero performance
overhead when not used.
For complete metric descriptions, custom implementations, and advanced patterns, see Diagnostics Guide.
For detailed architectural documentation, see:
- Intervals.NET - Robust interval and range handling library that
underpins cache logic. See README and documentation for core concepts like
Range,Domain,RangeData, and interval operations.
- Invariants - Complete list of system invariants and guarantees
- Scenario Model - Temporal behavior scenarios (User Path, Decision Path, Rebalance Execution)
- Actors & Responsibilities - System actors and invariant ownership mapping
- Actors to Components Mapping - How architectural actors map to concrete components
- Cache State Machine - Formal state machine with mutation ownership and concurrency semantics
- Concurrency Model - Single-writer architecture and eventual consistency model
- Component Map - Comprehensive component catalog with responsibilities and interactions
- Storage Strategies - Detailed comparison of Snapshot vs. CopyOnRead modes and multi-level cache patterns
- Diagnostics - Optional instrumentation and observability guide
- Invariant Test Suite README - Comprehensive invariant test suite with deterministic synchronization
- Benchmark Suite README - BenchmarkDotNet performance
benchmarks
- RebalanceFlowBenchmarks - Behavior-driven rebalance cost analysis (Fixed/Growing/Shrinking span patterns)
- UserFlowBenchmarks - User-facing API latency (Full hit, Partial hit, Full miss scenarios)
- ScenarioBenchmarks - End-to-end cold start performance
- Storage Strategy Comparison - Snapshot vs CopyOnRead allocation and performance tradeoffs across all suites
- Deterministic Testing:
WaitForIdleAsync()API provides race-free synchronization with background rebalance operations for testing, graceful shutdown, health checks, and integration scenarios
-
Single-Writer Architecture: Only Rebalance Execution writes to cache state; User Path is read-only. Multiple rebalance executions are serialized via
SemaphoreSlimto guarantee only one execution writes to cache at a time. This eliminates race conditions and data corruption through architectural constraints and execution serialization. See Concurrency Model. -
Decision-Driven Execution: Rebalance necessity determined by synchronous CPU-only analytical validation in user thread (microseconds). Enables immediate work avoidance and prevents intent thrashing. See Invariants - Section D.
-
Multi-Stage Validation Pipeline:
- Stage 1: NoRebalanceRange containment check (fast-path rejection)
- Stage 2: Pending rebalance coverage check (anti-thrashing)
- Stage 3: Desired == Current check (no-op prevention)
Rebalance executes ONLY if ALL stages confirm necessity. See Scenario Model - Decision Path.
-
Smart Eventual Consistency: Cache converges to optimal configuration asynchronously while avoiding unnecessary work through validation. System prioritizes decision correctness and work avoidance over aggressive rebalance responsiveness. See Concurrency Model - Smart Eventual Consistency.
-
Intent Semantics: Intents represent observed access patterns (signals), not mandatory work (commands). Publishing an intent does not guarantee rebalance execution - validation determines necessity. See Invariants C.24.
-
Cache Contiguity Rule: Cache data must always remain contiguous (no gaps allowed). Non-intersecting requests fully replace the cache rather than creating partial/gapped states. See Invariants A.9a.
-
User Path Priority: User requests always served immediately. When validation confirms new rebalance is necessary, pending rebalance is cancelled and rescheduled. Cancellation is mechanical coordination (prevents concurrent executions), not a decision mechanism. See Cache State Machine.
-
Lock-Free Concurrency: Intent management uses
Volatile.Read/WriteandInterlocked.Exchangefor atomic operations - no locks, no race conditions, guaranteed progress. Execution serialization viaSemaphoreSlimensures single-writer semantics. Thread-safety achieved through architectural constraints and atomic operations. See Concurrency Model - Lock-Free Implementation.
- Snapshot mode: O(1) reads, but O(n) rebalance with array allocation
- CopyOnRead mode: O(n) reads (copy cost), but cheaper rebalance operations
- Rebalancing is asynchronous: Does not block user reads
- Debouncing: Multiple rapid requests trigger only one rebalance operation
- Diagnostics overhead: Zero when not used (NoOpDiagnostics); minimal when enabled (~1-5ns per event)
This project uses GitHub Actions for automated testing and deployment:
-
Build & Test: Runs on every push and pull request
- Compiles entire solution in Release configuration
- Executes all test suites (Unit, Integration, Invariants) with code coverage
- Validates WebAssembly compatibility via
net8.0-browsercompilation - Uploads coverage reports to Codecov
-
NuGet Publishing: Automatic on main branch pushes
- Packages library with symbols and source link
- Publishes to NuGet.org with skip-duplicate
- Stores package artifacts in workflow runs
SlidingWindowCache is validated for WebAssembly compatibility:
- Target Framework:
net8.0-browsercompilation validated in CI - Validation Project:
SlidingWindowCache.WasmValidationensures all public APIs work in browser environments - Compatibility: All library features available in Blazor WebAssembly and other WASM scenarios
Package ID: SlidingWindowCache
Current Version: 1.0.0
# Install via .NET CLI
dotnet add package SlidingWindowCache
# Install via Package Manager
Install-Package SlidingWindowCachePackage Contents:
- Main library assembly (
SlidingWindowCache.dll) - Debug symbols (
.snupkgfor debugging) - Source Link (GitHub source integration for "Go to Definition")
- README.md (this file)
Dependencies:
- Intervals.NET.Data (>= 0.0.1)
- Intervals.NET.Domain.Default (>= 0.0.2)
- Intervals.NET.Domain.Extensions (>= 0.0.3)
- .NET 8.0 or higher
This project is a personal R&D and engineering exploration focused on cache design patterns, concurrent systems architecture, and performance optimization. While it's primarily a research endeavor, feedback and community input are highly valued and welcomed.
- Bug reports - Found an issue? Please open a GitHub issue with reproduction steps
- Feature suggestions - Have ideas for improvements? Start a discussion or open an issue
- Performance insights - Benchmarked the cache in your scenario? Share your findings
- Architecture feedback - Thoughts on the design patterns or implementation? Let's discuss
- Documentation improvements - Found something unclear? Contributions to docs are appreciated
- Positive feedback - If the library is useful to you, that's great to know!
- Issues: Use GitHub Issues for bugs, feature requests, or questions
- Discussions: Use GitHub Discussions for broader topics, ideas, or design conversations
- Pull Requests: Code contributions are welcome, but please open an issue first to discuss significant changes
This project benefits from community feedback while maintaining a focused research direction. All constructive input helps improve the library's design, implementation, and documentation.
MIT