Add Object Table Cache (#1708)

Proposed improvement to object table access speeds; prevents creating objects for every plugin iterating the object table.

---------

Co-authored-by: rootdarkarchon <root.darkarchon@outlook.com>
Co-authored-by: Soreepeong <soreepeong@gmail.com>
This commit is contained in:
rootdarkarchon 2024-03-13 22:13:29 +01:00 committed by GitHub
parent 6f2ebdc7a7
commit 31227016c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 230 additions and 41 deletions

View file

@ -1,13 +1,18 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Microsoft.Extensions.ObjectPool;
using Serilog;
namespace Dalamud.Game.ClientState.Objects;
@ -21,22 +26,45 @@ namespace Dalamud.Game.ClientState.Objects;
#pragma warning disable SA1015
[ResolveVia<IObjectTable>]
#pragma warning restore SA1015
internal sealed partial class ObjectTable : IServiceType, IObjectTable
internal sealed partial class ObjectTable : IServiceType, IObjectTable, IDisposable
{
private const int ObjectTableLength = 599;
private readonly ClientStateAddressResolver address;
private readonly ClientState clientState;
private readonly Framework framework;
private readonly CachedEntry[] cachedObjectTable = new CachedEntry[ObjectTableLength];
private readonly ObjectPool<Enumerator> multiThreadedEnumerators =
new DefaultObjectPoolProvider().Create<Enumerator>();
private readonly Enumerator?[] frameworkThreadEnumerators = new Enumerator?[64];
private long nextMultithreadedUsageWarnTime;
[ServiceManager.ServiceConstructor]
private ObjectTable(ClientState clientState)
private ObjectTable(ClientState clientState, Framework framework)
{
this.address = clientState.AddressResolver;
this.clientState = clientState;
this.framework = framework;
foreach (ref var e in this.cachedObjectTable.AsSpan())
e = CachedEntry.CreateNew();
for (var i = 0; i < this.frameworkThreadEnumerators.Length; i++)
this.frameworkThreadEnumerators[i] = new(this, i);
Log.Verbose($"Object table address 0x{this.address.ObjectTable.ToInt64():X}");
framework.BeforeUpdate += this.FrameworkOnBeforeUpdate;
Log.Verbose($"Object table address 0x{this.clientState.AddressResolver.ObjectTable.ToInt64():X}");
}
/// <inheritdoc/>
public IntPtr Address => this.address.ObjectTable;
public nint Address
{
get
{
_ = this.WarnMultithreadedUsage();
return this.clientState.AddressResolver.ObjectTable;
}
}
/// <inheritdoc/>
public int Length => ObjectTableLength;
@ -46,14 +74,17 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
{
get
{
var address = this.GetObjectAddress(index);
return this.CreateObjectReference(address);
_ = this.WarnMultithreadedUsage();
return index is >= ObjectTableLength or < 0 ? null : this.cachedObjectTable[index].ActiveObject;
}
}
/// <inheritdoc/>
public GameObject? SearchById(ulong objectId)
{
_ = this.WarnMultithreadedUsage();
if (objectId is GameObject.InvalidGameObjectId or 0)
return null;
@ -70,23 +101,22 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
}
/// <inheritdoc/>
public unsafe IntPtr GetObjectAddress(int index)
public nint GetObjectAddress(int index)
{
if (index < 0 || index >= ObjectTableLength)
return IntPtr.Zero;
_ = this.WarnMultithreadedUsage();
return *(IntPtr*)(this.address.ObjectTable + (8 * index));
return index is < 0 or >= ObjectTableLength ? nint.Zero : this.GetObjectAddressUnsafe(index);
}
/// <inheritdoc/>
public unsafe GameObject? CreateObjectReference(IntPtr address)
public unsafe GameObject? CreateObjectReference(nint address)
{
var clientState = Service<ClientState>.GetNullable();
_ = this.WarnMultithreadedUsage();
if (clientState == null || clientState.LocalContentId == 0)
if (this.clientState.LocalContentId == 0)
return null;
if (address == IntPtr.Zero)
if (address == nint.Zero)
return null;
var obj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)address;
@ -104,6 +134,88 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
_ => new GameObject(address),
};
}
/// <inheritdoc />
public void Dispose()
{
this.framework.BeforeUpdate -= this.FrameworkOnBeforeUpdate;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool WarnMultithreadedUsage()
{
if (ThreadSafety.IsMainThread)
return false;
var n = Environment.TickCount64;
if (this.nextMultithreadedUsageWarnTime < n)
{
this.nextMultithreadedUsageWarnTime = n + 30000;
Log.Warning(
"{plugin} is accessing {objectTable} outside the main thread. This is deprecated.",
Service<PluginManager>.Get().FindCallingPlugin()?.Name ?? "<unknown plugin>",
nameof(ObjectTable));
}
return true;
}
private void FrameworkOnBeforeUpdate(IFramework unused)
{
for (var i = 0; i < ObjectTableLength; i++)
this.cachedObjectTable[i].Update(this.GetObjectAddressUnsafe(i));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private unsafe nint GetObjectAddressUnsafe(int index) =>
*(nint*)(this.clientState.AddressResolver.ObjectTable + (8 * index));
private struct CachedEntry
{
public GameObject? ActiveObject;
public PlayerCharacter PlayerCharacter;
public BattleNpc BattleNpc;
public Npc Npc;
public EventObj EventObj;
public GameObject GameObject;
public static CachedEntry CreateNew() =>
new()
{
PlayerCharacter = new(nint.Zero),
BattleNpc = new(nint.Zero),
Npc = new(nint.Zero),
EventObj = new(nint.Zero),
GameObject = new(nint.Zero),
};
public unsafe void Update(nint address)
{
if (address == nint.Zero)
{
this.ActiveObject = null;
return;
}
var obj = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)address;
var objKind = (ObjectKind)obj->ObjectKind;
var activeObject = objKind switch
{
ObjectKind.Player => this.PlayerCharacter,
ObjectKind.BattleNpc => this.BattleNpc,
ObjectKind.EventNpc => this.Npc,
ObjectKind.Retainer => this.Npc,
ObjectKind.EventObj => this.EventObj,
ObjectKind.Companion => this.Npc,
ObjectKind.MountType => this.Npc,
ObjectKind.Ornament => this.Npc,
_ => this.GameObject,
};
activeObject.Address = address;
this.ActiveObject = activeObject;
}
}
}
/// <summary>
@ -117,17 +229,87 @@ internal sealed partial class ObjectTable
/// <inheritdoc/>
public IEnumerator<GameObject> GetEnumerator()
{
for (var i = 0; i < ObjectTableLength; i++)
if (this.WarnMultithreadedUsage())
{
var obj = this[i];
if (obj == null)
continue;
yield return obj;
// let's not
var e = this.multiThreadedEnumerators.Get();
e.InitializeForPooledObjects(this);
return e;
}
foreach (ref var x in this.frameworkThreadEnumerators.AsSpan())
{
if (x is not null)
{
var t = x;
x = null;
t.Reset();
return t;
}
}
return new Enumerator(this, -1);
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
private sealed class Enumerator : IEnumerator<GameObject>, IResettable
{
private readonly int slotId;
private ObjectTable? owner;
private int index = -1;
public Enumerator() => this.slotId = -1;
public Enumerator(ObjectTable owner, int slotId)
{
this.owner = owner;
this.slotId = slotId;
}
public GameObject Current { get; private set; } = null!;
object IEnumerator.Current => this.Current;
public bool MoveNext()
{
if (this.index == ObjectTableLength)
return false;
var cache = this.owner!.cachedObjectTable.AsSpan();
for (this.index++; this.index < ObjectTableLength; this.index++)
{
if (cache[this.index].ActiveObject is { } ao)
{
this.Current = ao;
return true;
}
}
return false;
}
public void InitializeForPooledObjects(ObjectTable ot) => this.owner = ot;
public void Reset() => this.index = -1;
public void Dispose()
{
if (this.owner is not { } o)
return;
if (this.index == -1)
o.multiThreadedEnumerators.Return(this);
else
o.frameworkThreadEnumerators[this.slotId] = this;
}
public bool TryReset()
{
this.Reset();
return true;
}
}
}

