Reduce heap allocation every frame in AddonLifecycle (#1555)

This commit is contained in:
srkizer 2023-12-08 08:49:09 +09:00 committed by GitHub
parent 711d5e2859
commit 0bfcc55774
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 212 additions and 188 deletions

View file

@ -14,6 +14,7 @@ public abstract unsafe class AddonArgs
public const string InvalidAddon = "NullAddon";
private string? addonName;
private IntPtr addon;
/// <summary>
/// Gets the name of the addon this args referrers to.
@ -23,13 +24,41 @@ public abstract unsafe class AddonArgs
/// <summary>
/// Gets the pointer to the addons AtkUnitBase.
/// </summary>
public nint Addon { get; init; }
public nint Addon
{
get => this.addon;
internal set
{
if (this.addon == value)
return;
this.addon = value;
this.addonName = null;
}
}
/// <summary>
/// Gets the type of these args.
/// </summary>
public abstract AddonArgsType Type { get; }
/// <summary>
/// Checks if addon name matches the given span of char.
/// </summary>
/// <param name="name">The name to check.</param>
/// <returns>Whether it is the case.</returns>
internal bool IsAddon(ReadOnlySpan<char> name)
{
if (this.Addon == nint.Zero) return false;
if (name.Length is 0 or > 0x20)
return false;
var addonPointer = (AtkUnitBase*)this.Addon;
if (addonPointer->Name is null) return false;
return MemoryHelper.EqualsZeroTerminatedString(name, (nint)addonPointer->Name, null, 0x20);
}
/// <summary>
/// Helper method for ensuring the name of the addon is valid.
/// </summary>

View file

@ -3,8 +3,14 @@
/// <summary>
/// Addon argument data for Draw events.
/// </summary>
public class AddonDrawArgs : AddonArgs
public class AddonDrawArgs : AddonArgs, ICloneable
{
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Draw;
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonDrawArgs Clone() => (AddonDrawArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
}

View file

@ -3,8 +3,14 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for ReceiveEvent events.
/// </summary>
public class AddonFinalizeArgs : AddonArgs
public class AddonFinalizeArgs : AddonArgs, ICloneable
{
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Finalize;
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonFinalizeArgs Clone() => (AddonFinalizeArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
}

View file

@ -3,28 +3,34 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for ReceiveEvent events.
/// </summary>
public class AddonReceiveEventArgs : AddonArgs
public class AddonReceiveEventArgs : AddonArgs, ICloneable
{
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.ReceiveEvent;
/// <summary>
/// Gets the AtkEventType for this event message.
/// Gets or sets the AtkEventType for this event message.
/// </summary>
public byte AtkEventType { get; init; }
public byte AtkEventType { get; set; }
/// <summary>
/// Gets the event id for this event message.
/// Gets or sets the event id for this event message.
/// </summary>
public int EventParam { get; init; }
public int EventParam { get; set; }
/// <summary>
/// Gets the pointer to an AtkEvent for this event message.
/// Gets or sets the pointer to an AtkEvent for this event message.
/// </summary>
public nint AtkEvent { get; init; }
public nint AtkEvent { get; set; }
/// <summary>
/// Gets the pointer to a block of data for this event message.
/// Gets or sets the pointer to a block of data for this event message.
/// </summary>
public nint Data { get; init; }
public nint Data { get; set; }
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonReceiveEventArgs Clone() => (AddonReceiveEventArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
}

View file

@ -5,23 +5,29 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Refresh events.
/// </summary>
public class AddonRefreshArgs : AddonArgs
public class AddonRefreshArgs : AddonArgs, ICloneable
{
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Refresh;
/// <summary>
/// Gets the number of AtkValues.
/// Gets or sets the number of AtkValues.
/// </summary>
public uint AtkValueCount { get; init; }
public uint AtkValueCount { get; set; }
/// <summary>
/// Gets the address of the AtkValue array.
/// Gets or sets the address of the AtkValue array.
/// </summary>
public nint AtkValues { get; init; }
public nint AtkValues { get; set; }
/// <summary>
/// Gets the AtkValues in the form of a span.
/// </summary>
public unsafe Span<AtkValue> AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonRefreshArgs Clone() => (AddonRefreshArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
}

View file

@ -3,18 +3,24 @@
/// <summary>
/// Addon argument data for OnRequestedUpdate events.
/// </summary>
public class AddonRequestedUpdateArgs : AddonArgs
public class AddonRequestedUpdateArgs : AddonArgs, ICloneable
{
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.RequestedUpdate;
/// <summary>
/// Gets the NumberArrayData** for this event.
/// Gets or sets the NumberArrayData** for this event.
/// </summary>
public nint NumberArrayData { get; init; }
public nint NumberArrayData { get; set; }
/// <summary>
/// Gets the StringArrayData** for this event.
/// Gets or sets the StringArrayData** for this event.
/// </summary>
public nint StringArrayData { get; init; }
public nint StringArrayData { get; set; }
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonRequestedUpdateArgs Clone() => (AddonRequestedUpdateArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
}

View file

@ -5,23 +5,29 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Setup events.
/// </summary>
public class AddonSetupArgs : AddonArgs
public class AddonSetupArgs : AddonArgs, ICloneable
{
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Setup;
/// <summary>
/// Gets the number of AtkValues.
/// Gets or sets the number of AtkValues.
/// </summary>
public uint AtkValueCount { get; init; }
public uint AtkValueCount { get; set; }
/// <summary>
/// Gets the address of the AtkValue array.
/// Gets or sets the address of the AtkValue array.
/// </summary>
public nint AtkValues { get; init; }
public nint AtkValues { get; set; }
/// <summary>
/// Gets the AtkValues in the form of a span.
/// </summary>
public unsafe Span<AtkValue> AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonSetupArgs Clone() => (AddonSetupArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
}

View file

@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Update events.
/// </summary>
public class AddonUpdateArgs : AddonArgs
public class AddonUpdateArgs : AddonArgs, ICloneable
{
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Update;
@ -11,5 +11,11 @@ public class AddonUpdateArgs : AddonArgs
/// <summary>
/// Gets the time since the last update.
/// </summary>
public float TimeDelta { get; init; }
public float TimeDelta { get; internal set; }
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonUpdateArgs Clone() => (AddonUpdateArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
}

View file

@ -1,6 +1,7 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Hooking;
@ -40,6 +41,15 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
private readonly ConcurrentBag<AddonLifecycleEventListener> newEventListeners = new();
private readonly ConcurrentBag<AddonLifecycleEventListener> removeEventListeners = new();
// Note: these can be sourced from ObjectPool of appropriate types instead, but since we don't import that NuGet
// package, and these events are always called from the main thread, this is fine.
private readonly AddonSetupArgs recyclingSetupArgs = new();
private readonly AddonFinalizeArgs recyclingFinalizeArgs = new();
private readonly AddonDrawArgs recyclingDrawArgs = new();
private readonly AddonUpdateArgs recyclingUpdateArgs = new();
private readonly AddonRefreshArgs recyclingRefreshArgs = new();
private readonly AddonRequestedUpdateArgs recyclingRequestedUpdateArgs = new();
[ServiceManager.ServiceConstructor]
private AddonLifecycle(TargetSigScanner sigScanner)
{
@ -132,13 +142,28 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
/// </summary>
/// <param name="eventType">Event Type.</param>
/// <param name="args">AddonArgs.</param>
internal void InvokeListeners(AddonEvent eventType, AddonArgs args)
/// <param name="blame">What to blame on errors.</param>
internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "")
{
// Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better.
foreach (var listener in this.EventListeners)
{
if (listener.EventType != eventType)
continue;
// Match on string.empty for listeners that want events for all addons.
foreach (var listener in this.EventListeners.Where(listener => listener.EventType == eventType && (listener.AddonName == args.AddonName || listener.AddonName == string.Empty)))
if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName))
continue;
try
{
listener.FunctionDelegate.Invoke(eventType, args);
}
catch (Exception e)
{
Log.Error(e, $"Exception in {blame} during {eventType} invoke.");
}
}
}
// Used to prevent concurrency issues if plugins try to register during iteration of listeners.
@ -250,19 +275,12 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration.");
}
try
{
this.InvokeListeners(AddonEvent.PreSetup, new AddonSetupArgs
{
Addon = (nint)addon,
AtkValueCount = valueCount,
AtkValues = (nint)values,
});
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonSetup pre-setup invoke.");
}
this.recyclingSetupArgs.Addon = (nint)addon;
this.recyclingSetupArgs.AtkValueCount = valueCount;
this.recyclingSetupArgs.AtkValues = (nint)values;
this.InvokeListenersSafely(AddonEvent.PreSetup, this.recyclingSetupArgs);
valueCount = this.recyclingSetupArgs.AtkValueCount;
values = (AtkValue*)this.recyclingSetupArgs.AtkValues;
try
{
@ -273,19 +291,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method.");
}
try
{
this.InvokeListeners(AddonEvent.PostSetup, new AddonSetupArgs
{
Addon = (nint)addon,
AtkValueCount = valueCount,
AtkValues = (nint)values,
});
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonSetup post-setup invoke.");
}
this.InvokeListenersSafely(AddonEvent.PostSetup, this.recyclingSetupArgs);
}
private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase)
@ -300,14 +306,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal.");
}
try
{
this.InvokeListeners(AddonEvent.PreFinalize, new AddonFinalizeArgs { Addon = (nint)atkUnitBase[0] });
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonFinalize pre-finalize invoke.");
}
this.recyclingFinalizeArgs.Addon = (nint)atkUnitBase[0];
this.InvokeListenersSafely(AddonEvent.PreFinalize, this.recyclingFinalizeArgs);
try
{
@ -321,14 +321,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
private void OnAddonDraw(AtkUnitBase* addon)
{
try
{
this.InvokeListeners(AddonEvent.PreDraw, new AddonDrawArgs { Addon = (nint)addon });
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonDraw pre-draw invoke.");
}
this.recyclingDrawArgs.Addon = (nint)addon;
this.InvokeListenersSafely(AddonEvent.PreDraw, this.recyclingDrawArgs);
try
{
@ -339,26 +333,14 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method.");
}
try
{
this.InvokeListeners(AddonEvent.PostDraw, new AddonDrawArgs { Addon = (nint)addon });
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonDraw post-draw invoke.");
}
this.InvokeListenersSafely(AddonEvent.PostDraw, this.recyclingDrawArgs);
}
private void OnAddonUpdate(AtkUnitBase* addon, float delta)
{
try
{
this.InvokeListeners(AddonEvent.PreUpdate, new AddonUpdateArgs { Addon = (nint)addon, TimeDelta = delta });
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonUpdate pre-update invoke.");
}
this.recyclingUpdateArgs.Addon = (nint)addon;
this.recyclingUpdateArgs.TimeDelta = delta;
this.InvokeListenersSafely(AddonEvent.PreUpdate, this.recyclingUpdateArgs);
try
{
@ -369,33 +351,19 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method.");
}
try
{
this.InvokeListeners(AddonEvent.PostUpdate, new AddonUpdateArgs { Addon = (nint)addon, TimeDelta = delta });
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonUpdate post-update invoke.");
}
this.InvokeListenersSafely(AddonEvent.PostUpdate, this.recyclingUpdateArgs);
}
private byte OnAddonRefresh(AtkUnitManager* atkUnitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values)
{
byte result = 0;
try
{
this.InvokeListeners(AddonEvent.PreRefresh, new AddonRefreshArgs
{
Addon = (nint)addon,
AtkValueCount = valueCount,
AtkValues = (nint)values,
});
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonRefresh pre-refresh invoke.");
}
this.recyclingRefreshArgs.Addon = (nint)addon;
this.recyclingRefreshArgs.AtkValueCount = valueCount;
this.recyclingRefreshArgs.AtkValues = (nint)values;
this.InvokeListenersSafely(AddonEvent.PreRefresh, this.recyclingRefreshArgs);
valueCount = this.recyclingRefreshArgs.AtkValueCount;
values = (AtkValue*)this.recyclingRefreshArgs.AtkValues;
try
{
@ -406,38 +374,18 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method.");
}
try
{
this.InvokeListeners(AddonEvent.PostRefresh, new AddonRefreshArgs
{
Addon = (nint)addon,
AtkValueCount = valueCount,
AtkValues = (nint)values,
});
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonRefresh post-refresh invoke.");
}
this.InvokeListenersSafely(AddonEvent.PostRefresh, this.recyclingRefreshArgs);
return result;
}
private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
{
try
{
this.InvokeListeners(AddonEvent.PreRequestedUpdate, new AddonRequestedUpdateArgs
{
Addon = (nint)addon,
NumberArrayData = (nint)numberArrayData,
StringArrayData = (nint)stringArrayData,
});
}
catch (Exception e)
{
Log.Error(e, "Exception in OnRequestedUpdate pre-requestedUpdate invoke.");
}
this.recyclingRequestedUpdateArgs.Addon = (nint)addon;
this.recyclingRequestedUpdateArgs.NumberArrayData = (nint)numberArrayData;
this.recyclingRequestedUpdateArgs.StringArrayData = (nint)stringArrayData;
this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.recyclingRequestedUpdateArgs);
numberArrayData = (NumberArrayData**)this.recyclingRequestedUpdateArgs.NumberArrayData;
stringArrayData = (StringArrayData**)this.recyclingRequestedUpdateArgs.StringArrayData;
try
{
@ -448,19 +396,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method.");
}
try
{
this.InvokeListeners(AddonEvent.PostRequestedUpdate, new AddonRequestedUpdateArgs
{
Addon = (nint)addon,
NumberArrayData = (nint)numberArrayData,
StringArrayData = (nint)stringArrayData,
});
}
catch (Exception e)
{
Log.Error(e, "Exception in OnRequestedUpdate post-requestedUpdate invoke.");
}
this.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.recyclingRequestedUpdateArgs);
}
}

