Optimizations

This commit is contained in:
Soreepeong 2023-11-30 16:47:54 +09:00
parent 805615d9f4
commit 5204bb723d
8 changed files with 424 additions and 301 deletions

View file

@ -1,11 +1,12 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
namespace Dalamud.Game.Inventory;
@ -14,193 +15,258 @@ namespace Dalamud.Game.Inventory;
/// This class provides events for the players in-game inventory.
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.EarlyLoadedService]
[ServiceManager.BlockingEarlyLoadedService]
internal class GameInventory : IDisposable, IServiceType, IGameInventory
{
private static readonly ModuleLog Log = new("GameInventory");
private readonly List<IGameInventory.GameInventoryEventArgs> changelog = new();
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
private readonly Dictionary<GameInventoryType, Dictionary<int, InventoryItem>> inventoryCache;
private readonly GameInventoryType[] inventoryTypes;
private readonly GameInventoryItem[][] inventoryItems;
private readonly unsafe GameInventoryItem*[] inventoryItemsPointers;
[ServiceManager.ServiceConstructor]
private GameInventory()
private unsafe GameInventory()
{
this.inventoryCache = new Dictionary<GameInventoryType, Dictionary<int, InventoryItem>>();
this.inventoryTypes = Enum.GetValues<GameInventoryType>();
foreach (var inventoryType in Enum.GetValues<GameInventoryType>())
// Using GC.AllocateArray(pinned: true), so that Unsafe.AsPointer(ref array[0]) does not fail.
this.inventoryItems = new GameInventoryItem[this.inventoryTypes.Length][];
this.inventoryItemsPointers = new GameInventoryItem*[this.inventoryTypes.Length];
for (var i = 0; i < this.inventoryItems.Length; i++)
{
this.inventoryCache.Add(inventoryType, new Dictionary<int, InventoryItem>());
this.inventoryItems[i] = GC.AllocateArray<GameInventoryItem>(1, true);
this.inventoryItemsPointers[i] = (GameInventoryItem*)Unsafe.AsPointer(ref this.inventoryItems[i][0]);
}
this.framework.Update += this.OnFrameworkUpdate;
}
/// <inheritdoc/>
public event IGameInventory.OnItemMovedDelegate? ItemMoved;
/// <inheritdoc/>
public event IGameInventory.OnItemRemovedDelegate? ItemRemoved;
/// <inheritdoc/>
public event IGameInventory.OnItemAddedDelegate? ItemAdded;
/// <inheritdoc/>
public event IGameInventory.OnItemChangedDelegate? ItemChanged;
public event IGameInventory.InventoryChangeDelegate? InventoryChanged;
/// <inheritdoc/>
public void Dispose()
{
this.framework.Update -= this.OnFrameworkUpdate;
}
private void OnFrameworkUpdate(IFramework framework1)
{
// If no one is listening for event's then we don't need to track anything.
if (!this.AnyListeners()) return;
var performanceMonitor = Stopwatch.StartNew();
var changelog = new List<GameInventoryItemChangelog>();
foreach (var (inventoryType, cachedInventoryItems) in this.inventoryCache)
/// <summary>
/// Gets a <see cref="Span{T}"/> view of <see cref="InventoryItem"/>s, wrapped as <see cref="GameInventoryItem"/>.
/// </summary>
/// <param name="type">The inventory type.</param>
/// <returns>The span.</returns>
private static unsafe Span<GameInventoryItem> GetItemsForInventory(GameInventoryType type)
{
var inventoryManager = InventoryManager.Instance();
if (inventoryManager is null) return default;
var inventory = inventoryManager->GetInventoryContainer((InventoryType)type);
if (inventory is null) return default;
return new(inventory->Items, (int)inventory->Size);
}
/// <summary>
/// Looks for the first index of <paramref name="type"/>, or the supposed position one should be if none could be found.
/// </summary>
/// <param name="span">The span to look in.</param>
/// <param name="type">The type.</param>
/// <returns>The index.</returns>
private static int FindTypeIndex(Span<IGameInventory.GameInventoryEventArgs> span, GameInventoryEvent type)
{
// Use linear lookup if span size is small enough
if (span.Length < 64)
{
foreach (var item in this.GetItemsForInventory(inventoryType))
var i = 0;
for (; i < span.Length; i++)
{
if (cachedInventoryItems.TryGetValue(item.Slot, out var inventoryItem))
if (type <= span[i].Type)
break;
}
return i;
}
var lo = 0;
var hi = span.Length - 1;
while (lo <= hi)
{
var i = lo + ((hi - lo) >> 1);
var type2 = span[i].Type;
if (type == type2)
return i;
if (type < type2)
lo = i + 1;
else
hi = i - 1;
}
return lo;
}
private unsafe void OnFrameworkUpdate(IFramework framework1)
{
// TODO: Uncomment this
// // If no one is listening for event's then we don't need to track anything.
// if (this.InventoryChanged is null) return;
for (var i = 0; i < this.inventoryTypes.Length;)
{
var oldItemsArray = this.inventoryItems[i];
var oldItemsLength = oldItemsArray.Length;
var oldItemsPointer = this.inventoryItemsPointers[i];
var resizeRequired = 0;
foreach (ref var newItem in GetItemsForInventory(this.inventoryTypes[i]))
{
var slot = newItem.InternalItem.Slot;
if (slot >= oldItemsLength)
{
// Gained Item
// If the item we have cached has an item id of 0, then we expect it to be an empty slot.
// However, if the item we see in the game data has an item id that is not 0, then it now has an item.
if (inventoryItem.ItemID is 0 && item.ItemID is not 0)
{
var gameInventoryItem = new GameInventoryItem(item);
this.ItemAdded?.Invoke(inventoryType, (uint)item.Slot, gameInventoryItem);
changelog.Add(new GameInventoryItemChangelog(GameInventoryChangelogState.Added, gameInventoryItem));
Log.Verbose($"New Item Added to {inventoryType}: {item.ItemID}");
this.inventoryCache[inventoryType][item.Slot] = item;
}
// Removed Item
// If the item we have cached has an item id of not 0, then we expect it to have an item.
// However, if the item we see in the game data has an item id that is 0, then it was removed from this inventory.
if (inventoryItem.ItemID is not 0 && item.ItemID is 0)
{
var gameInventoryItem = new GameInventoryItem(inventoryItem);
this.ItemRemoved?.Invoke(inventoryType, (uint)item.Slot, gameInventoryItem);
changelog.Add(new GameInventoryItemChangelog(GameInventoryChangelogState.Removed, gameInventoryItem));
Log.Verbose($"Item Removed from {inventoryType}: {inventoryItem.ItemID}");
this.inventoryCache[inventoryType][item.Slot] = item;
}
// Changed Item
// If the item we have cached, does not match the item that we see in the game data
// AND if neither item is empty, then the item has been changed.
if (this.IsItemChanged(inventoryItem, item) && inventoryItem.ItemID is not 0 && item.ItemID is not 0)
{
var gameInventoryItem = new GameInventoryItem(inventoryItem);
this.ItemChanged?.Invoke(inventoryType, (uint)item.Slot, gameInventoryItem);
Log.Verbose($"Item Changed {inventoryType}: {inventoryItem.ItemID}");
this.inventoryCache[inventoryType][item.Slot] = item;
}
resizeRequired = Math.Max(resizeRequired, slot + 1);
continue;
}
// We already checked the range above. Go raw.
ref var oldItem = ref oldItemsPointer[slot];
if (oldItem.IsEmpty)
{
if (newItem.IsEmpty)
continue;
this.changelog.Add(new(GameInventoryEvent.Added, default, newItem));
}
else
{
cachedInventoryItems.Add(item.Slot, item);
if (newItem.IsEmpty)
this.changelog.Add(new(GameInventoryEvent.Removed, oldItem, default));
else if (!oldItem.Equals(newItem))
this.changelog.Add(new(GameInventoryEvent.Changed, oldItem, newItem));
else
continue;
}
Log.Verbose($"[{this.changelog.Count - 1}] {this.changelog[^1]}");
oldItem = newItem;
}
// Did the max slot number get changed?
if (resizeRequired != 0)
{
// Resize our buffer, and then try again.
var oldItemsExpanded = GC.AllocateArray<GameInventoryItem>(resizeRequired, true);
oldItemsArray.CopyTo(oldItemsExpanded, 0);
this.inventoryItems[i] = oldItemsExpanded;
this.inventoryItemsPointers[i] = (GameInventoryItem*)Unsafe.AsPointer(ref oldItemsExpanded[0]);
}
else
{
// Proceed to the next inventory.
i++;
}
}
// Resolve changelog for item moved
// Group all changelogs that have the same itemId, and check if there was an add and a remove event for that item.
foreach (var itemGroup in changelog.GroupBy(log => log.Item.ItemId))
// Was there any change? If not, stop further processing.
if (this.changelog.Count == 0)
return;
try
{
var hasAdd = false;
var hasRemove = false;
foreach (var log in itemGroup)
// From this point, the size of changelog shall not change.
var span = CollectionsMarshal.AsSpan(this.changelog);
span.Sort((a, b) => a.Type.CompareTo(b.Type));
var addedFrom = FindTypeIndex(span, GameInventoryEvent.Added);
var removedFrom = FindTypeIndex(span, GameInventoryEvent.Removed);
var changedFrom = FindTypeIndex(span, GameInventoryEvent.Changed);
// Resolve changelog for item moved, from 1 added + 1 removed
for (var iAdded = addedFrom; iAdded < removedFrom; iAdded++)
{
switch (log.State)
ref var added = ref span[iAdded];
for (var iRemoved = removedFrom; iRemoved < changedFrom; iRemoved++)
{
case GameInventoryChangelogState.Added:
hasAdd = true;
ref var removed = ref span[iRemoved];
if (added.Target.ItemId == removed.Source.ItemId)
{
span[iAdded] = new(GameInventoryEvent.Moved, span[iRemoved].Source, span[iAdded].Target);
span[iRemoved] = default;
Log.Verbose($"[{iAdded}] Interpreting instead as: {span[iAdded]}");
Log.Verbose($"[{iRemoved}] Discarding");
break;
case GameInventoryChangelogState.Removed:
hasRemove = true;
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
var itemMoved = hasAdd && hasRemove;
if (itemMoved)
// Resolve changelog for item moved, from 2 changeds
for (var i = changedFrom; i < this.changelog.Count; i++)
{
var added = itemGroup.FirstOrDefault(log => log.State == GameInventoryChangelogState.Added);
var removed = itemGroup.FirstOrDefault(log => log.State == GameInventoryChangelogState.Removed);
if (added is null || removed is null) continue;
this.ItemMoved?.Invoke(removed.Item.ContainerType, removed.Item.InventorySlot, added.Item.ContainerType, added.Item.InventorySlot, added.Item);
Log.Verbose($"Item Moved {removed.Item.ContainerType}:{removed.Item.InventorySlot} -> {added.Item.ContainerType}:{added.Item.InventorySlot}: {added.Item.ItemId}");
if (span[i].IsEmpty)
continue;
ref var e1 = ref span[i];
for (var j = i + 1; j < this.changelog.Count; j++)
{
ref var e2 = ref span[j];
if (e1.Target.ItemId == e2.Source.ItemId && e1.Source.ItemId == e2.Target.ItemId)
{
if (e1.Target.IsEmpty)
{
// e1 got moved to e2
e1 = new(GameInventoryEvent.Moved, e1.Source, e2.Target);
e2 = default;
Log.Verbose($"[{i}] Interpreting instead as: {e1}");
Log.Verbose($"[{j}] Discarding");
}
else if (e2.Target.IsEmpty)
{
// e2 got moved to e1
e1 = new(GameInventoryEvent.Moved, e2.Source, e1.Target);
e2 = default;
Log.Verbose($"[{i}] Interpreting instead as: {e1}");
Log.Verbose($"[{j}] Discarding");
}
else
{
// e1 and e2 got swapped
(e1, e2) = (new(GameInventoryEvent.Moved, e1.Target, e2.Target),
new(GameInventoryEvent.Moved, e2.Target, e1.Target));
Log.Verbose($"[{i}] Interpreting instead as: {e1}");
Log.Verbose($"[{j}] Interpreting instead as: {e2}");
}
}
}
}
// Filter out the emptied out entries.
// We do not care about the order of items in the changelog anymore.
for (var i = 0; i < span.Length;)
{
if (span[i].IsEmpty)
{
span[i] = span[^1];
span = span[..^1];
}
else
{
i++;
}
}
// Actually broadcast the changes to subscribers.
if (!span.IsEmpty)
this.InventoryChanged?.Invoke(span);
}
finally
{
this.changelog.Clear();
}
var elapsed = performanceMonitor.Elapsed;
Log.Verbose($"Processing Time: {elapsed.Ticks}ticks :: {elapsed.TotalMilliseconds}ms");
}
private bool AnyListeners()
{
if (this.ItemMoved is not null) return true;
if (this.ItemRemoved is not null) return true;
if (this.ItemAdded is not null) return true;
if (this.ItemChanged is not null) return true;
return false;
}
private unsafe ReadOnlySpan<InventoryItem> GetItemsForInventory(GameInventoryType type)
{
var inventoryManager = InventoryManager.Instance();
if (inventoryManager is null) return ReadOnlySpan<InventoryItem>.Empty;
var inventory = inventoryManager->GetInventoryContainer((InventoryType)type);
if (inventory is null) return ReadOnlySpan<InventoryItem>.Empty;
return new ReadOnlySpan<InventoryItem>(inventory->Items, (int)inventory->Size);
}
private bool IsItemChanged(InventoryItem a, InventoryItem b)
{
if (a.Container != b.Container) return true; // Shouldn't be possible, but shouldn't hurt.
if (a.Slot != b.Slot) return true; // Shouldn't be possible, but shouldn't hurt.
if (a.ItemID != b.ItemID) return true;
if (a.Quantity != b.Quantity) return true;
if (a.Spiritbond != b.Spiritbond) return true;
if (a.Condition != b.Condition) return true;
if (a.Flags != b.Flags) return true;
if (a.CrafterContentID != b.CrafterContentID) return true;
if (this.IsMateriaChanged(a, b)) return true;
if (this.IsMateriaGradeChanged(a, b)) return true;
if (a.Stain != b.Stain) return true;
if (a.GlamourID != b.GlamourID) return true;
return false;
}
private unsafe bool IsMateriaChanged(InventoryItem a, InventoryItem b)
=> new ReadOnlySpan<ushort>(a.Materia, 5) != new ReadOnlySpan<ushort>(b.Materia, 5);
private unsafe bool IsMateriaGradeChanged(InventoryItem a, InventoryItem b)
=> new ReadOnlySpan<byte>(a.MateriaGrade, 5) != new ReadOnlySpan<byte>(b.MateriaGrade, 5);
}
/// <summary>
@ -222,47 +288,19 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven
/// </summary>
public GameInventoryPluginScoped()
{
this.gameInventoryService.ItemMoved += this.OnItemMovedForward;
this.gameInventoryService.ItemRemoved += this.OnItemRemovedForward;
this.gameInventoryService.ItemAdded += this.OnItemAddedForward;
this.gameInventoryService.ItemChanged += this.OnItemChangedForward;
this.gameInventoryService.InventoryChanged += this.OnInventoryChangedForward;
}
/// <inheritdoc/>
public event IGameInventory.OnItemMovedDelegate? ItemMoved;
/// <inheritdoc/>
public event IGameInventory.OnItemRemovedDelegate? ItemRemoved;
/// <inheritdoc/>
public event IGameInventory.OnItemAddedDelegate? ItemAdded;
/// <inheritdoc/>
public event IGameInventory.OnItemChangedDelegate? ItemChanged;
public event IGameInventory.InventoryChangeDelegate? InventoryChanged;
/// <inheritdoc/>
public void Dispose()
{
this.gameInventoryService.ItemMoved -= this.OnItemMovedForward;
this.gameInventoryService.ItemRemoved -= this.OnItemRemovedForward;
this.gameInventoryService.ItemAdded -= this.OnItemAddedForward;
this.gameInventoryService.ItemChanged -= this.OnItemChangedForward;
this.ItemMoved = null;
this.ItemRemoved = null;
this.ItemAdded = null;
this.ItemChanged = null;
this.gameInventoryService.InventoryChanged -= this.OnInventoryChangedForward;
this.InventoryChanged = null;
}
private void OnItemMovedForward(GameInventoryType source, uint sourceSlot, GameInventoryType destination, uint destinationSlot, GameInventoryItem item)
=> this.ItemMoved?.Invoke(source, sourceSlot, destination, destinationSlot, item);
private void OnItemRemovedForward(GameInventoryType source, uint sourceSlot, GameInventoryItem item)
=> this.ItemRemoved?.Invoke(source, sourceSlot, item);
private void OnItemAddedForward(GameInventoryType destination, uint destinationSlot, GameInventoryItem item)
=> this.ItemAdded?.Invoke(destination, destinationSlot, item);
private void OnItemChangedForward(GameInventoryType inventory, uint slot, GameInventoryItem item)
=> this.ItemChanged?.Invoke(inventory, slot, item);
private void OnInventoryChangedForward(ReadOnlySpan<IGameInventory.GameInventoryEventArgs> events)
=> this.InventoryChanged?.Invoke(events);
}