From 9413755ee3d34acf2f8922ff91c41af6ba180960 Mon Sep 17 00:00:00 2001 From: kizer Date: Thu, 12 May 2022 17:36:05 +0900 Subject: [PATCH] Add Service.RunOnTick() (#832) --- Dalamud/Game/Framework.cs | 179 +++++++++++++++++- .../Interface/Internal/Windows/DataWindow.cs | 64 ++++++- 2 files changed, 237 insertions(+), 6 deletions(-) diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index ede9f7cbb..52b9ef020 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -4,7 +4,7 @@ using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Threading; - +using System.Threading.Tasks; using Dalamud.Game.Gui; using Dalamud.Game.Gui.Toast; using Dalamud.Game.Libc; @@ -26,7 +26,9 @@ namespace Dalamud.Game public sealed class Framework : IDisposable { private static Stopwatch statsStopwatch = new(); - private Stopwatch updateStopwatch = new(); + + private readonly List runOnNextTickTaskList = new(); + private readonly Stopwatch updateStopwatch = new(); private bool tier2Initialized = false; private bool tier3Initialized = false; @@ -36,6 +38,8 @@ namespace Dalamud.Game private Hook destroyHook; private Hook realDestroyHook; + private Thread? frameworkUpdateThread; + /// /// Initializes a new instance of the class. /// @@ -113,6 +117,11 @@ namespace Dalamud.Game /// public TimeSpan UpdateDelta { get; private set; } = TimeSpan.Zero; + /// + /// Gets a value indicating whether currently executing code is running in the game's framework update thread. + /// + public bool IsInFrameworkUpdateThread => Thread.CurrentThread == this.frameworkUpdateThread; + /// /// Gets or sets a value indicating whether to dispatch update events. /// @@ -132,6 +141,85 @@ namespace Dalamud.Game this.realDestroyHook.Enable(); } + /// + /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. + /// + /// Return type. + /// Function to call. + /// Task representing the pending or already completed function. + public Task RunOnFrameworkThread(Func func) => this.IsInFrameworkUpdateThread ? Task.FromResult(func()) : this.RunOnTick(func); + + /// + /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. + /// + /// Function to call. + /// Task representing the pending or already completed function. + public Task RunOnFrameworkThread(Action action) + { + if (this.IsInFrameworkUpdateThread) + { + try + { + action(); + return Task.CompletedTask; + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + else + { + return this.RunOnTick(action); + } + } + + /// + /// Run given function in upcoming Framework.Tick call. + /// + /// Return type. + /// Function to call. + /// Wait for given timespan before calling this function. + /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. + /// Cancellation token which will prevent the execution of this function if wait conditions are not met. + /// Task representing the pending function. + public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource(); + this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc() + { + RemainingTicks = delayTicks, + RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds), + CancellationToken = cancellationToken, + TaskCompletionSource = tcs, + Func = func, + }); + return tcs.Task; + } + + /// + /// Run given function in upcoming Framework.Tick call. + /// + /// Return type. + /// Function to call. + /// Wait for given timespan before calling this function. + /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. + /// Cancellation token which will prevent the execution of this function if wait conditions are not met. + /// Task representing the pending function. + public Task RunOnTick(Action action, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource(); + this.runOnNextTickTaskList.Add(new RunOnNextTickTaskAction() + { + RemainingTicks = delayTicks, + RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds), + CancellationToken = cancellationToken, + TaskCompletionSource = tcs, + Action = action, + }); + return tcs.Task; + } + /// /// Dispose of managed and unmanaged resources. /// @@ -179,6 +267,8 @@ namespace Dalamud.Game if (this.tierInitError) goto original; + this.frameworkUpdateThread ??= Thread.CurrentThread; + var dalamud = Service.Get(); // If this is the first time we are running this loop, we need to init Dalamud subsystems synchronously @@ -223,6 +313,8 @@ namespace Dalamud.Game try { + this.runOnNextTickTaskList.RemoveAll(x => x.Run()); + if (StatsEnabled && this.Update != null) { // Stat Tracking for Framework Updates @@ -312,5 +404,88 @@ namespace Dalamud.Game // Return the original trampoline location to cleanly exit return originalPtr; } + + private abstract class RunOnNextTickTaskBase + { + internal int RemainingTicks { get; set; } + + internal long RunAfterTickCount { get; init; } + + internal CancellationToken CancellationToken { get; init; } + + internal bool Run() + { + if (this.CancellationToken.IsCancellationRequested) + { + this.CancelImpl(); + return true; + } + + if (this.RemainingTicks > 0) + this.RemainingTicks -= 1; + if (this.RemainingTicks > 0) + return false; + + if (this.RunAfterTickCount > Environment.TickCount64) + return false; + + this.RunImpl(); + + return true; + } + + protected abstract void RunImpl(); + + protected abstract void CancelImpl(); + } + + private class RunOnNextTickTaskFunc : RunOnNextTickTaskBase + { + internal TaskCompletionSource TaskCompletionSource { get; init; } + + internal Func Func { get; init; } + + protected override void RunImpl() + { + try + { + this.TaskCompletionSource.SetResult(this.Func()); + } + catch (Exception ex) + { + this.TaskCompletionSource.SetException(ex); + } + } + + protected override void CancelImpl() + { + this.TaskCompletionSource.SetCanceled(); + } + } + + private class RunOnNextTickTaskAction : RunOnNextTickTaskBase + { + internal TaskCompletionSource TaskCompletionSource { get; init; } + + internal Action Action { get; init; } + + protected override void RunImpl() + { + try + { + this.Action(); + this.TaskCompletionSource.SetResult(); + } + catch (Exception ex) + { + this.TaskCompletionSource.SetException(ex); + } + } + + protected override void CancelImpl() + { + this.TaskCompletionSource.SetCanceled(); + } + } } } diff --git a/Dalamud/Interface/Internal/Windows/DataWindow.cs b/Dalamud/Interface/Internal/Windows/DataWindow.cs index 2783d00ce..98a5bb63f 100644 --- a/Dalamud/Interface/Internal/Windows/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/DataWindow.cs @@ -114,6 +114,9 @@ namespace Dalamud.Interface.Internal.Windows private DtrBarEntry? dtrTest2; private DtrBarEntry? dtrTest3; + // Task Scheduler + private CancellationTokenSource taskSchedCancelSource = new(); + private uint copyButtonIndex = 0; /// @@ -1359,6 +1362,15 @@ namespace Dalamud.Interface.Internal.Windows ImGuiHelpers.ScaledDummy(10); ImGui.SameLine(); + if (ImGui.Button("Cancel using CancellationTokenSource")) + { + this.taskSchedCancelSource.Cancel(); + this.taskSchedCancelSource = new(); + } + + ImGui.Text("Run in any thread: "); + ImGui.SameLine(); + if (ImGui.Button("Short Task.Run")) { Task.Run(() => { Thread.Sleep(500); }); @@ -1368,7 +1380,8 @@ namespace Dalamud.Interface.Internal.Windows if (ImGui.Button("Task in task(Delay)")) { - Task.Run(async () => await this.TestTaskInTaskDelay()); + var token = this.taskSchedCancelSource.Token; + Task.Run(async () => await this.TestTaskInTaskDelay(token)); } ImGui.SameLine(); @@ -1391,28 +1404,71 @@ namespace Dalamud.Interface.Internal.Windows }); } + ImGui.Text("Run in Framework.Update: "); + ImGui.SameLine(); + + if (ImGui.Button("ASAP")) + { + Task.Run(async () => await Service.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedCancelSource.Token)); + } + + ImGui.SameLine(); + + if (ImGui.Button("In 1s")) + { + Task.Run(async () => await Service.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedCancelSource.Token, delay: TimeSpan.FromSeconds(1))); + } + + ImGui.SameLine(); + + if (ImGui.Button("In 60f")) + { + Task.Run(async () => await Service.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedCancelSource.Token, delayTicks: 60)); + } + + ImGui.SameLine(); + + if (ImGui.Button("Error in 1s")) + { + Task.Run(async () => await Service.Get().RunOnTick(() => throw new Exception("Test Exception"), cancellationToken: this.taskSchedCancelSource.Token, delay: TimeSpan.FromSeconds(1))); + } + + ImGui.SameLine(); + + if (ImGui.Button("As long as it's in Framework Thread")) + { + Task.Run(async () => await Service.Get().RunOnFrameworkThread(() => { Log.Information("Task dispatched from non-framework.update thread"); })); + Service.Get().RunOnFrameworkThread(() => { Log.Information("Task dispatched from framework.update thread"); }).Wait(); + } + if (ImGui.Button("Drown in tasks")) { + var token = this.taskSchedCancelSource.Token; Task.Run(() => { for (var i = 0; i < 100; i++) { + token.ThrowIfCancellationRequested(); Task.Run(() => { for (var i = 0; i < 100; i++) { + token.ThrowIfCancellationRequested(); Task.Run(() => { for (var i = 0; i < 100; i++) { + token.ThrowIfCancellationRequested(); Task.Run(() => { for (var i = 0; i < 100; i++) { - Task.Run(() => + token.ThrowIfCancellationRequested(); + Task.Run(async () => { for (var i = 0; i < 100; i++) { + token.ThrowIfCancellationRequested(); Thread.Sleep(1); } }); @@ -1652,9 +1708,9 @@ namespace Dalamud.Interface.Internal.Windows } } - private async Task TestTaskInTaskDelay() + private async Task TestTaskInTaskDelay(CancellationToken token) { - await Task.Delay(5000); + await Task.Delay(5000, token); } #pragma warning disable 1998