using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading; namespace ChocolArm64.Translation { class TranslatorCache { // Maximum size of the cache, the unit used is completely arbitrary. private const int MaxTotalSize = 0x800000; // Minimum time required in milliseconds for a method to be eligible for deletion. private const int MinTimeDelta = 2 * 60000; // Minimum number of calls required to update the timestamp. private const int MinCallCountForUpdate = 250; private class CacheBucket { public TranslatedSub Subroutine { get; private set; } public LinkedListNode Node { get; private set; } public int CallCount { get; set; } public int Size { get; private set; } public long Timestamp { get; private set; } public CacheBucket(TranslatedSub subroutine, LinkedListNode node, int size) { Subroutine = subroutine; Size = size; UpdateNode(node); } public void UpdateNode(LinkedListNode node) { Node = node; Timestamp = GetTimestamp(); } } private ConcurrentDictionary _cache; private LinkedList _sortedCache; private int _totalSize; public TranslatorCache() { _cache = new ConcurrentDictionary(); _sortedCache = new LinkedList(); } public TranslatedSub GetOrAdd(long position, TranslatedSub subroutine, int size) { ClearCacheIfNeeded(); lock (_sortedCache) { LinkedListNode node = _sortedCache.AddLast(position); CacheBucket bucket = new CacheBucket(subroutine, node, size); bucket = _cache.GetOrAdd(position, bucket); if (bucket.Node == node) { _totalSize += size; } else { _sortedCache.Remove(node); } return bucket.Subroutine; } } public void AddOrUpdate(long position, TranslatedSub subroutine, int size) { ClearCacheIfNeeded(); lock (_sortedCache) { _totalSize += size; LinkedListNode node = _sortedCache.AddLast(position); CacheBucket newBucket = new CacheBucket(subroutine, node, size); _cache.AddOrUpdate(position, newBucket, (key, bucket) => { _totalSize -= bucket.Size; _sortedCache.Remove(bucket.Node); return newBucket; }); } } public bool HasSubroutine(long position) { return _cache.ContainsKey(position); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryGetSubroutine(long position, out TranslatedSub subroutine) { if (_cache.TryGetValue(position, out CacheBucket bucket)) { if (bucket.CallCount++ > MinCallCountForUpdate) { if (Monitor.TryEnter(_sortedCache)) { try { // The bucket value on the dictionary may have changed between the // time we get the value from the dictionary, and we acquire the // lock. So we need to ensure we are working with the latest value, // we can do that by getting the value again, inside the lock. if (_cache.TryGetValue(position, out CacheBucket latestBucket)) { latestBucket.CallCount = 0; _sortedCache.Remove(latestBucket.Node); latestBucket.UpdateNode(_sortedCache.AddLast(position)); } } finally { Monitor.Exit(_sortedCache); } } } subroutine = bucket.Subroutine; return true; } subroutine = default(TranslatedSub); return false; } private void ClearCacheIfNeeded() { long timestamp = GetTimestamp(); while (_totalSize > MaxTotalSize) { lock (_sortedCache) { LinkedListNode node = _sortedCache.First; if (node == null) { break; } CacheBucket bucket = _cache[node.Value]; long timeDelta = timestamp - bucket.Timestamp; if (timeDelta <= MinTimeDelta) { break; } if (_cache.TryRemove(node.Value, out bucket)) { _totalSize -= bucket.Size; _sortedCache.Remove(bucket.Node); } } } } private static long GetTimestamp() { long timestamp = Stopwatch.GetTimestamp(); return timestamp / (Stopwatch.Frequency / 1000); } } }