diff --git a/Dalamud/Game/Internal/Framework.cs b/Dalamud/Game/Internal/Framework.cs index e65ead3a0..e55f502ee 100644 --- a/Dalamud/Game/Internal/Framework.cs +++ b/Dalamud/Game/Internal/Framework.cs @@ -1,11 +1,13 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; using System.Runtime.InteropServices; using Dalamud.Game.Internal.Gui; using Dalamud.Game.Internal.Libc; using Dalamud.Game.Internal.Network; using Dalamud.Hooking; using Serilog; -using Dalamud.Game.Internal.File; namespace Dalamud.Game.Internal { /// @@ -30,6 +32,11 @@ namespace Dalamud.Game.Internal { /// public FrameworkAddressResolver Address { get; } +#region Stats + public static bool StatsEnabled { get; set; } + public static Dictionary> StatsHistory = new Dictionary>(); + private static Stopwatch statsStopwatch = new Stopwatch(); +#endregion #region Subsystems /// @@ -108,7 +115,35 @@ namespace Dalamud.Game.Internal { } try { - OnUpdateEvent?.Invoke(this); + if (StatsEnabled && OnUpdateEvent != null) { + // Stat Tracking for Framework Updates + var invokeList = OnUpdateEvent.GetInvocationList(); + var notUpdated = StatsHistory.Keys.ToList(); + // Individually invoke OnUpdate handlers and time them. + foreach (var d in invokeList) { + statsStopwatch.Restart(); + d.Method.Invoke(d.Target, new object[]{ this }); + statsStopwatch.Stop(); + var key = $"{d.Target}::{d.Method.Name}"; + if (notUpdated.Contains(key)) notUpdated.Remove(key); + if (!StatsHistory.ContainsKey(key)) StatsHistory.Add(key, new List()); + StatsHistory[key].Add(statsStopwatch.Elapsed.TotalMilliseconds); + if (StatsHistory[key].Count > 1000) { + StatsHistory[key].RemoveRange(0, StatsHistory[key].Count - 1000); + } + } + + // Cleanup handlers that are no longer being called + foreach (var key in notUpdated) { + if (StatsHistory[key].Count > 0) { + StatsHistory[key].RemoveAt(0); + } else { + StatsHistory.Remove(key); + } + } + } else { + OnUpdateEvent?.Invoke(this); + } } catch (Exception ex) { Log.Error(ex, "Exception while dispatching Framework::Update event."); } diff --git a/Dalamud/Hooking/Hook.cs b/Dalamud/Hooking/Hook.cs index c6311a79d..a2c4ec3f6 100644 --- a/Dalamud/Hooking/Hook.cs +++ b/Dalamud/Hooking/Hook.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using EasyHook; @@ -9,9 +10,7 @@ namespace Dalamud.Hooking { /// This class is basically a thin wrapper around the LocalHook type to provide helper functions. /// /// Delegate type to represents a function prototype. This must be the same prototype as original function do. - public sealed class Hook : IDisposable where T : Delegate { - private bool isDisposed; - + public sealed class Hook : IDisposable, IDalamudHook where T : Delegate { private readonly IntPtr address; private readonly T original; @@ -69,19 +68,20 @@ namespace Dalamud.Hooking { this.hookInfo = LocalHook.Create(address, detour, callbackParam); // Installs a hook here this.address = address; this.original = Marshal.GetDelegateForFunctionPointer(this.hookInfo.HookBypassAddress); + HookInfo.TrackedHooks.Add(new HookInfo() { Delegate = detour, Hook = this, Assembly = Assembly.GetCallingAssembly()}); } /// /// Remove a hook from the current process. /// public void Dispose() { - if (this.isDisposed) { + if (this.IsDisposed) { return; } this.hookInfo.Dispose(); - this.isDisposed = true; + this.IsDisposed = true; } /// @@ -104,9 +104,24 @@ namespace Dalamud.Hooking { [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CheckDisposed() { - if (this.isDisposed) { + if (this.IsDisposed) { throw new ObjectDisposedException("Hook is already disposed."); } } + + /// + /// Check if the hook is enabled. + /// + public bool IsEnabled { + get { + CheckDisposed(); + return this.hookInfo.ThreadACL.IsExclusive; + } + } + + /// + /// Check if the hook has been disposed + /// + public bool IsDisposed { get; private set; } } } diff --git a/Dalamud/Hooking/HookInfo.cs b/Dalamud/Hooking/HookInfo.cs new file mode 100644 index 000000000..6f5e1d9e0 --- /dev/null +++ b/Dalamud/Hooking/HookInfo.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; + +namespace Dalamud.Hooking { + internal class HookInfo { + + internal static List TrackedHooks = new List(); + + internal IDalamudHook Hook { get; set; } + internal Delegate Delegate { get; set; } + internal Assembly Assembly { get; set; } + + private ulong? inProcessMemory = 0; + internal ulong? InProcessMemory { + get { + if (Hook.IsDisposed) return 0; + if (this.inProcessMemory == null) return null; + if (this.inProcessMemory.Value > 0) return this.inProcessMemory.Value; + var p = Process.GetCurrentProcess().MainModule; + var begin = (ulong) p.BaseAddress.ToInt64(); + var end = begin + (ulong) p.ModuleMemorySize; + var hookAddr = (ulong) Hook.Address.ToInt64(); + if (hookAddr >= begin && hookAddr <= end) { + this.inProcessMemory = hookAddr - begin; + return this.inProcessMemory.Value; + } else { + this.inProcessMemory = null; + return null; + } + } + } + + } +} diff --git a/Dalamud/Hooking/IDalamudHook.cs b/Dalamud/Hooking/IDalamudHook.cs new file mode 100644 index 000000000..fb756229d --- /dev/null +++ b/Dalamud/Hooking/IDalamudHook.cs @@ -0,0 +1,9 @@ +using System; + +namespace Dalamud.Hooking { + internal interface IDalamudHook { + public IntPtr Address { get; } + public bool IsEnabled { get; } + public bool IsDisposed { get; } + } +} diff --git a/Dalamud/Interface/DalamudPluginStatWindow.cs b/Dalamud/Interface/DalamudPluginStatWindow.cs index 7862075f1..d99192a14 100644 --- a/Dalamud/Interface/DalamudPluginStatWindow.cs +++ b/Dalamud/Interface/DalamudPluginStatWindow.cs @@ -1,14 +1,19 @@ using System; using System.Linq; +using System.Reflection; +using Dalamud.Game.Internal; +using Dalamud.Hooking; using Dalamud.Plugin; using ImGuiNET; namespace Dalamud.Interface { - class DalamudPluginStatWindow : IDisposable { + internal class DalamudPluginStatWindow : IDisposable { - private PluginManager pm; - public DalamudPluginStatWindow(PluginManager pm) { - this.pm = pm; + private readonly PluginManager pluginManager; + private bool showDalamudHooks; + + public DalamudPluginStatWindow(PluginManager pluginManager) { + this.pluginManager = pluginManager; } public bool Draw() { @@ -29,7 +34,7 @@ namespace Dalamud.Interface { ImGui.SameLine(); if (ImGui.Button("Reset")) { - foreach (var a in this.pm.Plugins) { + foreach (var a in this.pluginManager.Plugins) { a.PluginInterface.UiBuilder.lastDrawTime = -1; a.PluginInterface.UiBuilder.maxDrawTime = -1; a.PluginInterface.UiBuilder.drawTimeHistory.Clear(); @@ -51,7 +56,7 @@ namespace Dalamud.Interface { ImGui.Text("Average"); ImGui.NextColumn(); ImGui.Separator(); - foreach (var a in this.pm.Plugins) { + foreach (var a in this.pluginManager.Plugins) { ImGui.Text(a.Definition.Name); ImGui.NextColumn(); ImGui.Text($"{a.PluginInterface.UiBuilder.lastDrawTime/10000f:F4}ms"); @@ -69,7 +74,117 @@ namespace Dalamud.Interface { ImGui.Columns(1); } + ImGui.EndTabItem(); + } + + if (ImGui.BeginTabItem("Framework times")) { + + var doStats = Framework.StatsEnabled; + + if (ImGui.Checkbox("Enable Framework Update Tracking", ref doStats)) { + Framework.StatsEnabled = doStats; + } + + if (doStats) { + ImGui.SameLine(); + if (ImGui.Button("Reset")) { + Framework.StatsHistory.Clear(); + } + + ImGui.Columns(4); + ImGui.SetColumnWidth(0, ImGui.GetWindowContentRegionWidth() - 300); + ImGui.SetColumnWidth(1, 100f); + ImGui.SetColumnWidth(2, 100f); + ImGui.SetColumnWidth(3, 100f); + ImGui.Text("Method"); + ImGui.NextColumn(); + ImGui.Text("Last"); + ImGui.NextColumn(); + ImGui.Text("Longest"); + ImGui.NextColumn(); + ImGui.Text("Average"); + ImGui.NextColumn(); + ImGui.Separator(); + ImGui.Separator(); + + foreach (var handlerHistory in Framework.StatsHistory) { + if (handlerHistory.Value.Count == 0) continue; + ImGui.SameLine(); + ImGui.Text($"{handlerHistory.Key}"); + ImGui.NextColumn(); + ImGui.Text($"{handlerHistory.Value.Last():F4}ms"); + ImGui.NextColumn(); + ImGui.Text($"{handlerHistory.Value.Max():F4}ms"); + ImGui.NextColumn(); + ImGui.Text($"{handlerHistory.Value.Average():F4}ms"); + ImGui.NextColumn(); + ImGui.Separator(); + } + ImGui.Columns(0); + } + + ImGui.EndTabItem(); + } + + if (ImGui.BeginTabItem("Hooks")) { + ImGui.Columns(3); + ImGui.SetColumnWidth(0, ImGui.GetWindowContentRegionWidth() - 280); + ImGui.SetColumnWidth(1, 180f); + ImGui.SetColumnWidth(2, 100f); + ImGui.Text("Detour Method"); + ImGui.SameLine(); + ImGui.Text(" "); + ImGui.SameLine(); + ImGui.Checkbox("Show Dalamud Hooks ###showDalamudHooksCheckbox", ref showDalamudHooks); + ImGui.NextColumn(); + ImGui.Text("Address"); + ImGui.NextColumn(); + ImGui.Text("Status"); + ImGui.NextColumn(); + ImGui.Separator(); + ImGui.Separator(); + + foreach (var trackedHook in HookInfo.TrackedHooks) { + try { + if (!this.showDalamudHooks && trackedHook.Assembly == Assembly.GetExecutingAssembly()) continue; + + ImGui.Text($"{trackedHook.Delegate.Target} :: {trackedHook.Delegate.Method.Name}"); + ImGui.TextDisabled(trackedHook.Assembly.FullName); + ImGui.NextColumn(); + if (!trackedHook.Hook.IsDisposed) { + ImGui.Text($"{trackedHook.Hook.Address.ToInt64():X}"); + if (ImGui.IsItemClicked()) ImGui.SetClipboardText($"{trackedHook.Hook.Address.ToInt64():X}"); + + var processMemoryOffset = trackedHook.InProcessMemory; + if (processMemoryOffset.HasValue) { + ImGui.Text($"ffxiv_dx11.exe + {processMemoryOffset:X}"); + if (ImGui.IsItemClicked()) ImGui.SetClipboardText($"ffxiv_dx11.exe+{processMemoryOffset:X}"); + } + + } + ImGui.NextColumn(); + + if (trackedHook.Hook.IsDisposed) { + ImGui.Text("Disposed"); + } else { + ImGui.Text(trackedHook.Hook.IsEnabled ? "Enabled" : "Disabled"); + } + + ImGui.NextColumn(); + } catch (Exception ex) { + ImGui.Text(ex.Message); + ImGui.NextColumn(); + while (ImGui.GetColumnIndex() != 0) ImGui.NextColumn(); + } + + ImGui.Separator(); + } + ImGui.Columns(); + } + + if (ImGui.IsWindowAppearing()) { + HookInfo.TrackedHooks.RemoveAll(h => h.Hook.IsDisposed); } ImGui.EndTabBar();