// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.MixedReality.Toolkit.Utilities
{
///
/// An object cache where items can be accessed by key and the least recently used item is evicted when the cache exceeds
/// its capacity.
///
///
/// Implementation involves a dictionary for fast entry lookup, and a doubly linked list to track recent access.
///
/// The type that is used as key to look up values in the cache.
/// The type of values stored in the cache.
internal class LRUCache : IDictionary
{
///
/// Data structure used to construct a linked list of cached items in temporal order.
///
private class CacheEntry
{
///
/// Cached item key.
///
public TKey Key { get; set; }
/// .
/// Cached item value
///
public TValue Value { get; set; }
///
/// Reference to previous cache entry.
///
public CacheEntry Previous { get; set; }
///
/// Reference to next cache entry.
///
public CacheEntry Next { get; set; }
public CacheEntry(TKey key, TValue item)
{
Key = key;
Value = item;
}
}
// Dictionary for key value entry lookup
private readonly Dictionary keyToEntryTable = new Dictionary();
// The least recent entry, stored as the tail of the doubly linked list
private CacheEntry leastRecentEntry = null;
// The most recent entry, stored as the head of the doubly linked list
private CacheEntry mostRecentEntry = null;
///
/// Constructs a new with a given capacity.
///
/// The maximum number items that may be cached.
public LRUCache(int capacity)
{
if (capacity <= 0)
{
throw new ArgumentOutOfRangeException("Cache capacity must be greater than 0");
}
Capacity = capacity;
}
///
/// Gets the maximum number items that may be cached. If items are added beyond this capacity, then the least
/// recently used entry is evicted.
///
public int Capacity { get; }
///
/// Gets the number of items currently cached. This value will never exceed
///
public int Count => keyToEntryTable.Count;
///
/// Gets or sets the item with the associated key.
///
///
/// The property is retrieved and is not present in the cache.
///
///
/// is null.
///
public TValue this[TKey key]
{
get
{
if (TryGetValue(key, out var value))
{
return value;
}
throw new KeyNotFoundException($"Item with key '{key}' is not cached");
}
set => Add(key, value);
}
///
/// Gets a collection containing keys to cached items in order of most to least recently used.
///
public ICollection Keys => this.Select(kv => kv.Key).ToList();
///
/// Gets a collection containing the cached items in order of most to least recently used.
///
public ICollection Values => this.Select(kv => kv.Value).ToList();
///
/// Try to retrieve a cached item by key. Getting a cached item by key will update the most recent access status of
/// the item, bringing it to the front of the recent access list.
///
/// The key for the cached item.
/// The value for the cached item.
/// True if the cached item exists in the cache, false if the item does not exist.
///
/// if is null.
///
public bool TryGetValue(TKey key, out TValue value)
{
if (keyToEntryTable.TryGetValue(key, out CacheEntry cacheEntry))
{
MakeCacheEntryRecent(cacheEntry);
value = cacheEntry.Value;
return true;
}
value = default(TValue);
return false;
}
public bool ContainsKey(TKey key) => keyToEntryTable.ContainsKey(key);
///
/// Adds an item to the cache. If the item key doesn't exist, the cache generates a new entry to store the item.
/// If the key already exists, then the cached entry is updated with the new item value. In either case, the entry
/// is brought to the front of the recent access list and if the new exceeds
/// then the item lest recently accessed is evicted.
///
/// The key for the item to cache
/// The value for the item to cache
///
/// if is null.
///
public void Add(TKey key, TValue value)
{
// Retrieve of create cached entry
if (keyToEntryTable.TryGetValue(key, out CacheEntry cacheEntry))
{
cacheEntry.Value = value;
}
else
{
if (keyToEntryTable.Count >= Capacity && leastRecentEntry != null)
{
// Handle overcapacity by removing least recent entry
keyToEntryTable.Remove(leastRecentEntry.Key);
// Cache the new leastRecentEntry before reuse
CacheEntry newLeastRecentEntry = leastRecentEntry.Previous;
// Reuse the leastRecentEntry CacheEntry to avoid new allocations
cacheEntry = leastRecentEntry;
cacheEntry.Key = key;
cacheEntry.Value = value;
cacheEntry.Next = null;
cacheEntry.Previous = null;
// Rotate leastRecentEntry
leastRecentEntry = newLeastRecentEntry;
leastRecentEntry.Next = null;
}
else
{
cacheEntry = new CacheEntry(key, value);
// Assign least recent entry if this is the first entry in the cache
if (leastRecentEntry == null)
{
leastRecentEntry = cacheEntry;
}
}
keyToEntryTable.Add(key, cacheEntry);
}
// Make the entry the most recently accessed entry in the list
MakeCacheEntryRecent(cacheEntry);
}
///
/// Removes an entry from the cache by its key.
///
/// The key for the cached item.
/// True if the item exists, false if item does not exists in the cache.
///
/// if is null.
///
public bool Remove(TKey key)
{
if (keyToEntryTable.TryGetValue(key, out var cacheEntry))
{
keyToEntryTable.Remove(cacheEntry.Key);
RemoveEntryFromList(cacheEntry);
// update most recent entry
if (cacheEntry == mostRecentEntry)
{
mostRecentEntry = cacheEntry.Next;
}
// update least recent entry
if (cacheEntry == leastRecentEntry)
{
leastRecentEntry = cacheEntry.Previous;
}
return true;
}
return false;
}
///
/// Clear all entry out of the cache.
///
public void Clear()
{
// Clear all entry stored in the dictionary.
keyToEntryTable.Clear();
// Iterate through the list and unhook all the pointers in the list.
while (mostRecentEntry != null)
{
var nextEntry = mostRecentEntry.Next;
mostRecentEntry.Next = null;
if (nextEntry != null)
{
nextEntry.Previous = null;
}
mostRecentEntry = nextEntry;
}
leastRecentEntry = null;
}
///
/// Returns an enumerator that iterates through cached items in order of most to least recently used.
///
public IEnumerator> GetEnumerator()
{
if (keyToEntryTable.Count == 0)
{
yield break;
}
for (var entry = mostRecentEntry; entry != null; entry = entry.Next)
{
yield return new KeyValuePair(entry.Key, entry.Value);
}
}
///
/// Update the doubly linked list to bring the given entry to the front of the list, with the front being the
/// most recently access entry.
///
/// The entry to bring to the front of the list
private void MakeCacheEntryRecent(CacheEntry entry)
{
// Only perform operation if the entry is not already the most recently accessed item
if (entry != mostRecentEntry)
{
RemoveEntryFromList(entry);
// Update leastRecentyEntry value if necessary
if (entry == leastRecentEntry && entry.Previous != null)
{
leastRecentEntry = entry.Previous;
}
// Move entry to the front of the list
entry.Previous = null;
entry.Next = mostRecentEntry;
if (mostRecentEntry != null)
{
mostRecentEntry.Previous = entry;
}
// Mark entry as the most recent entry
mostRecentEntry = entry;
}
}
///
/// Remove an entry from the doubly linked list.
///
/// The entry to remove from the list.
private void RemoveEntryFromList(CacheEntry entry)
{
// Point the next entry pointer in the previous entry to point to the next entry in the list
if (entry.Previous != null)
{
entry.Previous.Next = entry.Next;
}
// Point the prev entry pointer in the next entry to the prev entry in the list
if (entry.Next != null)
{
entry.Next.Previous = entry.Previous;
}
}
// ICollection implementation
bool ICollection>.IsReadOnly { get; } = false;
void ICollection>.Add(KeyValuePair item)
=> Add(item.Key, item.Value);
bool ICollection>.Contains(KeyValuePair item)
=> TryGetValue(item.Key, out var value) && Equals(value, item.Value);
bool ICollection>.Remove(KeyValuePair item)
=> Remove(item.Key);
void ICollection>.CopyTo(KeyValuePair[] array, int index)
{
foreach (var kv in this)
{
array[index++] = kv;
}
}
// IEnumerable implementation
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}