// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Diagnostics; namespace Microsoft.AspNetCore.Razor.Tools { /// /// 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. /// internal class ConcurrentLruCache { private readonly int _capacity; private readonly Dictionary _cache; private readonly LinkedList _nodeList; // This is a naive course-grained lock, it can probably be optimized private readonly object _lockObject = new object(); public ConcurrentLruCache(int capacity) : this (capacity, EqualityComparer.Default) { } public ConcurrentLruCache(int capacity, IEqualityComparer comparer) { if (capacity <= 0) { throw new ArgumentOutOfRangeException(nameof(capacity)); } _capacity = capacity; _cache = new Dictionary(capacity, comparer); _nodeList = new LinkedList(); } /// /// 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 /// will be thrown. /// public ConcurrentLruCache(KeyValuePair[] 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); } } /// /// For testing. Very expensive. /// internal IEnumerable> TestingEnumerable { get { lock (_lockObject) { foreach (var key in _nodeList) { var kvp = new KeyValuePair(key, _cache[key].Value); yield return kvp; } } } } /// /// Doesn't lock. /// 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 node) { if (!object.ReferenceEquals(_nodeList.First, node)) { _nodeList.Remove(node); _nodeList.AddFirst(node); } } /// /// Expects non-empty cache. Does not lock. /// 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(key); _cache.Add(key, new CacheValue(value, node)); _nodeList.AddFirst(node); } /// /// Doesn't lock. /// 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 node) { Value = value; Node = node; } public TValue Value { get; } public LinkedListNode Node { get; } } } }