File: ConcurrentLruCache.cs
Web Access
Project: ..\..\..\src\RazorSdk\Tool\Microsoft.NET.Sdk.Razor.Tool.csproj (rzc)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable disable
 
using System.Diagnostics;
 
namespace Microsoft.NET.Sdk.Razor.Tool
{
    /// <summary>
    /// Cache with a fixed size that evicts the least recently used members.
    /// Thread-safe.
    /// This was taken from https://github.com/dotnet/roslyn/blob/749c0ec135d7d080658dc1aa794d15229c3d10d2/src/Compilers/Core/Portable/InternalUtilities/ConcurrentLruCache.cs.
    /// </summary>
    internal class ConcurrentLruCache<TKey, TValue>
    {
        private readonly int _capacity;
 
        private readonly Dictionary<TKey, CacheValue> _cache;
        private readonly LinkedList<TKey> _nodeList;
        // This is a naive course-grained lock, it can probably be optimized
        private readonly object _lockObject = new();
 
        public ConcurrentLruCache(int capacity)
            : this(capacity, EqualityComparer<TKey>.Default)
        {
        }
 
        public ConcurrentLruCache(int capacity, IEqualityComparer<TKey> comparer)
        {
            if (capacity <= 0)
            {
                throw new ArgumentOutOfRangeException(nameof(capacity));
            }
            _capacity = capacity;
            _cache = new Dictionary<TKey, CacheValue>(capacity, comparer);
            _nodeList = new LinkedList<TKey>();
        }
 
        /// <summary>
        /// Create cache from an array. The cache capacity will be the size
        /// of the array. All elements of the array will be added to the 
        /// cache. If any duplicate keys are found in the array a
        /// <see cref="ArgumentException"/> will be thrown.
        /// </summary>
        public ConcurrentLruCache(KeyValuePair<TKey, TValue>[] array)
            : this(array.Length)
        {
            foreach (var kvp in array)
            {
                UnsafeAdd(kvp.Key, kvp.Value);
            }
        }
 
        public int Count
        {
            get
            {
                lock (_lockObject)
                {
                    return _cache.Count;
                }
            }
        }
 
        public void Add(TKey key, TValue value)
        {
            lock (_lockObject)
            {
                UnsafeAdd(key, value);
            }
        }
 
        public TValue GetOrAdd(TKey key, TValue value)
        {
            lock (_lockObject)
            {
                if (UnsafeTryGetValue(key, out var result))
                {
                    return result;
                }
                else
                {
                    UnsafeAdd(key, value);
                    return value;
                }
            }
        }
 
        public bool TryGetValue(TKey key, out TValue value)
        {
            lock (_lockObject)
            {
                return UnsafeTryGetValue(key, out value);
            }
        }
 
        public bool Remove(TKey key)
        {
            lock (_lockObject)
            {
                return UnsafeRemove(key);
            }
        }
 
        /// <summary>
        /// For testing. Very expensive.
        /// </summary>
        internal IEnumerable<KeyValuePair<TKey, TValue>> TestingEnumerable
        {
            get
            {
                lock (_lockObject)
                {
                    foreach (var key in _nodeList)
                    {
                        var kvp = new KeyValuePair<TKey, TValue>(key, _cache[key].Value);
#pragma warning disable CS9237 // 'yield return' should not be used in the body of a lock statement
                        yield return kvp;
#pragma warning restore CS9237
                    }
                }
            }
        }
 
        /// <summary>
        /// Doesn't lock.
        /// </summary>
        private bool UnsafeTryGetValue(TKey key, out TValue value)
        {
            if (_cache.TryGetValue(key, out var result))
            {
                MoveNodeToTop(result.Node);
                value = result.Value;
                return true;
            }
            else
            {
                value = default(TValue);
                return false;
            }
        }
 
        private void MoveNodeToTop(LinkedListNode<TKey> node)
        {
            if (!ReferenceEquals(_nodeList.First, node))
            {
                _nodeList.Remove(node);
                _nodeList.AddFirst(node);
            }
        }
 
        /// <summary>
        /// Expects non-empty cache. Does not lock.
        /// </summary>
        private void UnsafeEvictLastNode()
        {
            Debug.Assert(_capacity > 0);
            var lastNode = _nodeList.Last;
            _nodeList.Remove(lastNode);
            _cache.Remove(lastNode.Value);
        }
 
        private void UnsafeAddNodeToTop(TKey key, TValue value)
        {
            var node = new LinkedListNode<TKey>(key);
            _cache.Add(key, new CacheValue(value, node));
            _nodeList.AddFirst(node);
        }
 
        /// <summary>
        /// Doesn't lock.
        /// </summary>
        private void UnsafeAdd(TKey key, TValue value)
        {
            if (_cache.TryGetValue(key, out var result))
            {
                throw new ArgumentException("Key already exists", nameof(key));
            }
            else
            {
                if (_cache.Count == _capacity)
                {
                    UnsafeEvictLastNode();
                }
                UnsafeAddNodeToTop(key, value);
            }
        }
 
        private bool UnsafeRemove(TKey key)
        {
            _nodeList.Remove(key);
            return _cache.Remove(key);
        }
 
        private struct CacheValue
        {
            public CacheValue(TValue value, LinkedListNode<TKey> node)
            {
                Value = value;
                Node = node;
            }
 
            public TValue Value { get; }
 
            public LinkedListNode<TKey> Node { get; }
        }
    }
}