diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index b2d4c1108..fdcaa2d44 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -104,6 +104,11 @@ namespace Dalamud Service.Set(); +#if DEBUG + Service.Set(); + Log.Information("[T1] TaskTracker OK!"); +#endif + // Initialize the process information. Service.Set(new SigScanner(true)); Service.Set(); diff --git a/Dalamud/Interface/Internal/Windows/DataWindow.cs b/Dalamud/Interface/Internal/Windows/DataWindow.cs index d61a67311..678d9563a 100644 --- a/Dalamud/Interface/Internal/Windows/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/DataWindow.cs @@ -2,6 +2,9 @@ using System; using System.Collections.Generic; using System.Linq; using System.Numerics; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Data; @@ -27,6 +30,7 @@ using Dalamud.Game.Text; using Dalamud.Interface.Colors; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Windowing; +using Dalamud.Logging.Internal; using Dalamud.Memory; using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc.Internal; @@ -137,6 +141,7 @@ namespace Dalamud.Interface.Internal.Windows KeyState, Gamepad, Configuration, + TaskSched, } /// @@ -304,6 +309,10 @@ namespace Dalamud.Interface.Internal.Windows case DataKind.Configuration: this.DrawConfiguration(); break; + + case DataKind.TaskSched: + this.DrawTaskSched(); + break; } } else @@ -1256,6 +1265,127 @@ namespace Dalamud.Interface.Internal.Windows Util.ShowObject(config); } + private void DrawTaskSched() + { + if (ImGui.Button("Clear list")) + { + TaskTracker.Clear(); + } + + ImGui.SameLine(); + ImGuiHelpers.ScaledDummy(10); + ImGui.SameLine(); + + if (ImGui.Button("Short Task.Run")) + { + Task.Run(() => { Thread.Sleep(500); }); + } + + ImGui.SameLine(); + + if (ImGui.Button("Task in task(Delay)")) + { + Task.Run(async () => await this.TestTaskInTaskDelay()); + } + + ImGui.SameLine(); + + if (ImGui.Button("Task in task(Sleep)")) + { + Task.Run(async () => await this.TestTaskInTaskSleep()); + } + + ImGui.SameLine(); + + if (ImGui.Button("Faulting task")) + { + Task.Run(() => + { + Thread.Sleep(200); + + string a = null; + a.Contains("dalamud"); + }); + } + + ImGuiHelpers.ScaledDummy(20); + + // Needed to init the task tracker, if we're not on a debug build + var tracker = Service.GetNullable() ?? Service.Set(); + + for (var i = 0; i < TaskTracker.Tasks.Count; i++) + { + var task = TaskTracker.Tasks[i]; + var subTime = DateTime.Now; + if (task.Task == null) + subTime = task.FinishTime; + + switch (task.Status) + { + case TaskStatus.Created: + case TaskStatus.WaitingForActivation: + case TaskStatus.WaitingToRun: + ImGui.PushStyleColor(ImGuiCol.Header, ImGuiColors.DalamudGrey); + break; + case TaskStatus.Running: + case TaskStatus.WaitingForChildrenToComplete: + ImGui.PushStyleColor(ImGuiCol.Header, ImGuiColors.ParsedBlue); + break; + case TaskStatus.RanToCompletion: + ImGui.PushStyleColor(ImGuiCol.Header, ImGuiColors.ParsedGreen); + break; + case TaskStatus.Canceled: + case TaskStatus.Faulted: + ImGui.PushStyleColor(ImGuiCol.Header, ImGuiColors.DalamudRed); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + if (ImGui.CollapsingHeader($"#{task.Id} - {task.Status} {(subTime - task.StartTime).TotalMilliseconds}ms###task{i}")) + { + if (ImGui.Button("CANCEL (May not work)")) + { + try + { + var cancelFunc = + typeof(Task).GetMethod("InternalCancel", BindingFlags.NonPublic | BindingFlags.Instance); + cancelFunc.Invoke(task, null); + } + catch (Exception ex) + { + Log.Error(ex, "Could not cancel task."); + } + } + + ImGuiHelpers.ScaledDummy(10); + + ImGui.TextUnformatted(task.StackTrace.ToString()); + + if (task.Exception != null) + { + ImGuiHelpers.ScaledDummy(15); + ImGui.TextColored(ImGuiColors.DalamudRed, "EXCEPTION:"); + ImGui.TextUnformatted(task.Exception.ToString()); + } + } + + ImGui.PopStyleColor(1); + } + } + + private async Task TestTaskInTaskDelay() + { + await Task.Delay(5000); + } + +#pragma warning disable 1998 + private async Task TestTaskInTaskSleep() +#pragma warning restore 1998 + { + Thread.Sleep(5000); + } + private void Load() { var dataManager = Service.Get(); diff --git a/Dalamud/Logging/Internal/TaskTracker.cs b/Dalamud/Logging/Internal/TaskTracker.cs new file mode 100644 index 000000000..3526d273f --- /dev/null +++ b/Dalamud/Logging/Internal/TaskTracker.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Threading.Tasks; + +using Dalamud.Game; +using MonoMod.RuntimeDetour; +using Serilog; + +namespace Dalamud.Logging.Internal +{ + /// + /// Class responsible for tracking asynchronous tasks. + /// + internal class TaskTracker : IDisposable + { + private static readonly List TrackedTasksInternal = new(); + + private Hook? scheduleAndStartHook; + + /// + /// Initializes a new instance of the class. + /// + public TaskTracker() + { + this.ApplyPatch(); + + var framework = Service.Get(); + framework.Update += this.FrameworkOnUpdate; + } + + /// + /// Gets a read-only list of tracked tasks. + /// + public static IReadOnlyList Tasks => TrackedTasksInternal; + + /// + /// Clear the list of tracked tasks. + /// + public static void Clear() => TrackedTasksInternal.Clear(); + + /// + /// Update the tracked data. + /// + public static void UpdateData() + { + foreach (var taskInfo in TrackedTasksInternal) + { + if (taskInfo.Task == null) + continue; + + taskInfo.IsCompleted = taskInfo.Task.IsCompleted; + taskInfo.IsFaulted = taskInfo.Task.IsFaulted; + taskInfo.IsCanceled = taskInfo.Task.IsCanceled; + taskInfo.IsCompletedSuccessfully = taskInfo.Task.IsCompletedSuccessfully; + taskInfo.Status = taskInfo.Task.Status; + + if (taskInfo.IsCompleted || taskInfo.IsFaulted || taskInfo.IsCanceled || + taskInfo.IsCompletedSuccessfully) + { + taskInfo.Exception = taskInfo.Task.Exception; + + taskInfo.Task = null; + taskInfo.FinishTime = DateTime.Now; + } + } + } + + /// + public void Dispose() + { + this.scheduleAndStartHook?.Dispose(); + + var framework = Service.Get(); + framework.Update -= this.FrameworkOnUpdate; + } + + private static bool AddToActiveTasksHook(Func orig, Task self) + { + orig(self); + + var trace = new StackTrace(); + TrackedTasksInternal.Add(new TaskInfo + { + Task = self, + Id = self.Id, + StackTrace = trace, + }); + + return true; + } + + private void FrameworkOnUpdate(Framework framework) + { + UpdateData(); + } + + private void ApplyPatch() + { + var targetType = typeof(Task); + + var debugField = targetType.GetField("s_asyncDebuggingEnabled", BindingFlags.Static | BindingFlags.NonPublic); + debugField.SetValue(null, true); + + Log.Information("s_asyncDebuggingEnabled: {0}", debugField.GetValue(null)); + + var targetMethod = targetType.GetMethod("AddToActiveTasks", BindingFlags.Static | BindingFlags.NonPublic); + var patchMethod = + typeof(TaskTracker).GetMethod("AddToActiveTasksHook", BindingFlags.NonPublic | BindingFlags.Static); + + if (targetMethod == null) + Log.Error("TargetMethod null!"); + + if (patchMethod == null) + Log.Error("PatchMethod null!"); + + this.scheduleAndStartHook = new Hook(targetMethod, patchMethod); + + Log.Information("AddToActiveTasks Hooked!"); + } + + /// + /// Class representing a tracked task. + /// + internal class TaskInfo + { + /// + /// Gets or sets the tracked task. + /// + public Task? Task { get; set; } + + /// + /// Gets or sets the ID of the task. + /// + public int Id { get; set; } + + /// + /// Gets or sets the stack trace of where the task was started. + /// + public StackTrace? StackTrace { get; set; } + + /// + /// Gets or sets a value indicating whether or not the task was completed. + /// + public bool IsCompleted { get; set; } + + /// + /// Gets or sets a value indicating whether or not the task faulted. + /// + public bool IsFaulted { get; set; } + + /// + /// Gets or sets a value indicating whether or not the task was canceled. + /// + public bool IsCanceled { get; set; } + + /// + /// Gets or sets a value indicating whether or not the task was completed successfully. + /// + public bool IsCompletedSuccessfully { get; set; } + + /// + /// Gets or sets the status of the task. + /// + public TaskStatus Status { get; set; } + + /// + /// Gets the start time of the task. + /// + public DateTime StartTime { get; } = DateTime.Now; + + /// + /// Gets or sets the end time of the task. + /// + public DateTime FinishTime { get; set; } + + /// + /// Gets or sets the exception that occurred within the task. + /// + public AggregateException? Exception { get; set; } + } + } +} diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index c9ede6123..d5a07d43c 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1064,15 +1064,15 @@ namespace Dalamud.Plugin.Internal /// internal partial class PluginManager { - private MonoMod.RuntimeDetour.Hook assemblyLocationMonoHook; - private MonoMod.RuntimeDetour.Hook assemblyCodeBaseMonoHook; - /// /// A mapping of plugin assembly name to patch data. Used to fill in missing data due to loading /// plugins via byte[]. /// internal static readonly Dictionary PluginLocations = new(); + private MonoMod.RuntimeDetour.Hook assemblyLocationMonoHook; + private MonoMod.RuntimeDetour.Hook assemblyCodeBaseMonoHook; + /// /// Patch method for internal class RuntimeAssembly.Location, also known as Assembly.Location. /// This patch facilitates resolving the assembly location for plugins that are loaded via byte[].