refactor cache invalidation & propagation to allow for future optimizations

This commit is contained in:
Jesse Plamondon-Willard 2019-12-14 21:31:34 -05:00
parent 6dc442803f
commit 16f986c51b
No known key found for this signature in database
GPG Key ID: CF8B1456B3E29F49
6 changed files with 65 additions and 54 deletions

View File

@ -119,13 +119,12 @@ namespace StardewModdingAPI.Framework.Content
/// <param name="predicate">Matches the asset keys to invalidate.</param> /// <param name="predicate">Matches the asset keys to invalidate.</param>
/// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param> /// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param>
/// <returns>Returns the removed keys (if any).</returns> /// <returns>Returns the removed keys (if any).</returns>
public IEnumerable<string> Remove(Func<string, Type, bool> predicate, bool dispose) public IEnumerable<string> Remove(Func<string, object, bool> predicate, bool dispose)
{ {
List<string> removed = new List<string>(); List<string> removed = new List<string>();
foreach (string key in this.Cache.Keys.ToArray()) foreach (string key in this.Cache.Keys.ToArray())
{ {
Type type = this.Cache[key].GetType(); if (predicate(key, this.Cache[key]))
if (predicate(key, type))
{ {
this.Remove(key, dispose); this.Remove(key, dispose);
removed.Add(key); removed.Add(key);

View File

@ -7,6 +7,7 @@ using Microsoft.Xna.Framework.Content;
using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Content;
using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.ContentManagers;
using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Framework.StateTracking.Comparers;
using StardewModdingAPI.Metadata; using StardewModdingAPI.Metadata;
using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Toolkit.Utilities; using StardewModdingAPI.Toolkit.Utilities;
@ -207,24 +208,28 @@ namespace StardewModdingAPI.Framework
/// <returns>Returns the invalidated asset names.</returns> /// <returns>Returns the invalidated asset names.</returns>
public IEnumerable<string> InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false) public IEnumerable<string> InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false)
{ {
// invalidate cache // invalidate cache & track removed assets
IDictionary<string, Type> removedAssetNames = new Dictionary<string, Type>(StringComparer.InvariantCultureIgnoreCase); IDictionary<string, ISet<object>> removedAssets = new Dictionary<string, ISet<object>>(StringComparer.InvariantCultureIgnoreCase);
foreach (IContentManager contentManager in this.ContentManagers) foreach (IContentManager contentManager in this.ContentManagers)
{ {
foreach (Tuple<string, Type> asset in contentManager.InvalidateCache(predicate, dispose)) foreach (var entry in contentManager.InvalidateCache(predicate, dispose))
removedAssetNames[asset.Item1] = asset.Item2; {
if (!removedAssets.TryGetValue(entry.Key, out ISet<object> assets))
removedAssets[entry.Key] = assets = new HashSet<object>(new ObjectReferenceComparer<object>());
assets.Add(entry.Value);
}
} }
// reload core game assets // reload core game assets
int reloaded = this.CoreAssets.Propagate(this.MainContentManager, removedAssetNames); // use an intercepted content manager if (removedAssets.Any())
{
// report result IDictionary<string, bool> propagated = this.CoreAssets.Propagate(this.MainContentManager, removedAssets.ToDictionary(p => p.Key, p => p.Value.First().GetType())); // use an intercepted content manager
if (removedAssetNames.Any()) this.Monitor.Log($"Invalidated {removedAssets.Count} asset names ({string.Join(", ", removedAssets.Keys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}); propagated {propagated.Count(p => p.Value)} core assets.", LogLevel.Trace);
this.Monitor.Log($"Invalidated {removedAssetNames.Count} asset names: {string.Join(", ", removedAssetNames.Keys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace); }
else else
this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace);
return removedAssetNames.Keys; return removedAssets.Keys;
} }
/// <summary>Dispose held resources.</summary> /// <summary>Dispose held resources.</summary>

View File

@ -184,25 +184,25 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>Purge matched assets from the cache.</summary> /// <summary>Purge matched assets from the cache.</summary>
/// <param name="predicate">Matches the asset keys to invalidate.</param> /// <param name="predicate">Matches the asset keys to invalidate.</param>
/// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param> /// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param>
/// <returns>Returns the invalidated asset names and types.</returns> /// <returns>Returns the invalidated asset names and instances.</returns>
public IEnumerable<Tuple<string, Type>> InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false) public IDictionary<string, object> InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false)
{ {
Dictionary<string, Type> removeAssetNames = new Dictionary<string, Type>(StringComparer.InvariantCultureIgnoreCase); IDictionary<string, object> removeAssets = new Dictionary<string, object>(StringComparer.InvariantCultureIgnoreCase);
this.Cache.Remove((key, type) => this.Cache.Remove((key, asset) =>
{ {
this.ParseCacheKey(key, out string assetName, out _); this.ParseCacheKey(key, out string assetName, out _);
if (removeAssetNames.ContainsKey(assetName)) if (removeAssets.ContainsKey(assetName))
return true; return true;
if (predicate(assetName, type)) if (predicate(assetName, asset.GetType()))
{ {
removeAssetNames[assetName] = type; removeAssets[assetName] = asset;
return true; return true;
} }
return false; return false;
}, dispose); }, dispose);
return removeAssetNames.Select(p => Tuple.Create(p.Key, p.Value)); return removeAssets;
} }
/// <summary>Dispose held resources.</summary> /// <summary>Dispose held resources.</summary>

View File

@ -130,7 +130,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
removeAssetNames.Contains(key) removeAssetNames.Contains(key)
|| (this.TryParseExplicitLanguageAssetKey(key, out string assetName, out _) && removeAssetNames.Contains(assetName)) || (this.TryParseExplicitLanguageAssetKey(key, out string assetName, out _) && removeAssetNames.Contains(assetName))
) )
.Select(p => p.Item1) .Select(p => p.Key)
.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase) .OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase)
.ToArray(); .ToArray();
if (invalidated.Any()) if (invalidated.Any())