View file

@ -16,6 +16,10 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
{
private static readonly ModuleLog Log = new("AddonLifecycle");
// Note: these can be sourced from ObjectPool of appropriate types instead, but since we don't import that NuGet
// package, and these events are always called from the main thread, this is fine.
private readonly AddonReceiveEventArgs recyclingReceiveEventArgs = new();
/// <summary>
/// Initializes a new instance of the <see cref="AddonLifecycleReceiveEventListener"/> class.
/// </summary>
@ -75,21 +79,16 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
return;
}
try
{
this.AddonLifecycle.InvokeListeners(AddonEvent.PreReceiveEvent, new AddonReceiveEventArgs
{
Addon = (nint)addon,
AtkEventType = (byte)eventType,
EventParam = eventParam,
AtkEvent = (nint)atkEvent,
Data = data,
});
}
catch (Exception e)
{
Log.Error(e, "Exception in OnReceiveEvent pre-receiveEvent invoke.");
}
this.recyclingReceiveEventArgs.Addon = (nint)addon;
this.recyclingReceiveEventArgs.AtkEventType = (byte)eventType;
this.recyclingReceiveEventArgs.EventParam = eventParam;
this.recyclingReceiveEventArgs.AtkEvent = (IntPtr)atkEvent;
this.recyclingReceiveEventArgs.Data = data;
this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.recyclingReceiveEventArgs);
eventType = (AtkEventType)this.recyclingReceiveEventArgs.AtkEventType;
eventParam = this.recyclingReceiveEventArgs.EventParam;
atkEvent = (AtkEvent*)this.recyclingReceiveEventArgs.AtkEvent;
data = this.recyclingReceiveEventArgs.Data;
try
{
@ -100,20 +99,6 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
}
try
{
this.AddonLifecycle.InvokeListeners(AddonEvent.PostReceiveEvent, new AddonReceiveEventArgs
{
Addon = (nint)addon,
AtkEventType = (byte)eventType,
EventParam = eventParam,
AtkEvent = (nint)atkEvent,
Data = data,
});
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonRefresh post-receiveEvent invoke.");
}
this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.recyclingReceiveEventArgs);
}
}

