// MIT License - Copyright (c) 2026 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Core.DataStructure { using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; using UnityEngine; using WallstopStudios.UnityHelpers.Core.Random; #if !SINGLE_THREADED using System.Collections.Concurrent; #endif /// /// A high-performance, configurable cache with multiple eviction policies, /// time-based expiration, dynamic sizing, and comprehensive callbacks. /// /// /// cache = CacheBuilder.NewBuilder() /// .MaximumSize(1000) /// .ExpireAfterWrite(TimeSpan.FromMinutes(5)) /// .Build(); /// /// cache.Set("user1", userData); /// if (cache.TryGet("user1", out UserData data)) /// { /// // Use cached data /// } /// /// // Loading cache /// Cache loadingCache = CacheBuilder.NewBuilder() /// .MaximumSize(100) /// .Build(key => ComputeExpensiveResult(key)); /// /// ExpensiveResult result = loadingCache.GetOrAdd(42, null); /// ]]> /// /// The type of keys in the cache. /// The type of values in the cache. public sealed class Cache : IDisposable { [StructLayout(LayoutKind.Sequential)] private struct CacheEntry { public TKey Key; public TValue Value; public float WriteTime; public float AccessTime; public float ExpirationTime; public int Frequency; public int PrevIndex; public int NextIndex; public byte SegmentIndex; public bool IsAlive; public long Weight; } private const int InvalidIndex = -1; private const int ProbationSegment = 0; private const int ProtectedSegment = 1; private const float ThrashWindowSeconds = 1f; private readonly CacheOptions _options; private readonly Func _timeProvider; // Lazy initialization to avoid triggering PRNG static initialization during Cache construction, // which can cause deadlocks during Unity's "Open Project: Open Scene" phase. private IRandom _random; private IRandom Random => _random ??= PRNG.Instance; private ReaderWriterLockSlim _lock; #if SINGLE_THREADED private readonly Dictionary _keyToIndex; #else private readonly ConcurrentDictionary _keyToIndex; #endif private CacheEntry[] _entries; private int _count; private int _capacity; private long _currentWeight; private int _lruHead; private int _lruTail; private int _fifoHead; private int _fifoTail; private int _probationHead; private int _probationTail; private int _protectedHead; private int _protectedTail; private int _probationCount; private int _protectedCount; private int _protectedCapacity; private int _freeListHead; private long _hitCount; private long _missCount; private long _evictionCount; private long _loadCount; private long _expiredCount; private int _peakSize; private int _growthEvents; private float _lastEvictionTime; private int _recentEvictionCount; private bool _disposed; /// /// Gets the current number of entries in the cache. /// public int Count => _count; /// /// Gets the total weight of all entries when using weighted caching. /// public long Size => _options.Weigher != null ? _currentWeight : _count; /// /// Gets the configured maximum number of entries the cache can hold. /// The cache may start smaller and grow up to this limit. /// public int MaximumSize => _options.MaximumSize; /// /// Gets the current internal capacity of the cache. /// This may be smaller than if the cache hasn't grown yet. /// public int Capacity => _capacity; /// /// Gets an enumerable of all keys currently in the cache. /// Note: This property allocates a state machine on each access. For zero-allocation /// enumeration, use instead. /// public IEnumerable Keys { get { float currentTime = _timeProvider(); for (int i = 0; i < _entries.Length; i++) { if (_entries[i].IsAlive && !IsExpired(i, currentTime)) { yield return _entries[i].Key; } } } } /// /// Populates a list with all keys currently in the cache. /// This method is zero-allocation if the list has sufficient capacity. /// /// The list to populate with keys. Will be cleared before populating. public void GetKeys(List keys) { if (keys == null || _disposed) { return; } keys.Clear(); float currentTime = _timeProvider(); for (int i = 0; i < _entries.Length; i++) { if (_entries[i].IsAlive && !IsExpired(i, currentTime)) { keys.Add(_entries[i].Key); } } } /// /// Creates a new cache with the specified options. /// /// Cache configuration options. public Cache(CacheOptions options) { if (options.MaximumSize <= 0) { options.MaximumSize = CacheOptions.DefaultMaximumSize; } if (options.ProtectedRatio <= 0f) { options.ProtectedRatio = CacheOptions.DefaultProtectedRatio; } if (options.GrowthFactor <= 1f) { options.GrowthFactor = CacheOptions.DefaultGrowthFactor; } if (options.ThrashThresholdEvictionsPerSecond <= 0f) { options.ThrashThresholdEvictionsPerSecond = CacheOptions< TKey, TValue >.DefaultThrashThreshold; } #pragma warning disable CS0618 // Type or member is obsolete if (options.Policy == EvictionPolicy.None) { options.Policy = CacheOptions.DefaultEvictionPolicy; } #pragma warning restore CS0618 _options = options; _timeProvider = options.TimeProvider ?? DefaultTimeProvider; // Note: _random is now lazy-initialized via the Random property to avoid // triggering PRNG static initialization during Cache construction. // Determine initial capacity for internal data structures. // Use InitialCapacity if specified, otherwise default to MaximumSize to ensure the cache // can hold the expected number of items without requiring growth. // Clamp to prevent excessive initial allocations while respecting MaximumSize as upper bound. int requestedInitialCapacity = options.InitialCapacity > 0 ? options.InitialCapacity : options.MaximumSize; // Clamp to reasonable bounds: at least 1, at most min(MaxReasonableInitialCapacity, MaximumSize) int maxInitialCapacity = Math.Min( CacheOptions.MaxReasonableInitialCapacity, options.MaximumSize ); int initialCapacity = Math.Max( 1, Math.Min(requestedInitialCapacity, maxInitialCapacity) ); _capacity = initialCapacity; #if SINGLE_THREADED _keyToIndex = new Dictionary(initialCapacity); #else _keyToIndex = new ConcurrentDictionary( Environment.ProcessorCount, initialCapacity ); #endif _lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); _entries = new CacheEntry[initialCapacity]; InitializeFreeList(); InitializeLinkedLists(); } // Use Stopwatch for timing instead of Time.realtimeSinceStartup to avoid // hanging during Unity's early initialization (e.g., during "Open Scene"). // Time.realtimeSinceStartup can block or behave unexpectedly when accessed // during static initialization before Unity is fully loaded. private static readonly System.Diagnostics.Stopwatch Stopwatch = System.Diagnostics.Stopwatch.StartNew(); private static float DefaultTimeProvider() { return (float)Stopwatch.Elapsed.TotalSeconds; } private void InitializeFreeList() { _freeListHead = 0; for (int i = 0; i < _entries.Length - 1; i++) { _entries[i].NextIndex = i + 1; } _entries[_entries.Length - 1].NextIndex = InvalidIndex; } private void InitializeLinkedLists() { _lruHead = InvalidIndex; _lruTail = InvalidIndex; _fifoHead = InvalidIndex; _fifoTail = InvalidIndex; _probationHead = InvalidIndex; _probationTail = InvalidIndex; _protectedHead = InvalidIndex; _protectedTail = InvalidIndex; _protectedCapacity = (int)(_capacity * _options.ProtectedRatio); } /// /// Attempts to get a value from the cache. /// /// The key to look up. /// The cached value if found. /// True if the key was found and not expired, false otherwise. public bool TryGet(TKey key, out TValue value) { if (_disposed) { value = default; return false; } _lock.EnterReadLock(); try { return TryGetUnlocked(key, out value); } finally { _lock.ExitReadLock(); } } private bool TryGetUnlocked(TKey key, out TValue value) { if (key == null || _disposed) { value = default; return false; } if (!_keyToIndex.TryGetValue(key, out int index)) { RecordMiss(); value = default; return false; } float currentTime = _timeProvider(); if (!_entries[index].IsAlive || IsExpired(index, currentTime)) { EvictEntry(index, EvictionReason.Expired); RecordMiss(); RecordExpired(); value = default; return false; } value = _entries[index].Value; OnAccess(index, currentTime); RecordHit(); InvokeOnGet(key, value); return true; } /// /// Gets a value from the cache, computing it if not present. /// /// The key to look up. /// Factory function to compute the value. If null, uses the default loader. /// The cached or computed value. public TValue GetOrAdd(TKey key, Func factory) { if (key == null || _disposed) { return default; } _lock.EnterUpgradeableReadLock(); try { if (TryGetUnlocked(key, out TValue value)) { return value; } _lock.EnterWriteLock(); try { if (TryGetUnlocked(key, out value)) { return value; } Func actualFactory = factory ?? _options.Loader; if (actualFactory == null) { return default; } TValue newValue; try { newValue = actualFactory(key); RecordLoad(); } catch { return default; } SetUnlocked(key, newValue); return newValue; } finally { _lock.ExitWriteLock(); } } finally { _lock.ExitUpgradeableReadLock(); } } /// /// Adds or updates an entry in the cache. /// /// The key of the entry. /// The value to cache. public void Set(TKey key, TValue value) { Set(key, value, null); } /// /// Adds or updates an entry in the cache with a custom TTL. /// /// The key of the entry. /// The value to cache. /// Custom TTL in seconds. If null, uses default expiration. public void Set(TKey key, TValue value, float? ttlSeconds) { if (key == null || _disposed) { return; } _lock.EnterWriteLock(); try { SetUnlocked(key, value, ttlSeconds); } finally { _lock.ExitWriteLock(); } } private void SetUnlocked(TKey key, TValue value, float? ttlSeconds = null) { float currentTime = _timeProvider(); if (_keyToIndex.TryGetValue(key, out int existingIndex)) { TValue oldValue = _entries[existingIndex].Value; long oldWeight = _options.Weigher != null ? _entries[existingIndex].Weight : 0; _entries[existingIndex].Value = value; _entries[existingIndex].WriteTime = currentTime; _entries[existingIndex].AccessTime = currentTime; _entries[existingIndex].ExpirationTime = ComputeExpirationTime( key, value, currentTime, ttlSeconds ); if (_options.Weigher != null) { long newWeight = _options.Weigher(key, value); _entries[existingIndex].Weight = newWeight; _currentWeight = _currentWeight - oldWeight + newWeight; } OnAccess(existingIndex, currentTime); InvokeOnEviction(key, oldValue, EvictionReason.Replaced); InvokeOnSet(key, value); return; } long weight = _options.Weigher != null ? _options.Weigher(key, value) : 0; // Evict until we have room for the new weight while ( _options.Weigher != null && _currentWeight + weight > _options.MaximumWeight && _count > 0 ) { if (_options.AllowGrowth && ShouldGrow()) { Grow(); break; } EvictOne(EvictionReason.Capacity); } // For non-weighted caches, use standard capacity check if (_options.Weigher == null) { EnsureCapacity(); } int newIndex = AllocateEntry(); if (newIndex == InvalidIndex) { return; } _entries[newIndex] = new CacheEntry { Key = key, Value = value, WriteTime = currentTime, AccessTime = currentTime, ExpirationTime = ComputeExpirationTime(key, value, currentTime, ttlSeconds), Frequency = 1, PrevIndex = InvalidIndex, NextIndex = InvalidIndex, SegmentIndex = ProbationSegment, IsAlive = true, Weight = weight, }; #if SINGLE_THREADED _keyToIndex[key] = newIndex; #else _keyToIndex.TryAdd(key, newIndex); #endif _count++; _currentWeight += weight; if (_count > _peakSize) { _peakSize = _count; } AddToEvictionList(newIndex); InvokeOnSet(key, value); } /// /// Attempts to remove an entry from the cache. /// /// The key to remove. /// True if the entry was removed, false if not found. public bool TryRemove(TKey key) { return TryRemove(key, out _); } /// /// Attempts to remove an entry from the cache, returning the removed value. /// /// The key to remove. /// The removed value if found. /// True if the entry was removed, false if not found. public bool TryRemove(TKey key, out TValue value) { if (key == null || _disposed) { value = default; return false; } _lock.EnterWriteLock(); try { if (!_keyToIndex.TryGetValue(key, out int index)) { value = default; return false; } value = _entries[index].Value; EvictEntry(index, EvictionReason.Explicit); return true; } finally { _lock.ExitWriteLock(); } } /// /// Removes all entries from the cache. /// public void Clear() { if (_disposed) { return; } _lock.EnterWriteLock(); try { for (int i = 0; i < _entries.Length; i++) { if (_entries[i].IsAlive) { InvokeOnEviction( _entries[i].Key, _entries[i].Value, EvictionReason.Explicit ); _entries[i] = default; } } _keyToIndex.Clear(); _count = 0; _currentWeight = 0; _probationCount = 0; _protectedCount = 0; InitializeFreeList(); InitializeLinkedLists(); } finally { _lock.ExitWriteLock(); } } /// /// Checks if the cache contains the specified key. /// /// The key to check. /// True if the key exists and is not expired. public bool ContainsKey(TKey key) { if (key == null || _disposed) { return false; } _lock.EnterReadLock(); try { if (!_keyToIndex.TryGetValue(key, out int index)) { return false; } if (!_entries[index].IsAlive || IsExpired(index, _timeProvider())) { return false; } return true; } finally { _lock.ExitReadLock(); } } /// /// Gets multiple values from the cache. /// Note: This overload allocates an enumerator. For zero-allocation enumeration, /// use instead. /// /// The keys to look up. /// Dictionary to receive the found key-value pairs. public void GetAll(IEnumerable keys, IDictionary results) { if (keys == null || results == null || _disposed) { return; } foreach (TKey key in keys) { if (TryGet(key, out TValue value)) { results[key] = value; } } } /// /// Gets multiple values from the cache using indexed access. /// This overload is zero-allocation. /// /// The keys to look up. /// Dictionary to receive the found key-value pairs. public void GetAll(IList keys, IDictionary results) { if (keys == null || results == null || _disposed) { return; } for (int i = 0; i < keys.Count; i++) { TKey key = keys[i]; if (TryGet(key, out TValue value)) { results[key] = value; } } } /// /// Adds or updates multiple entries in the cache. /// Note: This overload allocates an enumerator. For zero-allocation enumeration, /// use instead. /// /// The entries to add or update. public void SetAll(IEnumerable> entries) { if (entries == null || _disposed) { return; } foreach (KeyValuePair entry in entries) { Set(entry.Key, entry.Value); } } /// /// Adds or updates multiple entries in the cache using indexed access. /// This overload is zero-allocation. /// /// The entries to add or update. public void SetAll(IList> entries) { if (entries == null || _disposed) { return; } for (int i = 0; i < entries.Count; i++) { KeyValuePair entry = entries[i]; Set(entry.Key, entry.Value); } } /// /// Gets the current cache statistics. /// /// A snapshot of cache statistics. public CacheStatistics GetStatistics() { return new CacheStatistics( hitCount: Interlocked.Read(ref _hitCount), missCount: Interlocked.Read(ref _missCount), evictionCount: Interlocked.Read(ref _evictionCount), loadCount: Interlocked.Read(ref _loadCount), expiredCount: Interlocked.Read(ref _expiredCount), currentSize: _count, peakSize: _peakSize, growthEvents: _growthEvents ); } /// /// Forces an expiration scan to remove expired entries. /// public void CleanUp() { if (_disposed) { return; } _lock.EnterWriteLock(); try { float currentTime = _timeProvider(); for (int i = 0; i < _entries.Length; i++) { if (_entries[i].IsAlive && IsExpired(i, currentTime)) { EvictEntry(i, EvictionReason.Expired); RecordExpired(); } } } finally { _lock.ExitWriteLock(); } } /// /// Forces eviction of a percentage of entries. /// /// Percentage of entries to evict (0.0 to 1.0). public void Compact(float percentage) { if (_disposed || percentage <= 0f) { return; } if (percentage > 1f) { percentage = 1f; } _lock.EnterWriteLock(); try { int toEvict = (int)(_count * percentage); for (int i = 0; i < toEvict && _count > 0; i++) { EvictOne(EvictionReason.Capacity); } } finally { _lock.ExitWriteLock(); } } /// /// Resizes the cache to a new capacity. /// /// The new maximum capacity. public void Resize(int newCapacity) { if (_disposed || newCapacity <= 0) { return; } _lock.EnterWriteLock(); try { while (_count > newCapacity) { EvictOne(EvictionReason.Capacity); } _capacity = newCapacity; _protectedCapacity = (int)(_capacity * _options.ProtectedRatio); } finally { _lock.ExitWriteLock(); } } /// public void Dispose() { if (_disposed) { return; } Clear(); _disposed = true; _lock.Dispose(); _lock = null; } private float ComputeExpirationTime( TKey key, TValue value, float currentTime, float? explicitTtl ) { float ttl; if (explicitTtl.HasValue && explicitTtl.Value > 0f) { ttl = explicitTtl.Value; } else if (_options.ExpireAfter != null) { ttl = _options.ExpireAfter(key, value); } else if (_options.ExpireAfterWriteSeconds > 0f) { ttl = _options.ExpireAfterWriteSeconds; } else { return float.MaxValue; } if (_options.UseJitter && ttl > 0f) { float maxJitter = _options.JitterMaxSeconds > 0f ? _options.JitterMaxSeconds : ttl * 0.1f; ttl += Random.NextFloat(0f, maxJitter); } return currentTime + ttl; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool IsExpired(int index, float currentTime) { if (_entries[index].ExpirationTime <= currentTime) { return true; } if (_options.ExpireAfterAccessSeconds > 0f) { float slidingExpiration = _entries[index].AccessTime + _options.ExpireAfterAccessSeconds; if (slidingExpiration <= currentTime) { return true; } } return false; } private void OnAccess(int index, float currentTime) { _entries[index].AccessTime = currentTime; if (_options.ExpireAfterAccessSeconds > 0f) { _entries[index].ExpirationTime = currentTime + _options.ExpireAfterAccessSeconds; } switch (_options.Policy) { case EvictionPolicy.Lru: MoveToLruHead(index); break; case EvictionPolicy.Slru: PromoteInSlru(index); break; case EvictionPolicy.Lfu: _entries[index].Frequency++; break; case EvictionPolicy.Fifo: case EvictionPolicy.Random: break; } } private void EnsureCapacity() { EnsureCapacityForWeight(0); } private void EnsureCapacityForWeight(long incomingWeight) { // First, grow internal array if needed and possible (before eviction check) // We need to grow if count has reached capacity AND we haven't reached MaximumSize yet if (_options.Weigher == null && _count >= _capacity && _capacity < _options.MaximumSize) { GrowTowardsMaximumSize(); } // For weighted caches, evict based on weight if (_options.Weigher != null) { if (_currentWeight + incomingWeight > _options.MaximumWeight) { if (_options.AllowGrowth && ShouldGrow()) { Grow(); } else { EvictOne(EvictionReason.Capacity); } } return; } // For non-weighted caches, evict based on count vs MaximumSize // Only evict if we've reached the maximum allowed size (not just internal capacity) if (_count >= _options.MaximumSize) { if (_options.AllowGrowth && ShouldGrow()) { Grow(); } else { EvictOne(EvictionReason.Capacity); } } } private void GrowTowardsMaximumSize() { int newCapacity = Math.Min( (int)(_capacity * _options.GrowthFactor), _options.MaximumSize ); if (newCapacity <= _capacity) { newCapacity = Math.Min(_capacity + 1, _options.MaximumSize); } if (newCapacity <= _capacity) { return; } CacheEntry[] newEntries = new CacheEntry[newCapacity]; Array.Copy(_entries, newEntries, _entries.Length); int oldLength = _entries.Length; _entries = newEntries; for (int i = oldLength; i < newCapacity - 1; i++) { _entries[i].NextIndex = i + 1; } _entries[newCapacity - 1].NextIndex = _freeListHead; _freeListHead = oldLength; _capacity = newCapacity; _protectedCapacity = (int)(_capacity * _options.ProtectedRatio); } private bool ShouldGrow() { if (_options.MaxGrowthSize > 0 && _capacity >= _options.MaxGrowthSize) { return false; } float currentTime = _timeProvider(); if (currentTime - _lastEvictionTime > ThrashWindowSeconds) { _recentEvictionCount = 0; _lastEvictionTime = currentTime; } float evictionsPerSecond = _recentEvictionCount / Math.Max(0.001f, currentTime - _lastEvictionTime); return evictionsPerSecond >= _options.ThrashThresholdEvictionsPerSecond; } private void Grow() { int newCapacity = (int)(_capacity * _options.GrowthFactor); if (_options.MaxGrowthSize > 0) { newCapacity = Math.Min(newCapacity, _options.MaxGrowthSize); } if (newCapacity <= _capacity) { return; } CacheEntry[] newEntries = new CacheEntry[newCapacity]; Array.Copy(_entries, newEntries, _entries.Length); int oldLength = _entries.Length; _entries = newEntries; for (int i = oldLength; i < newCapacity - 1; i++) { _entries[i].NextIndex = i + 1; } _entries[newCapacity - 1].NextIndex = _freeListHead; _freeListHead = oldLength; _capacity = newCapacity; _protectedCapacity = (int)(_capacity * _options.ProtectedRatio); _growthEvents++; _recentEvictionCount = 0; } private int AllocateEntry() { if (_freeListHead == InvalidIndex) { return InvalidIndex; } int index = _freeListHead; _freeListHead = _entries[index].NextIndex; return index; } private void FreeEntry(int index) { _entries[index] = default; _entries[index].NextIndex = _freeListHead; _freeListHead = index; } private void AddToEvictionList(int index) { switch (_options.Policy) { case EvictionPolicy.Lru: AddToLruHead(index); break; case EvictionPolicy.Slru: AddToProbation(index); break; case EvictionPolicy.Lfu: AddToLruHead(index); break; case EvictionPolicy.Fifo: AddToFifoTail(index); break; case EvictionPolicy.Random: break; } } private void RemoveFromEvictionList(int index) { switch (_options.Policy) { case EvictionPolicy.Lru: case EvictionPolicy.Lfu: RemoveFromLru(index); break; case EvictionPolicy.Slru: RemoveFromSlru(index); break; case EvictionPolicy.Fifo: RemoveFromFifo(index); break; case EvictionPolicy.Random: break; } } private void EvictOne(EvictionReason reason) { int victim = SelectVictim(); if (victim != InvalidIndex) { EvictEntry(victim, reason); _recentEvictionCount++; } } private int SelectVictim() { float currentTime = _timeProvider(); for (int i = 0; i < _entries.Length; i++) { if (_entries[i].IsAlive && IsExpired(i, currentTime)) { return i; } } switch (_options.Policy) { case EvictionPolicy.Lru: return _lruTail; case EvictionPolicy.Slru: return _probationTail != InvalidIndex ? _probationTail : _protectedTail; case EvictionPolicy.Lfu: return SelectLfuVictim(); case EvictionPolicy.Fifo: return _fifoHead; case EvictionPolicy.Random: return SelectRandomVictim(); default: return InvalidIndex; } } private int SelectLfuVictim() { int victim = InvalidIndex; int minFrequency = int.MaxValue; float oldestAccess = float.MaxValue; for (int i = 0; i < _entries.Length; i++) { if (!_entries[i].IsAlive) { continue; } if ( _entries[i].Frequency < minFrequency || ( _entries[i].Frequency == minFrequency && _entries[i].AccessTime < oldestAccess ) ) { minFrequency = _entries[i].Frequency; oldestAccess = _entries[i].AccessTime; victim = i; } } return victim; } private int SelectRandomVictim() { if (_count == 0) { return InvalidIndex; } int targetCount = Random.Next(0, _count); int current = 0; for (int i = 0; i < _entries.Length; i++) { if (_entries[i].IsAlive) { if (current == targetCount) { return i; } current++; } } return InvalidIndex; } private void EvictEntry(int index, EvictionReason reason) { if (!_entries[index].IsAlive) { return; } TKey key = _entries[index].Key; TValue value = _entries[index].Value; long weight = _entries[index].Weight; byte segmentIndex = _entries[index].SegmentIndex; #if SINGLE_THREADED _keyToIndex.Remove(key); #else _keyToIndex.TryRemove(key, out _); #endif RemoveFromEvictionList(index); FreeEntry(index); _count--; _currentWeight -= weight; if (_options.Policy == EvictionPolicy.Slru) { if (segmentIndex == ProbationSegment) { _probationCount--; } else { _protectedCount--; } } RecordEviction(); InvokeOnEviction(key, value, reason); } private void AddToLruHead(int index) { _entries[index].PrevIndex = InvalidIndex; _entries[index].NextIndex = _lruHead; if (_lruHead != InvalidIndex) { _entries[_lruHead].PrevIndex = index; } _lruHead = index; if (_lruTail == InvalidIndex) { _lruTail = index; } } private void MoveToLruHead(int index) { if (index == _lruHead) { return; } RemoveFromLru(index); AddToLruHead(index); } private void RemoveFromLru(int index) { int prev = _entries[index].PrevIndex; int next = _entries[index].NextIndex; if (prev != InvalidIndex) { _entries[prev].NextIndex = next; } else { _lruHead = next; } if (next != InvalidIndex) { _entries[next].PrevIndex = prev; } else { _lruTail = prev; } _entries[index].PrevIndex = InvalidIndex; _entries[index].NextIndex = InvalidIndex; } private void AddToFifoTail(int index) { _entries[index].PrevIndex = _fifoTail; _entries[index].NextIndex = InvalidIndex; if (_fifoTail != InvalidIndex) { _entries[_fifoTail].NextIndex = index; } _fifoTail = index; if (_fifoHead == InvalidIndex) { _fifoHead = index; } } private void RemoveFromFifo(int index) { int prev = _entries[index].PrevIndex; int next = _entries[index].NextIndex; if (prev != InvalidIndex) { _entries[prev].NextIndex = next; } else { _fifoHead = next; } if (next != InvalidIndex) { _entries[next].PrevIndex = prev; } else { _fifoTail = prev; } _entries[index].PrevIndex = InvalidIndex; _entries[index].NextIndex = InvalidIndex; } private void AddToProbation(int index) { _entries[index].SegmentIndex = ProbationSegment; _entries[index].PrevIndex = InvalidIndex; _entries[index].NextIndex = _probationHead; if (_probationHead != InvalidIndex) { _entries[_probationHead].PrevIndex = index; } _probationHead = index; if (_probationTail == InvalidIndex) { _probationTail = index; } _probationCount++; } private void PromoteInSlru(int index) { if (_entries[index].SegmentIndex == ProtectedSegment) { MoveToProtectedHead(index); return; } RemoveFromProbation(index); AddToProtected(index); if (_protectedCount > _protectedCapacity) { DemoteFromProtected(); } } private void AddToProtected(int index) { _entries[index].SegmentIndex = ProtectedSegment; _entries[index].PrevIndex = InvalidIndex; _entries[index].NextIndex = _protectedHead; if (_protectedHead != InvalidIndex) { _entries[_protectedHead].PrevIndex = index; } _protectedHead = index; if (_protectedTail == InvalidIndex) { _protectedTail = index; } _protectedCount++; } private void MoveToProtectedHead(int index) { if (index == _protectedHead) { return; } int prev = _entries[index].PrevIndex; int next = _entries[index].NextIndex; if (prev != InvalidIndex) { _entries[prev].NextIndex = next; } if (next != InvalidIndex) { _entries[next].PrevIndex = prev; } else { _protectedTail = prev; } _entries[index].PrevIndex = InvalidIndex; _entries[index].NextIndex = _protectedHead; if (_protectedHead != InvalidIndex) { _entries[_protectedHead].PrevIndex = index; } _protectedHead = index; } private void DemoteFromProtected() { if (_protectedTail == InvalidIndex) { return; } int demoted = _protectedTail; RemoveFromProtected(demoted); AddToProbation(demoted); } private void RemoveFromProbation(int index) { int prev = _entries[index].PrevIndex; int next = _entries[index].NextIndex; if (prev != InvalidIndex) { _entries[prev].NextIndex = next; } else { _probationHead = next; } if (next != InvalidIndex) { _entries[next].PrevIndex = prev; } else { _probationTail = prev; } _entries[index].PrevIndex = InvalidIndex; _entries[index].NextIndex = InvalidIndex; _probationCount--; } private void RemoveFromProtected(int index) { int prev = _entries[index].PrevIndex; int next = _entries[index].NextIndex; if (prev != InvalidIndex) { _entries[prev].NextIndex = next; } else { _protectedHead = next; } if (next != InvalidIndex) { _entries[next].PrevIndex = prev; } else { _protectedTail = prev; } _entries[index].PrevIndex = InvalidIndex; _entries[index].NextIndex = InvalidIndex; _protectedCount--; } private void RemoveFromSlru(int index) { if (_entries[index].SegmentIndex == ProbationSegment) { RemoveFromProbation(index); } else { RemoveFromProtected(index); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void RecordHit() { if (_options.RecordStatistics) { Interlocked.Increment(ref _hitCount); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void RecordMiss() { if (_options.RecordStatistics) { Interlocked.Increment(ref _missCount); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void RecordEviction() { if (_options.RecordStatistics) { Interlocked.Increment(ref _evictionCount); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void RecordLoad() { if (_options.RecordStatistics) { Interlocked.Increment(ref _loadCount); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void RecordExpired() { if (_options.RecordStatistics) { Interlocked.Increment(ref _expiredCount); } } private void InvokeOnEviction(TKey key, TValue value, EvictionReason reason) { if (_options.OnEviction == null) { return; } try { _options.OnEviction(key, value, reason); } catch { // Swallow exceptions from callbacks } } private void InvokeOnGet(TKey key, TValue value) { if (_options.OnGet == null) { return; } try { _options.OnGet(key, value); } catch { // Swallow exceptions from callbacks } } private void InvokeOnSet(TKey key, TValue value) { if (_options.OnSet == null) { return; } try { _options.OnSet(key, value); } catch { // Swallow exceptions from callbacks } } } }