// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. #nullable disable using System; using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Statistics; namespace osu.Game.Database { /// /// A component which performs lookups (or calculations) and caches the results. /// Currently not persisted between game sessions. /// public abstract partial class MemoryCachingComponent : Component { private readonly ConcurrentDictionary cache = new ConcurrentDictionary(); private readonly GlobalStatistic statistics; protected virtual bool CacheNullValues => true; protected MemoryCachingComponent() { statistics = GlobalStatistics.Get(nameof(MemoryCachingComponent), GetType().ReadableName()); statistics.Value = new MemoryCachingStatistics(); } /// /// Retrieve the cached value for the given lookup. /// /// The lookup to retrieve. /// An optional to cancel the operation. protected async Task GetAsync([NotNull] TLookup lookup, CancellationToken token = default) { if (CheckExists(lookup, out TValue performance)) { statistics.Value.HitCount++; return performance; } var computed = await ComputeValueAsync(lookup, token).ConfigureAwait(false); statistics.Value.MissCount++; if (computed != null || CacheNullValues) { cache[lookup] = computed; statistics.Value.Usage = cache.Count; } return computed; } /// /// Invalidate all entries matching a provided predicate. /// /// The predicate to decide which keys should be invalidated. protected void Invalidate(Func matchKeyPredicate) { foreach (var kvp in cache) { if (matchKeyPredicate(kvp.Key)) cache.TryRemove(kvp.Key, out _); } statistics.Value.Usage = cache.Count; } protected bool CheckExists([NotNull] TLookup lookup, out TValue value) => cache.TryGetValue(lookup, out value); /// /// Called on cache miss to compute the value for the specified lookup. /// /// The lookup to retrieve. /// An optional to cancel the operation. /// The computed value. protected abstract Task ComputeValueAsync(TLookup lookup, CancellationToken token = default); private class MemoryCachingStatistics { /// /// Total number of cache hits. /// public int HitCount; /// /// Total number of cache misses. /// public int MissCount; /// /// Total number of cached entities. /// public int Usage; public override string ToString() { int totalAccesses = HitCount + MissCount; double hitRate = totalAccesses == 0 ? 0 : (double)HitCount / totalAccesses; return $"i:{Usage} h:{HitCount} m:{MissCount} {hitRate:0%}"; } } } }