View file

@ -163,6 +163,38 @@ public static unsafe class MemoryHelper
#region ReadString
/// <summary>
/// Compares if the given char span equals to the null-terminated string at <paramref name="memoryAddress"/>.
/// </summary>
/// <param name="charSpan">The character span.</param>
/// <param name="memoryAddress">The address of null-terminated string.</param>
/// <param name="encoding">The encoding of the null-terminated string.</param>
/// <param name="maxLength">The maximum length of the null-terminated string.</param>
/// <returns>Whether they are equal.</returns>
public static bool EqualsZeroTerminatedString(
ReadOnlySpan<char> charSpan,
nint memoryAddress,
Encoding? encoding = null,
int maxLength = int.MaxValue)
{
encoding ??= Encoding.UTF8;
maxLength = Math.Min(maxLength, charSpan.Length + 4);
var pmem = ((byte*)memoryAddress)!;
var length = 0;
while (length < maxLength && pmem[length] != 0)
length++;
var mem = new Span<byte>(pmem, length);
var memCharCount = encoding.GetCharCount(mem);
if (memCharCount != charSpan.Length)
return false;
Span<char> chars = stackalloc char[memCharCount];
encoding.GetChars(mem, chars);
return charSpan.SequenceEqual(chars);
}
/// <summary>
/// Read a UTF-8 encoded string from a specified memory address.
/// </summary>