View File

@ -66,7 +66,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>Purge matched assets from the cache.</summary> /// <summary>Purge matched assets from the cache.</summary>
/// <param name="predicate">Matches the asset keys to invalidate.</param> /// <param name="predicate">Matches the asset keys to invalidate.</param>
/// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param> /// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param>
/// <returns>Returns the invalidated asset names and types.</returns> /// <returns>Returns the invalidated asset names and instances.</returns>
IEnumerable<Tuple<string, Type>> InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false); IDictionary<string, object> InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false);
} }
} }

View File

@ -65,8 +65,8 @@ namespace StardewModdingAPI.Metadata
/// <summary>Reload one of the game's core assets (if applicable).</summary> /// <summary>Reload one of the game's core assets (if applicable).</summary>
/// <param name="content">The content manager through which to reload the asset.</param> /// <param name="content">The content manager through which to reload the asset.</param>
/// <param name="assets">The asset keys and types to reload.</param> /// <param name="assets">The asset keys and types to reload.</param>
/// <returns>Returns the number of reloaded assets.</returns> /// <returns>Returns a lookup of asset names to whether they've been propagated.</returns>
public int Propagate(LocalizedContentManager content, IDictionary<string, Type> assets) public IDictionary<string, bool> Propagate(LocalizedContentManager content, IDictionary<string, Type> assets)
{ {
// group into optimized lists // group into optimized lists
var buckets = assets.GroupBy(p => var buckets = assets.GroupBy(p =>
@ -81,25 +81,26 @@ namespace StardewModdingAPI.Metadata
}); });
// reload assets // reload assets
int reloaded = 0; IDictionary<string, bool> propagated = assets.ToDictionary(p => p.Key, p => false, StringComparer.InvariantCultureIgnoreCase);
foreach (var bucket in buckets) foreach (var bucket in buckets)
{ {
switch (bucket.Key) switch (bucket.Key)
{ {
case AssetBucket.Sprite: case AssetBucket.Sprite:
reloaded += this.ReloadNpcSprites(content, bucket.Select(p => p.Key)); this.ReloadNpcSprites(content, bucket.Select(p => p.Key), propagated);
break; break;
case AssetBucket.Portrait: case AssetBucket.Portrait:
reloaded += this.ReloadNpcPortraits(content, bucket.Select(p => p.Key)); this.ReloadNpcPortraits(content, bucket.Select(p => p.Key), propagated);
break; break;
default: default:
reloaded += bucket.Count(p => this.PropagateOther(content, p.Key, p.Value)); foreach (var entry in bucket)
propagated[entry.Key] = this.PropagateOther(content, entry.Key, entry.Value);
break; break;
} }
} }
return reloaded; return propagated;
} }
@ -750,51 +751,57 @@ namespace StardewModdingAPI.Metadata
/// <summary>Reload the sprites for matching NPCs.</summary> /// <summary>Reload the sprites for matching NPCs.</summary>
/// <param name="content">The content manager through which to reload the asset.</param> /// <param name="content">The content manager through which to reload the asset.</param>
/// <param name="keys">The asset keys to reload.</param> /// <param name="keys">The asset keys to reload.</param>
/// <returns>Returns the number of reloaded assets.</returns> /// <param name="propagated">The asset keys which have been propagated.</param>
private int ReloadNpcSprites(LocalizedContentManager content, IEnumerable<string> keys) private void ReloadNpcSprites(LocalizedContentManager content, IEnumerable<string> keys, IDictionary<string, bool> propagated)
{ {
// get NPCs // get NPCs
HashSet<string> lookup = new HashSet<string>(keys, StringComparer.InvariantCultureIgnoreCase); HashSet<string> lookup = new HashSet<string>(keys, StringComparer.InvariantCultureIgnoreCase);
NPC[] characters = this.GetCharacters() var characters =
.Where(npc => npc.Sprite != null && lookup.Contains(this.NormalizeAssetNameIgnoringEmpty(npc.Sprite?.Texture?.Name))) (
from npc in this.GetCharacters()
let key = this.NormalizeAssetNameIgnoringEmpty(npc.Sprite?.Texture?.Name)
where key != null && lookup.Contains(key)
select new { Npc = npc, Key = key }
)
.ToArray(); .ToArray();
if (!characters.Any()) if (!characters.Any())
return 0; return;
// update sprite // update sprite
int reloaded = 0; foreach (var target in characters)
foreach (NPC npc in characters)
{ {
this.SetSpriteTexture(npc.Sprite, content.Load<Texture2D>(npc.Sprite.textureName.Value)); this.SetSpriteTexture(target.Npc.Sprite, content.Load<Texture2D>(target.Key));
reloaded++; propagated[target.Key] = true;
} }
return reloaded;
} }
/// <summary>Reload the portraits for matching NPCs.</summary> /// <summary>Reload the portraits for matching NPCs.</summary>
/// <param name="content">The content manager through which to reload the asset.</param> /// <param name="content">The content manager through which to reload the asset.</param>
/// <param name="keys">The asset key to reload.</param> /// <param name="keys">The asset key to reload.</param>
/// <returns>Returns the number of reloaded assets.</returns> /// <param name="propagated">The asset keys which have been propagated.</param>
private int ReloadNpcPortraits(LocalizedContentManager content, IEnumerable<string> keys) private void ReloadNpcPortraits(LocalizedContentManager content, IEnumerable<string> keys, IDictionary<string, bool> propagated)
{ {
// get NPCs // get NPCs
HashSet<string> lookup = new HashSet<string>(keys, StringComparer.InvariantCultureIgnoreCase); HashSet<string> lookup = new HashSet<string>(keys, StringComparer.InvariantCultureIgnoreCase);
var villagers = this var characters =
.GetCharacters() (
.Where(npc => npc.isVillager() && lookup.Contains(this.NormalizeAssetNameIgnoringEmpty(npc.Portrait?.Name))) from npc in this.GetCharacters()
where npc.isVillager()
let key = this.NormalizeAssetNameIgnoringEmpty(npc.Portrait?.Name)
where key != null && lookup.Contains(key)
select new { Npc = npc, Key = key }
)
.ToArray(); .ToArray();
if (!villagers.Any()) if (!characters.Any())
return 0; return;
// update portrait // update portrait
int reloaded = 0; foreach (var target in characters)
foreach (NPC npc in villagers)
{ {
npc.Portrait = content.Load<Texture2D>(npc.Portrait.Name); target.Npc.Portrait = content.Load<Texture2D>(target.Key);
reloaded++; propagated[target.Key] = true;
} }
return reloaded;
} }
/// <summary>Reload tree textures.</summary> /// <summary>Reload tree textures.</summary>