// 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; }
}
}
}