mixedreality/com.microsoft.mixedreality..../SDK/Features/Utilities/LRUCache.cs

351 lines
13 KiB
C#

// 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
{
/// <summary>
/// An object cache where items can be accessed by key and the least recently used item is evicted when the cache exceeds
/// its capacity.
/// </summary>
/// <remarks>
/// Implementation involves a dictionary for fast entry lookup, and a doubly linked list to track recent access.
/// </remarks>
/// <typeparam name="TKey">The type that is used as key to look up values in the cache.</typeparam>
/// <typeparam name="TValue">The type of values stored in the cache.</typeparam>
internal class LRUCache<TKey, TValue> : IDictionary<TKey, TValue>
{
/// <summary>
/// Data structure used to construct a linked list of cached items in temporal order.
/// </summary>
private class CacheEntry
{
/// <summary>
/// Cached item key.
/// </summary>
public TKey Key { get; set; }
/// <summary>.
/// Cached item value
/// </summary>
public TValue Value { get; set; }
/// <summary>
/// Reference to previous cache entry.
/// </summary>
public CacheEntry Previous { get; set; }
/// <summary>
/// Reference to next cache entry.
/// </summary>
public CacheEntry Next { get; set; }
public CacheEntry(TKey key, TValue item)
{
Key = key;
Value = item;
}
}
// Dictionary for key value entry lookup
private readonly Dictionary<TKey, CacheEntry> keyToEntryTable = new Dictionary<TKey, CacheEntry>();
// 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;
/// <summary>
/// Constructs a new <see cref="LRUCache"/> with a given capacity.
/// </summary>
/// <param name="capacity">The maximum number items that may be cached.</param>
public LRUCache(int capacity)
{
if (capacity <= 0)
{
throw new ArgumentOutOfRangeException("Cache capacity must be greater than 0");
}
Capacity = capacity;
}
/// <summary>
/// Gets the maximum number items that may be cached. If items are added beyond this capacity, then the least
/// recently used entry is evicted.
/// </summary>
public int Capacity { get; }
/// <summary>
/// Gets the number of items currently cached. This value will never exceed <see cref="Capacity"/>
/// </summary>
public int Count => keyToEntryTable.Count;
/// <summary>
/// Gets or sets the item with the associated key.
/// </summary>
/// <exception cref="KeyNotFoundException"/>
/// The property is retrieved and <paramref name="key"/> is not present in the cache.
/// </exception>
/// <exception cref="ArgumentNullException"/>
/// <paramref name="key"/> is null.
/// </exception>
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);
}
/// <summary>
/// Gets a collection containing keys to cached items in order of most to least recently used.
/// </summary>
public ICollection<TKey> Keys => this.Select(kv => kv.Key).ToList();
/// <summary>
/// Gets a collection containing the cached items in order of most to least recently used.
/// </summary>
public ICollection<TValue> Values => this.Select(kv => kv.Value).ToList();
/// <summary>
/// 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.
/// </summary>
/// <param name="key">The key for the cached item.</param>
/// <param name="value">The value for the cached item.</param>
/// <returns>True if the cached item exists in the cache, false if the item does not exist.</returns>
/// <exception>
/// <see cref="ArgumentNullException"/> if <paramref name="key"/> is null.
/// </exception>
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);
/// <summary>
/// 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 <see cref="Count"/> exceeds
/// <see cref="Capacity"/> then the item lest recently accessed is evicted.
/// </summary>
/// <param name="key">The key for the item to cache</param>
/// <param name="value">The value for the item to cache</param>
/// <exception>
/// <see cref="ArgumentNullException"/> if <paramref name="key"/> is null.
/// </exception>
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);
}
/// <summary>
/// Removes an entry from the cache by its key.
/// </summary>
/// <param name="key">The key for the cached item.</param>
/// <returns>True if the item exists, false if item does not exists in the cache.</returns>
/// <exception>
/// <see cref="ArgumentNullException"/> if <paramref name="key"/> is null.
/// </exception>
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;
}
/// <summary>
/// Clear all entry out of the cache.
/// </summary>
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;
}
/// <summary>
/// Returns an enumerator that iterates through cached items in order of most to least recently used.
/// </summary>
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
{
if (keyToEntryTable.Count == 0)
{
yield break;
}
for (var entry = mostRecentEntry; entry != null; entry = entry.Next)
{
yield return new KeyValuePair<TKey, TValue>(entry.Key, entry.Value);
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="entry">The entry to bring to the front of the list</param>
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;
}
}
/// <summary>
/// Remove an entry from the doubly linked list.
/// </summary>
/// <param name="entry">The entry to remove from the list.</param>
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<KeyValuePair<TKey, TValue>>.IsReadOnly { get; } = false;
void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> item)
=> Add(item.Key, item.Value);
bool ICollection<KeyValuePair<TKey, TValue>>.Contains(KeyValuePair<TKey, TValue> item)
=> TryGetValue(item.Key, out var value) && Equals(value, item.Value);
bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> item)
=> Remove(item.Key);
void ICollection<KeyValuePair<TKey, TValue>>.CopyTo(KeyValuePair<TKey, TValue>[] array, int index)
{
foreach (var kv in this)
{
array[index++] = kv;
}
}
// IEnumerable implementation
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}