using Microsoft.Extensions.Caching.Memory;
using Sandbox;
using System.Collections.Concurrent;
using System.Reflection;
///
/// We only want 1 instance of a Resource class in C# and we want that to have 1 strong handle to native.
/// So we need a WeakReference lookup everytime we get a Resource from native to match that class.
/// This way GC can work for us and free anything we're no longer using anywhere, fantastic!
///
/// However sometimes GC is very good at it's job and will free Resources we don't keep a strong reference to
/// in generation 0 or 1 immediately after usage. This can cause the resource to need to be loaded every frame.
/// Or worse be finalized at unpredictable times.
///
/// So we keep a sliding memory cache of the Resources - realistically these only need to live for an extra frame.
/// But it's probably nice to keep around for longer if they're going to be used on and off.
///
internal static class NativeResourceCache
{
static readonly TimeSpan SlidingExpiration = TimeSpan.FromSeconds( 30 );
static readonly MemoryCache MemoryCache = new( new MemoryCacheOptions() { } );
///
/// We still want a WeakReference cache because we might have a strong reference somewhere to a resource
/// that has been expired from the cache. And we absolutely only want 1 instance of the resource.
///
static readonly ConcurrentDictionary WeakTable = new();
private static Action StartScanForExpiredItemsIfNeeded { get; } = typeof( MemoryCache )
.GetMethod( nameof( StartScanForExpiredItemsIfNeeded ), BindingFlags.Instance | BindingFlags.NonPublic )
.CreateDelegate>();
internal static void Add( long key, object value )
{
var cacheEntryOptions = new MemoryCacheEntryOptions().SetSlidingExpiration( SlidingExpiration );
MemoryCache.Set( key, value, cacheEntryOptions );
WeakTable.TryAdd( key, new WeakReference( value ) );
}
internal static bool TryGetValue( long key, out T value ) where T : class
{
if ( MemoryCache.TryGetValue( key, out value ) )
{
return true;
}
// If we missed the Cache, check our weak refs
if ( WeakTable.TryGetValue( key, out var weakValue ) && weakValue.Target is not null )
{
value = weakValue.Target as T;
// and add it back to the cache
Add( key, value );
return true;
}
return false;
}
static TimeSince LastScan = 0;
///
/// Ticks the underlying MemoryCache to clear expired entries
///
internal static void Tick()
{
if ( LastScan < 30 )
return;
LastScan = 0;
// MemoryCache doesn't have its own timer for clearing anything...
// This will get rid of any expired stuff
StartScanForExpiredItemsIfNeeded( MemoryCache, DateTime.UtcNow );
}
///
/// Clear the cache when games are closed etc. ready for a
///
internal static void Clear()
{
MemoryCache.Clear();
}
}