View file

@ -29,7 +29,7 @@ public unsafe partial class GameObject : IEquatable<GameObject>
/// <summary>
/// Gets the address of the game object in memory.
/// </summary>
public IntPtr Address { get; }
public IntPtr Address { get; internal set; }
/// <summary>
/// Gets the Dalamud instance.

View file

@ -25,9 +25,9 @@ namespace Dalamud.Game;
internal sealed class Framework : IDisposable, IServiceType, IFramework
{
private static readonly ModuleLog Log = new("Framework");
private static readonly Stopwatch StatsStopwatch = new();
private readonly GameLifecycle lifecycle;
private readonly Stopwatch updateStopwatch = new();
@ -76,6 +76,11 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
/// <inheritdoc/>
public event IFramework.OnUpdateDelegate? Update;
/// <summary>
/// Executes during FrameworkUpdate before all <see cref="Update"/> delegates.
/// </summary>
internal event IFramework.OnUpdateDelegate BeforeUpdate;
/// <summary>
/// Gets or sets a value indicating whether the collection of stats is enabled.
/// </summary>
@ -280,7 +285,7 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
this.updateStopwatch.Reset();
StatsStopwatch.Reset();
}
/// <summary>
/// Adds a update time to the stats history.
/// </summary>
@ -307,7 +312,7 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
internal void ProfileAndInvoke(IFramework.OnUpdateDelegate? eventDelegate, IFramework frameworkInstance)
{
if (eventDelegate is null) return;
var invokeList = eventDelegate.GetInvocationList();
// Individually invoke OnUpdate handlers and time them.
@ -353,6 +358,8 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
ThreadSafety.MarkMainThread();
this.BeforeUpdate?.InvokeSafely(this);
this.hitchDetector.Start();
try
@ -425,7 +432,7 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework
this.hitchDetector.Stop();
original:
original:
return this.updateHook.OriginalDisposeSafe(framework);
}
@ -558,19 +565,19 @@ internal class FrameworkPluginScoped : IDisposable, IServiceType, IFramework
/// <inheritdoc/>
public DateTime LastUpdate => this.frameworkService.LastUpdate;
/// <inheritdoc/>
public DateTime LastUpdateUTC => this.frameworkService.LastUpdateUTC;
/// <inheritdoc/>
public TimeSpan UpdateDelta => this.frameworkService.UpdateDelta;
/// <inheritdoc/>
public bool IsInFrameworkUpdateThread => this.frameworkService.IsInFrameworkUpdateThread;
/// <inheritdoc/>
public bool IsFrameworkUnloading => this.frameworkService.IsFrameworkUnloading;
/// <inheritdoc/>
public void Dispose()
{
@ -582,27 +589,27 @@ internal class FrameworkPluginScoped : IDisposable, IServiceType, IFramework
/// <inheritdoc/>
public Task<T> RunOnFrameworkThread<T>(Func<T> func)
=> this.frameworkService.RunOnFrameworkThread(func);
/// <inheritdoc/>
public Task RunOnFrameworkThread(Action action)
=> this.frameworkService.RunOnFrameworkThread(action);
/// <inheritdoc/>
public Task<T> RunOnFrameworkThread<T>(Func<Task<T>> func)
=> this.frameworkService.RunOnFrameworkThread(func);
/// <inheritdoc/>
public Task RunOnFrameworkThread(Func<Task> func)
=> this.frameworkService.RunOnFrameworkThread(func);
/// <inheritdoc/>
public Task<T> RunOnTick<T>(Func<T> func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
=> this.frameworkService.RunOnTick(func, delay, delayTicks, cancellationToken);
/// <inheritdoc/>
public Task RunOnTick(Action action, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
=> this.frameworkService.RunOnTick(action, delay, delayTicks, cancellationToken);
/// <inheritdoc/>
public Task<T> RunOnTick<T>(Func<Task<T>> func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default)
=> this.frameworkService.RunOnTick(func, delay, delayTicks, cancellationToken);