From 45216fe62b9c0402d8627f0fc48a31429f5e7c59 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Mar 2026 23:59:33 -0400 Subject: [PATCH] reduce allocations in collection watchers --- .../FieldWatchers/ComparableListWatcher.cs | 18 ++++++++++--- .../FieldWatchers/InventoryWatcher.cs | 27 +++++++++++++------ .../Framework/StateTracking/PlayerTracker.cs | 21 +++++++++++---- 3 files changed, 49 insertions(+), 17 deletions(-) diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs index 4e5c06b3..e2e6c015 100644 --- a/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/ComparableListWatcher.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using StardewValley.Extensions; namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers; @@ -15,6 +16,9 @@ internal class ComparableListWatcher : BaseDisposableWatcher, ICollectio /// The values during the previous update. private HashSet LastValues; + /// A pooled set used to update each update. + private HashSet NewValues; + /// The pairs added since the last reset. private readonly List AddedImpl = []; @@ -50,6 +54,7 @@ internal class ComparableListWatcher : BaseDisposableWatcher, ICollectio this.Name = name; this.CurrentValues = values; this.LastValues = new HashSet(comparer); + this.NewValues = new HashSet(comparer); } /// @@ -68,19 +73,24 @@ internal class ComparableListWatcher : BaseDisposableWatcher, ICollectio return; } + // get new values + this.NewValues.Clear(); + this.NewValues.AddRange(this.CurrentValues); + // detect changes - HashSet curValues = new HashSet(this.CurrentValues, this.LastValues.Comparer); foreach (TValue value in this.LastValues) { - if (!curValues.Contains(value)) + if (!this.NewValues.Contains(value)) this.RemovedImpl.Add(value); } - foreach (TValue value in curValues) + foreach (TValue value in this.NewValues) { if (!this.LastValues.Contains(value)) this.AddedImpl.Add(value); } - this.LastValues = curValues; + + // save result + (this.LastValues, this.NewValues) = (this.NewValues, this.LastValues); // reuse the now-unused previous 'LastValues' set on the next update } /// diff --git a/src/SMAPI/Framework/StateTracking/FieldWatchers/InventoryWatcher.cs b/src/SMAPI/Framework/StateTracking/FieldWatchers/InventoryWatcher.cs index d009ecd8..a5fd5c5c 100644 --- a/src/SMAPI/Framework/StateTracking/FieldWatchers/InventoryWatcher.cs +++ b/src/SMAPI/Framework/StateTracking/FieldWatchers/InventoryWatcher.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using StardewModdingAPI.Framework.StateTracking.Comparers; using StardewValley; +using StardewValley.Extensions; using StardewValley.Inventories; namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers; @@ -15,10 +16,16 @@ internal class InventoryWatcher : BaseDisposableWatcher, ICollectionWatcherThe pairs added since the last reset. - private readonly ISet AddedImpl = new HashSet(new ObjectReferenceComparer()); + private readonly HashSet AddedImpl = new(new ObjectReferenceComparer()); /// The pairs removed since the last reset. - private readonly ISet RemovedImpl = new HashSet(new ObjectReferenceComparer()); + private readonly HashSet RemovedImpl = new(new ObjectReferenceComparer()); + + /// A pooled set used to track the previous inventory when detecting changes. + private readonly HashSet PooledOldSet = new(new ObjectReferenceComparer()); + + /// A pooled set used to track the new inventory when detecting changes. + private readonly HashSet PooledNewSet = new(new ObjectReferenceComparer()); /********* @@ -87,17 +94,21 @@ internal class InventoryWatcher : BaseDisposableWatcher, ICollectionWatcherThe new list of values. private void OnInventoryReplaced(Inventory inventory, IList oldValues, IList newValues) { - ISet oldSet = new HashSet(oldValues, new ObjectReferenceComparer()); - ISet changed = new HashSet(newValues, new ObjectReferenceComparer()); + this.PooledOldSet.Clear(); + this.PooledOldSet.AddRange(oldValues); - foreach (Item value in oldSet) + this.PooledNewSet.Clear(); + this.PooledNewSet.AddRange(newValues); + + foreach (Item value in this.PooledOldSet) { - if (!changed.Contains(value)) + if (!this.PooledNewSet.Contains(value)) this.Remove(value); } - foreach (Item value in changed) + + foreach (Item value in this.PooledNewSet) { - if (!oldSet.Contains(value)) + if (!this.PooledOldSet.Contains(value)) this.Add(value); } } diff --git a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs index 94081c43..dc7533a2 100644 --- a/src/SMAPI/Framework/StateTracking/PlayerTracker.cs +++ b/src/SMAPI/Framework/StateTracking/PlayerTracker.cs @@ -20,6 +20,12 @@ internal class PlayerTracker : IDisposable /// The player's inventory change as of the last update. private readonly Dictionary CurrentInventory = []; + /// A pooled set used to track the added items when detecting changes. + private readonly HashSet PooledAdded; + + /// A pooled set used to track the removed items when detecting changes. + private readonly HashSet PooledRemoved; + /// The player's last valid location. private GameLocation? LastValidLocation; @@ -67,6 +73,11 @@ internal class PlayerTracker : IDisposable // track watchers for convenience this.Watchers.Add(this.LocationWatcher); this.Watchers.AddRange(this.SkillWatchers.Values); + + // init pooled sets + var comparer = new ObjectReferenceComparer(); + this.PooledAdded = new HashSet(comparer); + this.PooledRemoved = new HashSet(comparer); } /// Update the current values if needed. @@ -109,22 +120,22 @@ internal class PlayerTracker : IDisposable /// Returns whether anything changed. public bool TryGetInventoryChanges([NotNullWhen(true)] out SnapshotItemListDiff? changes) { - HashSet added = new(new ObjectReferenceComparer()); - HashSet removed = new(new ObjectReferenceComparer()); + this.PooledAdded.Clear(); + this.PooledRemoved.Clear(); foreach (Item item in this.PreviousInventory.Keys) { if (!this.CurrentInventory.ContainsKey(item)) - removed.Add(item); + this.PooledRemoved.Add(item); } foreach (Item item in this.CurrentInventory.Keys) { if (!this.PreviousInventory.ContainsKey(item)) - added.Add(item); + this.PooledAdded.Add(item); } - return SnapshotItemListDiff.TryGetChanges(added: added, removed: removed, stackSizes: this.PreviousInventory, out changes); + return SnapshotItemListDiff.TryGetChanges(added: this.PooledAdded, removed: this.PooledRemoved, stackSizes: this.PreviousInventory, out changes); } /// Release watchers and resources.