From 16f986c51b9c87c2253a39fd771dcc24f7c43db4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 14 Dec 2019 21:31:34 -0500 Subject: [PATCH] refactor cache invalidation & propagation to allow for future optimizations --- src/SMAPI/Framework/Content/ContentCache.cs | 5 +- src/SMAPI/Framework/ContentCoordinator.cs | 25 ++++--- .../ContentManagers/BaseContentManager.cs | 16 ++--- .../ContentManagers/GameContentManager.cs | 2 +- .../ContentManagers/IContentManager.cs | 4 +- src/SMAPI/Metadata/CoreAssetPropagator.cs | 67 ++++++++++--------- 6 files changed, 65 insertions(+), 54 deletions(-) diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs index c252b7b6..f33ff84d 100644 --- a/src/SMAPI/Framework/Content/ContentCache.cs +++ b/src/SMAPI/Framework/Content/ContentCache.cs @@ -119,13 +119,12 @@ namespace StardewModdingAPI.Framework.Content /// Matches the asset keys to invalidate. /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. /// Returns the removed keys (if any). - public IEnumerable Remove(Func predicate, bool dispose) + public IEnumerable Remove(Func predicate, bool dispose) { List removed = new List(); foreach (string key in this.Cache.Keys.ToArray()) { - Type type = this.Cache[key].GetType(); - if (predicate(key, type)) + if (predicate(key, this.Cache[key])) { this.Remove(key, dispose); removed.Add(key); diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 97b54c5b..82d3805b 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -7,6 +7,7 @@ using Microsoft.Xna.Framework.Content; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.StateTracking.Comparers; using StardewModdingAPI.Metadata; using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Utilities; @@ -207,24 +208,28 @@ namespace StardewModdingAPI.Framework /// Returns the invalidated asset names. public IEnumerable InvalidateCache(Func predicate, bool dispose = false) { - // invalidate cache - IDictionary removedAssetNames = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + // invalidate cache & track removed assets + IDictionary> removedAssets = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); foreach (IContentManager contentManager in this.ContentManagers) { - foreach (Tuple asset in contentManager.InvalidateCache(predicate, dispose)) - removedAssetNames[asset.Item1] = asset.Item2; + foreach (var entry in contentManager.InvalidateCache(predicate, dispose)) + { + if (!removedAssets.TryGetValue(entry.Key, out ISet assets)) + removedAssets[entry.Key] = assets = new HashSet(new ObjectReferenceComparer()); + assets.Add(entry.Value); + } } // reload core game assets - int reloaded = this.CoreAssets.Propagate(this.MainContentManager, removedAssetNames); // use an intercepted content manager - - // report result - if (removedAssetNames.Any()) - this.Monitor.Log($"Invalidated {removedAssetNames.Count} asset names: {string.Join(", ", removedAssetNames.Keys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace); + if (removedAssets.Any()) + { + IDictionary propagated = this.CoreAssets.Propagate(this.MainContentManager, removedAssets.ToDictionary(p => p.Key, p => p.Value.First().GetType())); // use an intercepted content manager + 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); + } else this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); - return removedAssetNames.Keys; + return removedAssets.Keys; } /// Dispose held resources. diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs index 4cfeeeba..41ce7c37 100644 --- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs @@ -184,25 +184,25 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Purge matched assets from the cache. /// Matches the asset keys to invalidate. /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. - /// Returns the invalidated asset names and types. - public IEnumerable> InvalidateCache(Func predicate, bool dispose = false) + /// Returns the invalidated asset names and instances. + public IDictionary InvalidateCache(Func predicate, bool dispose = false) { - Dictionary removeAssetNames = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - this.Cache.Remove((key, type) => + IDictionary removeAssets = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + this.Cache.Remove((key, asset) => { this.ParseCacheKey(key, out string assetName, out _); - if (removeAssetNames.ContainsKey(assetName)) + if (removeAssets.ContainsKey(assetName)) return true; - if (predicate(assetName, type)) + if (predicate(assetName, asset.GetType())) { - removeAssetNames[assetName] = type; + removeAssets[assetName] = asset; return true; } return false; }, dispose); - return removeAssetNames.Select(p => Tuple.Create(p.Key, p.Value)); + return removeAssets; } /// Dispose held resources. diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 04c4564f..8930267d 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -130,7 +130,7 @@ namespace StardewModdingAPI.Framework.ContentManagers removeAssetNames.Contains(key) || (this.TryParseExplicitLanguageAssetKey(key, out string assetName, out _) && removeAssetNames.Contains(assetName)) ) - .Select(p => p.Item1) + .Select(p => p.Key) .OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase) .ToArray(); if (invalidated.Any()) diff --git a/src/SMAPI/Framework/ContentManagers/IContentManager.cs b/src/SMAPI/Framework/ContentManagers/IContentManager.cs index 12c01352..8da9a777 100644 --- a/src/SMAPI/Framework/ContentManagers/IContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/IContentManager.cs @@ -66,7 +66,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Purge matched assets from the cache. /// Matches the asset keys to invalidate. /// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game. - /// Returns the invalidated asset names and types. - IEnumerable> InvalidateCache(Func predicate, bool dispose = false); + /// Returns the invalidated asset names and instances. + IDictionary InvalidateCache(Func predicate, bool dispose = false); } } diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 8b00d893..84102828 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -65,8 +65,8 @@ namespace StardewModdingAPI.Metadata /// Reload one of the game's core assets (if applicable). /// The content manager through which to reload the asset. /// The asset keys and types to reload. - /// Returns the number of reloaded assets. - public int Propagate(LocalizedContentManager content, IDictionary assets) + /// Returns a lookup of asset names to whether they've been propagated. + public IDictionary Propagate(LocalizedContentManager content, IDictionary assets) { // group into optimized lists var buckets = assets.GroupBy(p => @@ -81,25 +81,26 @@ namespace StardewModdingAPI.Metadata }); // reload assets - int reloaded = 0; + IDictionary propagated = assets.ToDictionary(p => p.Key, p => false, StringComparer.InvariantCultureIgnoreCase); foreach (var bucket in buckets) { switch (bucket.Key) { case AssetBucket.Sprite: - reloaded += this.ReloadNpcSprites(content, bucket.Select(p => p.Key)); + this.ReloadNpcSprites(content, bucket.Select(p => p.Key), propagated); break; case AssetBucket.Portrait: - reloaded += this.ReloadNpcPortraits(content, bucket.Select(p => p.Key)); + this.ReloadNpcPortraits(content, bucket.Select(p => p.Key), propagated); break; 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; } } - return reloaded; + return propagated; } @@ -750,51 +751,57 @@ namespace StardewModdingAPI.Metadata /// Reload the sprites for matching NPCs. /// The content manager through which to reload the asset. /// The asset keys to reload. - /// Returns the number of reloaded assets. - private int ReloadNpcSprites(LocalizedContentManager content, IEnumerable keys) + /// The asset keys which have been propagated. + private void ReloadNpcSprites(LocalizedContentManager content, IEnumerable keys, IDictionary propagated) { // get NPCs HashSet lookup = new HashSet(keys, StringComparer.InvariantCultureIgnoreCase); - NPC[] characters = this.GetCharacters() - .Where(npc => npc.Sprite != null && lookup.Contains(this.NormalizeAssetNameIgnoringEmpty(npc.Sprite?.Texture?.Name))) + var characters = + ( + 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(); if (!characters.Any()) - return 0; + return; // update sprite - int reloaded = 0; - foreach (NPC npc in characters) + foreach (var target in characters) { - this.SetSpriteTexture(npc.Sprite, content.Load(npc.Sprite.textureName.Value)); - reloaded++; + this.SetSpriteTexture(target.Npc.Sprite, content.Load(target.Key)); + propagated[target.Key] = true; } - - return reloaded; } /// Reload the portraits for matching NPCs. /// The content manager through which to reload the asset. /// The asset key to reload. - /// Returns the number of reloaded assets. - private int ReloadNpcPortraits(LocalizedContentManager content, IEnumerable keys) + /// The asset keys which have been propagated. + private void ReloadNpcPortraits(LocalizedContentManager content, IEnumerable keys, IDictionary propagated) { // get NPCs HashSet lookup = new HashSet(keys, StringComparer.InvariantCultureIgnoreCase); - var villagers = this - .GetCharacters() - .Where(npc => npc.isVillager() && lookup.Contains(this.NormalizeAssetNameIgnoringEmpty(npc.Portrait?.Name))) + var characters = + ( + 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(); - if (!villagers.Any()) - return 0; + if (!characters.Any()) + return; // update portrait - int reloaded = 0; - foreach (NPC npc in villagers) + foreach (var target in characters) { - npc.Portrait = content.Load(npc.Portrait.Name); - reloaded++; + target.Npc.Portrait = content.Load(target.Key); + propagated[target.Key] = true; } - return reloaded; } /// Reload tree textures.