using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Game.Gui; using Dalamud.Game.Gui.Toast; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; using CSFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework; namespace Dalamud.Game; /// /// This class represents the Framework of the native game client and grants access to various subsystems. /// [ServiceManager.EarlyLoadedService] internal sealed class Framework : IInternalDisposableService, IFramework { private static readonly ModuleLog Log = new("Framework"); private static readonly Stopwatch StatsStopwatch = new(); private readonly Stopwatch updateStopwatch = new(); private readonly HitchDetector hitchDetector; private readonly Hook updateHook; private readonly Hook destroyHook; [ServiceManager.ServiceDependency] private readonly GameLifecycle lifecycle = Service.Get(); [ServiceManager.ServiceDependency] private readonly DalamudConfiguration configuration = Service.Get(); private readonly CancellationTokenSource frameworkDestroy; private readonly ThreadBoundTaskScheduler frameworkThreadTaskScheduler; private readonly ConcurrentDictionary tickDelayedTaskCompletionSources = new(); private ulong tickCounter; [ServiceManager.ServiceConstructor] private unsafe Framework() { this.hitchDetector = new HitchDetector("FrameworkUpdate", this.configuration.FrameworkUpdateHitch); this.frameworkDestroy = new(); this.frameworkThreadTaskScheduler = new(); this.FrameworkThreadTaskFactory = new( this.frameworkDestroy.Token, TaskCreationOptions.None, TaskContinuationOptions.None, this.frameworkThreadTaskScheduler); this.updateHook = Hook.FromAddress((nint)CSFramework.StaticVirtualTablePointer->Tick, this.HandleFrameworkUpdate); this.destroyHook = Hook.FromAddress((nint)CSFramework.StaticVirtualTablePointer->Destroy, this.HandleFrameworkDestroy); this.updateHook.Enable(); this.destroyHook.Enable(); } /// public event IFramework.OnUpdateDelegate? Update; /// /// Executes during FrameworkUpdate before all delegates. /// internal event IFramework.OnUpdateDelegate? BeforeUpdate; /// /// Gets or sets a value indicating whether the collection of stats is enabled. /// public static bool StatsEnabled { get; set; } /// /// Gets the stats history mapping. /// public static Dictionary> StatsHistory { get; } = new(); /// public DateTime LastUpdate { get; private set; } = DateTime.MinValue; /// public DateTime LastUpdateUTC { get; private set; } = DateTime.MinValue; /// public TimeSpan UpdateDelta { get; private set; } = TimeSpan.Zero; /// public bool IsInFrameworkUpdateThread => this.frameworkThreadTaskScheduler.IsOnBoundThread; /// public bool IsFrameworkUnloading => this.frameworkDestroy.IsCancellationRequested; /// /// Gets the list of update sub-delegates that didn't get updated this frame. /// internal List NonUpdatedSubDelegates { get; private set; } = new(); /// /// Gets or sets a value indicating whether to dispatch update events. /// internal bool DispatchUpdateEvents { get; set; } = true; private TaskFactory FrameworkThreadTaskFactory { get; } /// public TaskFactory GetTaskFactory() => this.FrameworkThreadTaskFactory; /// public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default) { if (this.frameworkDestroy.IsCancellationRequested) return Task.FromCanceled(this.frameworkDestroy.Token); if (numTicks <= 0) return Task.CompletedTask; var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); this.tickDelayedTaskCompletionSources[tcs] = (this.tickCounter + (ulong)numTicks, cancellationToken); return tcs.Task; } /// public Task Run(Action action, CancellationToken cancellationToken = default) { if (cancellationToken == default) cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; return this.FrameworkThreadTaskFactory.StartNew(action, cancellationToken); } /// public Task Run(Func action, CancellationToken cancellationToken = default) { if (cancellationToken == default) cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; return this.FrameworkThreadTaskFactory.StartNew(action, cancellationToken); } /// public Task Run(Func action, CancellationToken cancellationToken = default) { if (cancellationToken == default) cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; return this.FrameworkThreadTaskFactory.StartNew(action, cancellationToken).Unwrap(); } /// public Task Run(Func> action, CancellationToken cancellationToken = default) { if (cancellationToken == default) cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; return this.FrameworkThreadTaskFactory.StartNew(action, cancellationToken).Unwrap(); } /// public Task RunOnFrameworkThread(Func func) => this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? Task.FromResult(func()) : this.RunOnTick(func); /// public Task RunOnFrameworkThread(Action action) { if (this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading) { try { action(); return Task.CompletedTask; } catch (Exception ex) { return Task.FromException(ex); } } else { return this.RunOnTick(action); } } /// public Task RunOnFrameworkThread(Func> func) => this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? func() : this.RunOnTick(func); /// public Task RunOnFrameworkThread(Func func) => this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? func() : this.RunOnTick(func); /// public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) { if (this.IsFrameworkUnloading) { if (delay == default && delayTicks == default) return this.RunOnFrameworkThread(func); var cts = new CancellationTokenSource(); cts.Cancel(); return Task.FromCanceled(cts.Token); } if (cancellationToken == default) cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; return this.FrameworkThreadTaskFactory.ContinueWhenAll( new[] { Task.Delay(delay, cancellationToken), this.DelayTicks(delayTicks, cancellationToken), }, _ => func(), cancellationToken, TaskContinuationOptions.HideScheduler, this.frameworkThreadTaskScheduler); } /// public Task RunOnTick(Action action, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) { if (this.IsFrameworkUnloading) { if (delay == default && delayTicks == default) return this.RunOnFrameworkThread(action); var cts = new CancellationTokenSource(); cts.Cancel(); return Task.FromCanceled(cts.Token); } if (cancellationToken == default) cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; return this.FrameworkThreadTaskFactory.ContinueWhenAll( new[] { Task.Delay(delay, cancellationToken), this.DelayTicks(delayTicks, cancellationToken), }, _ => action(), cancellationToken, TaskContinuationOptions.HideScheduler, this.frameworkThreadTaskScheduler); } /// public Task RunOnTick(Func> func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) { if (this.IsFrameworkUnloading) { if (delay == default && delayTicks == default) return this.RunOnFrameworkThread(func); var cts = new CancellationTokenSource(); cts.Cancel(); return Task.FromCanceled(cts.Token); } if (cancellationToken == default) cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; return this.FrameworkThreadTaskFactory.ContinueWhenAll( new[] { Task.Delay(delay, cancellationToken), this.DelayTicks(delayTicks, cancellationToken), }, _ => func(), cancellationToken, TaskContinuationOptions.HideScheduler, this.frameworkThreadTaskScheduler).Unwrap(); } /// public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) { if (this.IsFrameworkUnloading) { if (delay == default && delayTicks == default) return this.RunOnFrameworkThread(func); var cts = new CancellationTokenSource(); cts.Cancel(); return Task.FromCanceled(cts.Token); } if (cancellationToken == default) cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; return this.FrameworkThreadTaskFactory.ContinueWhenAll( new[] { Task.Delay(delay, cancellationToken), this.DelayTicks(delayTicks, cancellationToken), }, _ => func(), cancellationToken, TaskContinuationOptions.HideScheduler, this.frameworkThreadTaskScheduler).Unwrap(); } /// /// Dispose of managed and unmanaged resources. /// void IInternalDisposableService.DisposeService() { this.RunOnFrameworkThread(() => { // ReSharper disable once AccessToDisposedClosure this.updateHook.Disable(); // ReSharper disable once AccessToDisposedClosure this.destroyHook.Disable(); }).Wait(); this.updateHook.Dispose(); this.destroyHook.Dispose(); this.updateStopwatch.Reset(); StatsStopwatch.Reset(); } /// /// Adds a update time to the stats history. /// /// Delegate Name. /// Runtime. internal static void AddToStats(string key, double ms) { if (!StatsHistory.ContainsKey(key)) StatsHistory.Add(key, new List()); StatsHistory[key].Add(ms); if (StatsHistory[key].Count > 1000) { StatsHistory[key].RemoveRange(0, StatsHistory[key].Count - 1000); } } /// /// Profiles each sub-delegate in the eventDelegate and logs to StatsHistory. /// /// The Delegate to Profile. /// The Framework Instance to pass to delegate. internal void ProfileAndInvoke(IFramework.OnUpdateDelegate? eventDelegate, IFramework frameworkInstance) { // Individually invoke OnUpdate handlers and time them. foreach (var d in Delegate.EnumerateInvocationList(eventDelegate)) { var stopwatch = Stopwatch.StartNew(); try { d(frameworkInstance); } catch (Exception ex) { Log.Error(ex, "Exception while dispatching Framework::Update event."); } stopwatch.Stop(); var key = $"{d.Target}::{d.Method.Name}"; this.NonUpdatedSubDelegates.Remove(key); AddToStats(key, stopwatch.Elapsed.TotalMilliseconds); } } private unsafe bool HandleFrameworkUpdate(CSFramework* thisPtr) { this.frameworkThreadTaskScheduler.BoundThread ??= Thread.CurrentThread; ThreadSafety.MarkMainThread(); this.BeforeUpdate?.InvokeSafely(this); this.hitchDetector.Start(); try { var chatGui = Service.GetNullable(); var toastGui = Service.GetNullable(); var config = Service.GetNullable(); if (chatGui == null || toastGui == null) goto original; chatGui.UpdateQueue(); toastGui.UpdateQueue(); config?.Update(); } catch (Exception ex) { Log.Error(ex, "Exception while handling Framework::Update hook."); } if (this.DispatchUpdateEvents) { this.updateStopwatch.Stop(); this.UpdateDelta = TimeSpan.FromMilliseconds(this.updateStopwatch.ElapsedMilliseconds); this.updateStopwatch.Restart(); this.LastUpdate = DateTime.Now; this.LastUpdateUTC = DateTime.UtcNow; this.tickCounter++; foreach (var (k, (expiry, ct)) in this.tickDelayedTaskCompletionSources) { if (ct.IsCancellationRequested) k.SetCanceled(ct); else if (expiry <= this.tickCounter) k.SetResult(); else continue; this.tickDelayedTaskCompletionSources.Remove(k, out _); } if (StatsEnabled) { StatsStopwatch.Restart(); this.frameworkThreadTaskScheduler.Run(); StatsStopwatch.Stop(); AddToStats(nameof(this.frameworkThreadTaskScheduler), StatsStopwatch.Elapsed.TotalMilliseconds); } else { this.frameworkThreadTaskScheduler.Run(); } if (StatsEnabled && this.Update != null) { // Stat Tracking for Framework Updates this.NonUpdatedSubDelegates = StatsHistory.Keys.ToList(); this.ProfileAndInvoke(this.Update, this); // Cleanup handlers that are no longer being called foreach (var key in this.NonUpdatedSubDelegates) { if (key == nameof(this.FrameworkThreadTaskFactory)) continue; if (StatsHistory[key].Count > 0) { StatsHistory[key].RemoveAt(0); } else { StatsHistory.Remove(key); } } } else { this.Update?.InvokeSafely(this); } } this.hitchDetector.Stop(); original: return this.updateHook.OriginalDisposeSafe(thisPtr); } private unsafe bool HandleFrameworkDestroy(CSFramework* thisPtr) { this.frameworkDestroy.Cancel(); this.DispatchUpdateEvents = false; foreach (var k in this.tickDelayedTaskCompletionSources.Keys) k.SetCanceled(this.frameworkDestroy.Token); this.tickDelayedTaskCompletionSources.Clear(); // All the same, for now... this.lifecycle.SetShuttingDown(); this.lifecycle.SetUnloading(); Log.Information("Framework::Destroy!"); Service.Get().Unload(); this.frameworkThreadTaskScheduler.Run(); ServiceManager.WaitForServiceUnload(); Log.Information("Framework::Destroy OK!"); return this.destroyHook.OriginalDisposeSafe(thisPtr); } } /// /// Plugin-scoped version of a Framework service. /// [PluginInterface] [ServiceManager.ScopedService] #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 internal class FrameworkPluginScoped : IInternalDisposableService, IFramework { private readonly PluginErrorHandler pluginErrorHandler; [ServiceManager.ServiceDependency] private readonly Framework frameworkService = Service.Get(); /// /// Initializes a new instance of the class. /// /// Error handler instance. internal FrameworkPluginScoped(PluginErrorHandler pluginErrorHandler) { this.pluginErrorHandler = pluginErrorHandler; this.frameworkService.Update += this.OnUpdateForward; } /// public event IFramework.OnUpdateDelegate? Update; /// public DateTime LastUpdate => this.frameworkService.LastUpdate; /// public DateTime LastUpdateUTC => this.frameworkService.LastUpdateUTC; /// public TimeSpan UpdateDelta => this.frameworkService.UpdateDelta; /// public bool IsInFrameworkUpdateThread => this.frameworkService.IsInFrameworkUpdateThread; /// public bool IsFrameworkUnloading => this.frameworkService.IsFrameworkUnloading; /// void IInternalDisposableService.DisposeService() { this.frameworkService.Update -= this.OnUpdateForward; this.Update = null; } /// public TaskFactory GetTaskFactory() => this.frameworkService.GetTaskFactory(); /// public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default) => this.frameworkService.DelayTicks(numTicks, cancellationToken); /// public Task Run(Action action, CancellationToken cancellationToken = default) => this.frameworkService.Run(action, cancellationToken); /// public Task Run(Func action, CancellationToken cancellationToken = default) => this.frameworkService.Run(action, cancellationToken); /// public Task Run(Func action, CancellationToken cancellationToken = default) => this.frameworkService.Run(action, cancellationToken); /// public Task Run(Func> action, CancellationToken cancellationToken = default) => this.frameworkService.Run(action, cancellationToken); /// public Task RunOnFrameworkThread(Func func) => this.frameworkService.RunOnFrameworkThread(func); /// public Task RunOnFrameworkThread(Action action) => this.frameworkService.RunOnFrameworkThread(action); /// public Task RunOnFrameworkThread(Func> func) => this.frameworkService.RunOnFrameworkThread(func); /// public Task RunOnFrameworkThread(Func func) => this.frameworkService.RunOnFrameworkThread(func); /// public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) => this.frameworkService.RunOnTick(func, delay, delayTicks, cancellationToken); /// public Task RunOnTick(Action action, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) => this.frameworkService.RunOnTick(action, delay, delayTicks, cancellationToken); /// public Task RunOnTick(Func> func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) => this.frameworkService.RunOnTick(func, delay, delayTicks, cancellationToken); /// public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) => this.frameworkService.RunOnTick(func, delay, delayTicks, cancellationToken); private void OnUpdateForward(IFramework framework) { if (Framework.StatsEnabled && this.Update != null) { this.frameworkService.ProfileAndInvoke(this.Update, framework); } else { this.pluginErrorHandler.InvokeAndCatch(this.Update, $"{nameof(IFramework)}::{nameof(IFramework.Update)}", framework); } } }