// Copyright (c) Microsoft Corporation // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Microsoft.CmdPal.Core.Common.Helpers; namespace Microsoft.CmdPal.UI.Helpers; /// /// A high-performance, near-lock-free adaptive cache optimized for UI Icons. /// Eviction merely drops references to allow the GC to manage UI-bound lifetimes. /// internal sealed class AdaptiveCache where TKey : IEquatable { private readonly int _capacity; private readonly double _decayFactor; private readonly TimeSpan _decayInterval; private readonly ConcurrentDictionary _map; private readonly ConcurrentStack _pool = []; private readonly WaitCallback _maintenanceCallback; private long _currentTick; private long _lastDecayTicks = DateTime.UtcNow.Ticks; private InterlockedBoolean _maintenanceSwitch = new(false); public AdaptiveCache(int capacity = 384, TimeSpan? decayInterval = null, double decayFactor = 0.5) { _capacity = capacity; _decayInterval = decayInterval ?? TimeSpan.FromMinutes(5); _decayFactor = decayFactor; _map = new ConcurrentDictionary(Environment.ProcessorCount, capacity); _maintenanceCallback = static state => { var cache = (AdaptiveCache)state!; try { cache.PerformCleanup(); } finally { cache._maintenanceSwitch.Clear(); } }; } public TValue GetOrAdd(TKey key, Func factory, TArg arg) { if (_map.TryGetValue(key, out var entry)) { entry.Update(Interlocked.Increment(ref _currentTick)); return entry.Value!; } if (!_pool.TryPop(out var newEntry)) { newEntry = new CacheEntry(); } var value = factory(key, arg); var tick = Interlocked.Increment(ref _currentTick); newEntry.Initialize(key, value, 1.0, tick); if (!_map.TryAdd(key, newEntry)) { newEntry.Clear(); _pool.Push(newEntry); if (_map.TryGetValue(key, out var existing)) { existing.Update(tick); return existing.Value!; } } if (ShouldMaintenanceRun()) { TryRunMaintenance(); } return value; } public bool TryGet(TKey key, [MaybeNullWhen(false)] out TValue value) { if (_map.TryGetValue(key, out var entry)) { entry.Update(Interlocked.Increment(ref _currentTick)); value = entry.Value; return true; } value = default; return false; } public void Add(TKey key, TValue value) { var tick = Interlocked.Increment(ref _currentTick); if (_map.TryGetValue(key, out var existing)) { existing.Update(tick); existing.SetValue(value); return; } if (!_pool.TryPop(out var newEntry)) { newEntry = new CacheEntry(); } newEntry.Initialize(key, value, 1.0, tick); if (!_map.TryAdd(key, newEntry)) { newEntry.Clear(); _pool.Push(newEntry); } if (ShouldMaintenanceRun()) { TryRunMaintenance(); } } public bool TryRemove(TKey key) { if (_map.TryRemove(key, out var evicted)) { evicted.Clear(); _pool.Push(evicted); return true; } return false; } public void Clear() { foreach (var key in _map.Keys) { TryRemove(key); } Interlocked.Exchange(ref _currentTick, 0); } private bool ShouldMaintenanceRun() { return _map.Count > _capacity || (DateTime.UtcNow.Ticks - Interlocked.Read(ref _lastDecayTicks)) > _decayInterval.Ticks; } private void TryRunMaintenance() { if (_maintenanceSwitch.Set()) { ThreadPool.UnsafeQueueUserWorkItem(_maintenanceCallback, this); } } private void PerformCleanup() { var nowTicks = DateTime.UtcNow.Ticks; var isDecay = (nowTicks - Interlocked.Read(ref _lastDecayTicks)) > _decayInterval.Ticks; if (isDecay) { Interlocked.Exchange(ref _lastDecayTicks, nowTicks); } var currentTick = Interlocked.Read(ref _currentTick); foreach (var (key, entry) in _map) { if (isDecay) { entry.Decay(_decayFactor); } var score = CalculateScore(entry, currentTick); if (score < 0.1 || _map.Count > _capacity) { if (_map.TryRemove(key, out var evicted)) { evicted.Clear(); _pool.Push(evicted); } } } } /// /// Calculates the survival score of an entry. /// Higher score = stay in cache; Lower score = priority for eviction. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double CalculateScore(CacheEntry entry, long currentTick) { // Tuning parameter: How much weight to give recency vs frequency. // - a larger ageWeight makes the cache behave more like LRU (Least Recently Used). // - a smaller ageWeight makes it behave more like LFU (Least Frequently Used). const double ageWeight = 0.001; var frequency = entry.GetFrequency(); var age = currentTick - entry.GetLastAccess(); return frequency - (age * ageWeight); } /// /// Represents a single pooled entry in the cache, containing the value and /// atomic metadata for adaptive eviction logic. /// private sealed class CacheEntry { /// /// Gets the key associated with this entry. Used primarily for identification during cleanup. /// public TKey Key { get; private set; } = default!; /// /// Gets the cached value. This reference is cleared on eviction to allow GC collection. /// public TValue Value { get; private set; } = default!; /// /// Stores the frequency count as double bits to allow for Interlocked atomic math. /// Frequencies are decayed over time to ensure the cache adapts to new usage patterns. /// /// /// This allows the use of Interlocked.CompareExchange to perform thread-safe floating point /// arithmetic without a global lock. /// private long _frequencyBits; /// /// The tick (monotonically increasing counter) of the last time this entry was accessed. /// private long _lastAccessTick; public void Initialize(TKey key, TValue value, double frequency, long lastAccessTick) { Key = key; Value = value; _frequencyBits = BitConverter.DoubleToInt64Bits(frequency); _lastAccessTick = lastAccessTick; } public void SetValue(TValue value) { Value = value; } public void Clear() { Key = default!; Value = default!; } public void Update(long tick) { Interlocked.Exchange(ref _lastAccessTick, tick); long initial, updated; do { initial = Interlocked.Read(ref _frequencyBits); updated = BitConverter.DoubleToInt64Bits(BitConverter.Int64BitsToDouble(initial) + 1.0); } while (Interlocked.CompareExchange(ref _frequencyBits, updated, initial) != initial); } public void Decay(double factor) { long initial, updated; do { initial = Interlocked.Read(ref _frequencyBits); updated = BitConverter.DoubleToInt64Bits(BitConverter.Int64BitsToDouble(initial) * factor); } while (Interlocked.CompareExchange(ref _frequencyBits, updated, initial) != initial); } public double GetFrequency() { return BitConverter.Int64BitsToDouble(Interlocked.Read(ref _frequencyBits)); } public long GetLastAccess() { return Interlocked.Read(ref _lastAccessTick); } } }