|
| 1 | +# 🚀 Phase 3.2: Select Optimization - KICKOFF |
| 2 | + |
| 3 | +**Date:** 2025-01-28 |
| 4 | +**Status:** ✅ **ACTIVE - AGENT MODE** |
| 5 | +**Priority:** 🟡 **HIGH** |
| 6 | +**Target:** 4.1 ms → <1 ms (75% improvement) |
| 7 | + |
| 8 | +--- |
| 9 | + |
| 10 | +## 🎯 Objective |
| 11 | + |
| 12 | +**Optimize `SingleFileStorageProvider` select operations through metadata caching and read-ahead buffering.** |
| 13 | + |
| 14 | +### Current Performance (Baseline): |
| 15 | + |
| 16 | +``` |
| 17 | +SCDB_Single_Select: 4.1 ms |
| 18 | +SCDB_Dir_Select: 910 µs (4.5x faster) |
| 19 | +PageBased_Select: 1.1 ms (3.7x faster) |
| 20 | +
|
| 21 | +Problem: Single-file mode is 4.5x slower than directory mode |
| 22 | +Target: <1 ms (match or exceed directory mode) |
| 23 | +``` |
| 24 | + |
| 25 | +--- |
| 26 | + |
| 27 | +## 🔍 Root Cause Analysis |
| 28 | + |
| 29 | +### Issue #1: Block Metadata Lookup |
| 30 | +**Current Implementation:** |
| 31 | +```csharp |
| 32 | +// Every block read requires registry lookup |
| 33 | +var entry = _blockRegistry.GetEntry(blockName); |
| 34 | +_fileStream.Position = (long)entry.Offset; |
| 35 | +await _fileStream.ReadAsync(buffer, cancellationToken); |
| 36 | +``` |
| 37 | + |
| 38 | +**Problem:** |
| 39 | +- Registry lookup per block read |
| 40 | +- No metadata caching |
| 41 | +- Extra I/O for metadata |
| 42 | + |
| 43 | +**Solution:** LRU metadata cache |
| 44 | + |
| 45 | +--- |
| 46 | + |
| 47 | +### Issue #2: Sequential Scan Inefficiency |
| 48 | +**Current Implementation:** |
| 49 | +```csharp |
| 50 | +// Read blocks one-by-one |
| 51 | +foreach (var blockName in blockNames) |
| 52 | +{ |
| 53 | + var data = await ReadBlockAsync(blockName); |
| 54 | + // No prefetch for next block |
| 55 | +} |
| 56 | +``` |
| 57 | + |
| 58 | +**Problem:** |
| 59 | +- Sequential I/O not optimized |
| 60 | +- No read-ahead for next block |
| 61 | +- Cache-cold reads |
| 62 | + |
| 63 | +**Solution:** Read-ahead buffer |
| 64 | + |
| 65 | +--- |
| 66 | + |
| 67 | +### Issue #3: Memory-Mapped File Overhead |
| 68 | +**Current Implementation:** |
| 69 | +```csharp |
| 70 | +// Create accessor for each read |
| 71 | +using var accessor = _memoryMappedFile.CreateViewAccessor( |
| 72 | + viewOffset, viewLength, MemoryMappedFileAccess.Read); |
| 73 | +``` |
| 74 | + |
| 75 | +**Problem:** |
| 76 | +- Accessor creation overhead |
| 77 | +- No accessor pooling |
| 78 | +- OS handle allocation per read |
| 79 | + |
| 80 | +**Solution:** ViewAccessor pooling |
| 81 | + |
| 82 | +--- |
| 83 | + |
| 84 | +## 🎯 Phase 3.2 Optimizations |
| 85 | + |
| 86 | +### 1. ✅ Block Metadata Cache (LRU) |
| 87 | + |
| 88 | +**Implementation:** |
| 89 | +```csharp |
| 90 | +/// <summary> |
| 91 | +/// ✅ C# 14: LRU cache for block metadata using Lock class. |
| 92 | +/// Reduces registry lookups by caching frequently accessed block entries. |
| 93 | +/// </summary> |
| 94 | +public sealed class BlockMetadataCache |
| 95 | +{ |
| 96 | + private readonly Dictionary<string, CacheEntry> _cache = []; |
| 97 | + private readonly LinkedList<string> _lru = new(); |
| 98 | + private readonly Lock _cacheLock = new(); // C# 14 |
| 99 | + private const int MAX_CACHE_SIZE = 1000; |
| 100 | + |
| 101 | + private sealed record CacheEntry(BlockEntry Entry, DateTime AccessTime); |
| 102 | + |
| 103 | + public bool TryGet(string blockName, out BlockEntry entry) |
| 104 | + { |
| 105 | + lock (_cacheLock) |
| 106 | + { |
| 107 | + if (_cache.TryGetValue(blockName, out var cached)) |
| 108 | + { |
| 109 | + // Move to front (MRU) |
| 110 | + _lru.Remove(blockName); |
| 111 | + _lru.AddFirst(blockName); |
| 112 | + |
| 113 | + // Update access time |
| 114 | + _cache[blockName] = cached with { AccessTime = DateTime.UtcNow }; |
| 115 | + |
| 116 | + entry = cached.Entry; |
| 117 | + return true; |
| 118 | + } |
| 119 | + |
| 120 | + entry = default; |
| 121 | + return false; |
| 122 | + } |
| 123 | + } |
| 124 | + |
| 125 | + public void Add(string blockName, BlockEntry entry) |
| 126 | + { |
| 127 | + lock (_cacheLock) |
| 128 | + { |
| 129 | + if (_cache.Count >= MAX_CACHE_SIZE) |
| 130 | + { |
| 131 | + // Evict LRU |
| 132 | + var lru = _lru.Last!.Value; |
| 133 | + _cache.Remove(lru); |
| 134 | + _lru.RemoveLast(); |
| 135 | + } |
| 136 | + |
| 137 | + _cache[blockName] = new CacheEntry(entry, DateTime.UtcNow); |
| 138 | + _lru.AddFirst(blockName); |
| 139 | + } |
| 140 | + } |
| 141 | + |
| 142 | + public (int Size, double HitRate) GetStatistics() |
| 143 | + { |
| 144 | + lock (_cacheLock) |
| 145 | + { |
| 146 | + // Calculate hit rate from access patterns |
| 147 | + return (_cache.Count, 0.0); // TODO: track hits/misses |
| 148 | + } |
| 149 | + } |
| 150 | +} |
| 151 | +``` |
| 152 | + |
| 153 | +**Expected Impact:** ~1-2ms improvement (25-50% reduction) |
| 154 | + |
| 155 | +--- |
| 156 | + |
| 157 | +### 2. ✅ Read-Ahead Buffer |
| 158 | + |
| 159 | +**Implementation:** |
| 160 | +```csharp |
| 161 | +/// <summary> |
| 162 | +/// ✅ C# 14: Prefetch buffer using Channel for async prefetching. |
| 163 | +/// Predicts sequential access patterns and prefetches next blocks. |
| 164 | +/// </summary> |
| 165 | +public sealed class ReadAheadBuffer |
| 166 | +{ |
| 167 | + private readonly int _bufferSize = 64 * 1024; // 64 KB |
| 168 | + private readonly Channel<PrefetchRequest> _prefetchQueue; |
| 169 | + private readonly Dictionary<string, byte[]> _buffer = []; |
| 170 | + private readonly Lock _bufferLock = new(); // C# 14 |
| 171 | + private readonly Task _prefetchTask; |
| 172 | + private readonly CancellationTokenSource _cts = new(); |
| 173 | + |
| 174 | + private sealed record PrefetchRequest(string BlockName, ulong Offset, int Length); |
| 175 | + |
| 176 | + public ReadAheadBuffer(SingleFileStorageProvider provider) |
| 177 | + { |
| 178 | + _prefetchQueue = Channel.CreateBounded<PrefetchRequest>(10); |
| 179 | + _prefetchTask = Task.Run(() => PrefetchWorkerAsync(provider), _cts.Token); |
| 180 | + } |
| 181 | + |
| 182 | + public void PrefetchAsync(string blockName, ulong offset, int length) |
| 183 | + { |
| 184 | + // Non-blocking hint to prefetch |
| 185 | + _prefetchQueue.Writer.TryWrite(new(blockName, offset, length)); |
| 186 | + } |
| 187 | + |
| 188 | + public bool TryGetPrefetched(string blockName, out byte[] data) |
| 189 | + { |
| 190 | + lock (_bufferLock) |
| 191 | + { |
| 192 | + return _buffer.Remove(blockName, out data!); |
| 193 | + } |
| 194 | + } |
| 195 | + |
| 196 | + private async Task PrefetchWorkerAsync(SingleFileStorageProvider provider) |
| 197 | + { |
| 198 | + await foreach (var request in _prefetchQueue.Reader.ReadAllAsync(_cts.Token)) |
| 199 | + { |
| 200 | + try |
| 201 | + { |
| 202 | + // Read block into buffer |
| 203 | + var data = await provider.ReadBlockInternalAsync( |
| 204 | + request.BlockName, request.Offset, request.Length, _cts.Token); |
| 205 | + |
| 206 | + lock (_bufferLock) |
| 207 | + { |
| 208 | + _buffer[request.BlockName] = data; |
| 209 | + } |
| 210 | + } |
| 211 | + catch |
| 212 | + { |
| 213 | + // Ignore prefetch errors |
| 214 | + } |
| 215 | + } |
| 216 | + } |
| 217 | + |
| 218 | + public void Dispose() |
| 219 | + { |
| 220 | + _cts.Cancel(); |
| 221 | + _prefetchTask.Wait(TimeSpan.FromSeconds(1)); |
| 222 | + _cts.Dispose(); |
| 223 | + } |
| 224 | +} |
| 225 | +``` |
| 226 | + |
| 227 | +**Expected Impact:** ~1-2ms improvement for sequential scans |
| 228 | + |
| 229 | +--- |
| 230 | + |
| 231 | +### 3. ✅ ViewAccessor Pooling |
| 232 | + |
| 233 | +**Implementation:** |
| 234 | +```csharp |
| 235 | +/// <summary> |
| 236 | +/// ✅ C# 14: Pool of MemoryMappedViewAccessor for reuse. |
| 237 | +/// Reduces OS handle allocation overhead. |
| 238 | +/// </summary> |
| 239 | +public sealed class ViewAccessorPool |
| 240 | +{ |
| 241 | + private readonly ConcurrentBag<MemoryMappedViewAccessor> _pool = []; |
| 242 | + private readonly MemoryMappedFile _mmf; |
| 243 | + private const int MAX_POOL_SIZE = 10; |
| 244 | + |
| 245 | + public ViewAccessorPool(MemoryMappedFile mmf) |
| 246 | + { |
| 247 | + _mmf = mmf; |
| 248 | + } |
| 249 | + |
| 250 | + public MemoryMappedViewAccessor Rent(long offset, long length) |
| 251 | + { |
| 252 | + if (_pool.TryTake(out var accessor)) |
| 253 | + { |
| 254 | + // Reuse existing accessor if it fits |
| 255 | + if (accessor.Capacity >= length) |
| 256 | + { |
| 257 | + return accessor; |
| 258 | + } |
| 259 | + |
| 260 | + accessor.Dispose(); |
| 261 | + } |
| 262 | + |
| 263 | + // Create new accessor |
| 264 | + return _mmf.CreateViewAccessor(offset, length, MemoryMappedFileAccess.Read); |
| 265 | + } |
| 266 | + |
| 267 | + public void Return(MemoryMappedViewAccessor accessor) |
| 268 | + { |
| 269 | + if (_pool.Count < MAX_POOL_SIZE) |
| 270 | + { |
| 271 | + _pool.Add(accessor); |
| 272 | + } |
| 273 | + else |
| 274 | + { |
| 275 | + accessor.Dispose(); |
| 276 | + } |
| 277 | + } |
| 278 | + |
| 279 | + public void Dispose() |
| 280 | + { |
| 281 | + while (_pool.TryTake(out var accessor)) |
| 282 | + { |
| 283 | + accessor.Dispose(); |
| 284 | + } |
| 285 | + } |
| 286 | +} |
| 287 | +``` |
| 288 | + |
| 289 | +**Expected Impact:** ~0.5ms improvement |
| 290 | + |
| 291 | +--- |
| 292 | + |
| 293 | +## 📊 Expected Performance Impact |
| 294 | + |
| 295 | +``` |
| 296 | +Current: 4.1 ms |
| 297 | +After Metadata Cache: ~2.5 ms (-1.6ms, 39%) |
| 298 | +After Read-Ahead: ~1.2 ms (-1.3ms, 52%) |
| 299 | +After Accessor Pool: ~0.8 ms (-0.4ms, 33%) |
| 300 | +
|
| 301 | +Target: <1 ms |
| 302 | +Expected Result: ~0.8 ms (80% improvement, 5x faster) 🚀 |
| 303 | +``` |
| 304 | + |
| 305 | +--- |
| 306 | + |
| 307 | +## 🔥 Modern C# 14 Features |
| 308 | + |
| 309 | +1. **Lock Class** - Modern synchronization |
| 310 | +2. **Channel<T>** - Async prefetching |
| 311 | +3. **ConcurrentBag<T>** - Lock-free pooling |
| 312 | +4. **Record Types** - Cache entries |
| 313 | +5. **with Expression** - Update cache entries |
| 314 | +6. **Collection Expressions** - `[]` for collections |
| 315 | + |
| 316 | +--- |
| 317 | + |
| 318 | +## 📋 Implementation Checklist |
| 319 | + |
| 320 | +- [ ] Create `BlockMetadataCache.cs` |
| 321 | +- [ ] Create `ReadAheadBuffer.cs` |
| 322 | +- [ ] Create `ViewAccessorPool.cs` |
| 323 | +- [ ] Integrate cache in `SingleFileStorageProvider` |
| 324 | +- [ ] Integrate read-ahead in `SingleFileStorageProvider` |
| 325 | +- [ ] Integrate accessor pool in `SingleFileStorageProvider` |
| 326 | +- [ ] Create `Phase3_2_SelectOptimizationTests.cs` |
| 327 | +- [ ] Run benchmarks |
| 328 | +- [ ] Validate <1ms target |
| 329 | + |
| 330 | +--- |
| 331 | + |
| 332 | +## ✅ Success Criteria |
| 333 | + |
| 334 | +- ✅ Select operations <1 ms |
| 335 | +- ✅ Cache hit rate >90% |
| 336 | +- ✅ All tests passing |
| 337 | +- ✅ No memory leaks |
| 338 | +- ✅ Backward compatible |
| 339 | + |
| 340 | +--- |
| 341 | + |
| 342 | +**Status:** READY TO START 🚀 |
| 343 | +**Next:** Implement BlockMetadataCache |
0 commit comments