reduce allocations in collection watchers

This commit is contained in:
Jesse Plamondon-Willard
2026-03-27 23:59:33 -04:00
parent aaa23edc49
commit 45216fe62b
3 changed files with 49 additions and 17 deletions
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using StardewValley.Extensions;
namespace StardewModdingAPI.Framework.StateTracking.FieldWatchers;
@@ -15,6 +16,9 @@ internal class ComparableListWatcher<TValue> : BaseDisposableWatcher, ICollectio
/// <summary>The values during the previous update.</summary>
private HashSet<TValue> LastValues;
/// <summary>A pooled set used to update <see cref="LastValues"/> each update.</summary>
private HashSet<TValue> NewValues;
/// <summary>The pairs added since the last reset.</summary>
private readonly List<TValue> AddedImpl = [];
@@ -50,6 +54,7 @@ internal class ComparableListWatcher<TValue> : BaseDisposableWatcher, ICollectio
this.Name = name;
this.CurrentValues = values;
this.LastValues = new HashSet<TValue>(comparer);
this.NewValues = new HashSet<TValue>(comparer);
}
/// <inheritdoc />
@@ -68,19 +73,24 @@ internal class ComparableListWatcher<TValue> : BaseDisposableWatcher, ICollectio
return;
}
// get new values
this.NewValues.Clear();
this.NewValues.AddRange(this.CurrentValues);
// detect changes
HashSet<TValue> curValues = new HashSet<TValue>(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
}
/// <inheritdoc />
@@ -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, ICollectionWatcher<Item
private readonly Inventory Inventory;
/// <summary>The pairs added since the last reset.</summary>
private readonly ISet<Item> AddedImpl = new HashSet<Item>(new ObjectReferenceComparer<Item>());
private readonly HashSet<Item> AddedImpl = new(new ObjectReferenceComparer<Item>());
/// <summary>The pairs removed since the last reset.</summary>
private readonly ISet<Item> RemovedImpl = new HashSet<Item>(new ObjectReferenceComparer<Item>());
private readonly HashSet<Item> RemovedImpl = new(new ObjectReferenceComparer<Item>());
/// <summary>A pooled set used to track the previous inventory when detecting changes.</summary>
private readonly HashSet<Item> PooledOldSet = new(new ObjectReferenceComparer<Item>());
/// <summary>A pooled set used to track the new inventory when detecting changes.</summary>
private readonly HashSet<Item> PooledNewSet = new(new ObjectReferenceComparer<Item>());
/*********
@@ -87,17 +94,21 @@ internal class InventoryWatcher : BaseDisposableWatcher, ICollectionWatcher<Item
/// <param name="newValues">The new list of values.</param>
private void OnInventoryReplaced(Inventory inventory, IList<Item> oldValues, IList<Item> newValues)
{
ISet<Item> oldSet = new HashSet<Item>(oldValues, new ObjectReferenceComparer<Item>());
ISet<Item> changed = new HashSet<Item>(newValues, new ObjectReferenceComparer<Item>());
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);
}
}
@@ -20,6 +20,12 @@ internal class PlayerTracker : IDisposable
/// <summary>The player's inventory change as of the last update.</summary>
private readonly Dictionary<Item, int> CurrentInventory = [];
/// <summary>A pooled set used to track the added items when detecting changes.</summary>
private readonly HashSet<Item> PooledAdded;
/// <summary>A pooled set used to track the removed items when detecting changes.</summary>
private readonly HashSet<Item> PooledRemoved;
/// <summary>The player's last valid location.</summary>
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<Item>();
this.PooledAdded = new HashSet<Item>(comparer);
this.PooledRemoved = new HashSet<Item>(comparer);
}
/// <summary>Update the current values if needed.</summary>
@@ -109,22 +120,22 @@ internal class PlayerTracker : IDisposable
/// <returns>Returns whether anything changed.</returns>
public bool TryGetInventoryChanges([NotNullWhen(true)] out SnapshotItemListDiff? changes)
{
HashSet<Item> added = new(new ObjectReferenceComparer<Item>());
HashSet<Item> removed = new(new ObjectReferenceComparer<Item>());
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);
}
/// <summary>Release watchers and resources.</summary>