Merge branch 'net5' of ssh://github.com/goatcorp/Dalamud into net5

This commit is contained in:
goaaats 2022-06-25 19:35:05 +02:00
commit 29dee596c4
No known key found for this signature in database
GPG key ID: 49E2AA8C6A76498B
19 changed files with 402 additions and 246 deletions

View file

@ -1,7 +1,5 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
@ -9,24 +7,15 @@ using Dalamud.Configuration.Internal;
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.Command;
using Dalamud.Game.Gui;
using Dalamud.Game.Gui.Internal;
using Dalamud.Game.Internal;
using Dalamud.Game.Network;
using Dalamud.Game.Network.Internal;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking.Internal;
using Dalamud.Interface;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Ipc.Internal;
using Dalamud.Support;
using Dalamud.Utility;
using Dalamud.Utility.Timing;
using Serilog;
using Serilog.Core;
using Serilog.Events;
@ -74,9 +63,7 @@ namespace Dalamud
SerilogEventSink.Instance.LogLine += SerilogOnLogLine;
Service<Dalamud>.Provide(this);
Service<DalamudStartInfo>.Provide(info);
Service<DalamudConfiguration>.Provide(configuration);
ServiceManager.InitializeProvidedServicesAndClientStructs(this, info, configuration);
if (!configuration.IsResumeGameAfterPluginLoad)
{

View file

@ -26,9 +26,9 @@ namespace Dalamud.Game.Gui.Dtr
private uint runningNodeIds = BaseNodeId;
[ServiceManager.ServiceConstructor]
private DtrBar(DalamudConfiguration configuration)
private DtrBar(DalamudConfiguration configuration, Framework framework)
{
Service<Framework>.Get().Update += this.Update;
framework.Update += this.Update;
configuration.DtrOrder ??= new List<string>();
configuration.DtrIgnore ??= new List<string>();

View file

@ -423,13 +423,13 @@ namespace Dalamud.Game.Gui
/// </summary>
public void Enable()
{
Service<ChatGui>.Get().Enable();
Service<ToastGui>.Get().Enable();
Service<FlyTextGui>.Get().Enable();
Service<PartyFinderGui>.Get().Enable();
Service<ChatGui>.GetAsync().ContinueWith(x => x.Result.Enable());
Service<ToastGui>.GetAsync().ContinueWith(x => x.Result.Enable());
Service<FlyTextGui>.GetAsync().ContinueWith(x => x.Result.Enable());
Service<PartyFinderGui>.GetAsync().ContinueWith(x => x.Result.Enable());
if (EnvironmentConfiguration.DalamudDoContextMenu)
Service<ContextMenu>.Get().Enable();
Service<ContextMenu>.GetAsync().ContinueWith(x => x.Result.Enable());
this.setGlobalBgmHook.Enable();
this.handleItemHoverHook.Enable();

View file

@ -9,7 +9,6 @@ using System.Runtime.InteropServices;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Utility.Timing;
using Newtonsoft.Json;
using Serilog;
@ -20,7 +19,6 @@ namespace Dalamud.Game
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
public class SigScanner : IDisposable
{
private readonly FileInfo? cacheFile;
@ -30,38 +28,6 @@ namespace Dalamud.Game
private ConcurrentDictionary<string, long>? textCache;
[ServiceManager.ServiceConstructor]
private SigScanner(DalamudStartInfo startInfo)
{
// Initialize the process information.
var cacheDir = new DirectoryInfo(Path.Combine(startInfo.WorkingDirectory!, "cachedSigs"));
if (!cacheDir.Exists)
cacheDir.Create();
this.cacheFile = new FileInfo(Path.Combine(cacheDir.FullName, $"{startInfo.GameVersion}.json"));
this.Module = Process.GetCurrentProcess().MainModule!;
this.Is32BitProcess = !Environment.Is64BitProcess;
this.IsCopy = true;
// Limit the search space to .text section.
this.SetupSearchSpace(this.Module);
if (this.IsCopy)
this.SetupCopiedSegments();
Log.Verbose($"Module base: 0x{this.TextSectionBase.ToInt64():X}");
Log.Verbose($"Module size: 0x{this.TextSectionSize:X}");
if (this.cacheFile != null)
this.Load();
// Initialize FFXIVClientStructs function resolver
using (Timings.Start("CS Resolver Init"))
{
FFXIVClientStructs.Resolver.InitializeParallel(new FileInfo(Path.Combine(cacheDir.FullName, $"{startInfo.GameVersion}_cs.json")));
}
}
/// <summary>
/// Initializes a new instance of the <see cref="SigScanner"/> class using the main module of the current process.
/// </summary>

View file

@ -57,7 +57,7 @@ namespace Dalamud.Interface.GameFonts
.Select(x => x.Glyphs.Select(y => y.TextureFileIndex).Max())
.Max())
.Select(x => dataManager.GetFile<TexFile>($"common/font/font{x}.tex")!)
.Select(x => new Task<byte[]>(() => x.ImageData!))
.Select(x => new Task<byte[]>(Timings.AttachTimingHandle(() => x.ImageData!)))
.ToArray();
foreach (var task in texTasks)
task.Start();

View file

@ -166,7 +166,7 @@ namespace Dalamud.Interface.Internal
try
{
Service<PluginManager>.Get().ReloadAllPlugins();
Service<PluginManager>.Get().ReloadAllPluginsAsync().Wait();
chatGui.Print("OK");
}
catch (Exception ex)

View file

@ -35,7 +35,7 @@ namespace Dalamud.Interface.Internal
/// <summary>
/// This plugin implements all of the Dalamud interface separately, to allow for reloading of the interface and rapid prototyping.
/// </summary>
[ServiceManager.EarlyLoadedService]
[ServiceManager.AfterDrawingEarlyLoadedService]
internal class DalamudInterface : IDisposable
{
private static readonly ModuleLog Log = new("DUI");
@ -714,7 +714,7 @@ namespace Dalamud.Interface.Internal
{
try
{
pluginManager.ReloadAllPlugins();
pluginManager.ReloadAllPluginsAsync().Wait();
}
catch (Exception ex)
{

View file

@ -60,8 +60,6 @@ namespace Dalamud.Interface.Internal.Windows
var dalamud = Service<Dalamud>.Get();
var interfaceManager = Service<InterfaceManager>.Get();
interfaceManager.SceneInitializeTask.Wait();
this.DefaultIcon = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "defaultIcon.png"))!;
this.TroubleIcon = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "troubleIcon.png"))!;
this.UpdateIcon = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "updateIcon.png"))!;

View file

@ -1735,7 +1735,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
if (!enableTask.Result)
return;
var loadTask = Task.Run(() => plugin.Load(PluginLoadReason.Installer))
var loadTask = Task.Run(() => plugin.LoadAsync(PluginLoadReason.Installer))
.ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_LoadFail(plugin.Name));
loadTask.Wait();

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Interface.Colors;
@ -13,25 +14,35 @@ public class ProfilerWindow : Window
{
private double min;
private double max;
private List<List<Tuple<double, double>>> occupied = new();
public ProfilerWindow() : base("Profiler", forceMainWindow: true) { }
public override void OnOpen()
{
this.min = Timings.AllTimings.Min(x => x.StartTime);
this.max = Timings.AllTimings.Max(x => x.EndTime);
this.min = Timings.AllTimings.Keys.Min(x => x.StartTime);
this.max = Timings.AllTimings.Keys.Max(x => x.EndTime);
}
private class RectInfo
{
internal TimingHandle Timing;
internal Vector2 MinPos;
internal Vector2 MaxPos;
internal Vector4 RectColor;
internal bool Hover;
}
/// <inheritdoc/>
public override void Draw()
{
var width = ImGui.GetWindowWidth();
var actualMin = Timings.AllTimings.Min(x => x.StartTime);
var actualMax = Timings.AllTimings.Max(x => x.EndTime);
var actualMin = Timings.AllTimings.Keys.Min(x => x.StartTime);
var actualMax = Timings.AllTimings.Keys.Max(x => x.EndTime);
ImGui.Text("Timings");
const int childHeight = 300;
var childHeight = Math.Max(300, 20 * (2 + this.occupied.Count));
if (ImGui.BeginChild("Timings", new Vector2(0, childHeight), true))
{
@ -68,48 +79,113 @@ public class ProfilerWindow : Window
uint maxRectDept = 0;
foreach (var timingHandle in Timings.AllTimings)
foreach (var l in this.occupied)
l.Clear();
var parentDepthDict = new Dictionary<long, int>();
var rects = new Dictionary<long, RectInfo>();
var mousePos = ImGui.GetMousePos();
foreach (var timingHandle in Timings.AllTimings.Keys)
{
var startX = (timingHandle.StartTime - this.min) / (this.max - this.min) * width;
var endX = (timingHandle.EndTime - this.min) / (this.max - this.min) * width;
var depth = timingHandle.IdChain.Length < 2 ? 0 : parentDepthDict.GetValueOrDefault(timingHandle.IdChain[^2]);
for (; depth < this.occupied.Count; depth++)
{
var acceptable = true;
foreach (var (x1, x2) in this.occupied[depth])
{
if (x2 <= timingHandle.StartTime || x1 >= timingHandle.EndTime)
continue;
acceptable = false;
break;
}
if (acceptable)
break;
}
if (depth == this.occupied.Count)
this.occupied.Add(new());
this.occupied[depth].Add(Tuple.Create(timingHandle.StartTime, timingHandle.EndTime));
parentDepthDict[timingHandle.Id] = depth;
startX = Math.Max(startX, 0);
endX = Math.Max(endX, 0);
var rectColor = timingHandle.IsMainThread ? ImGuiColors.ParsedBlue : ImGuiColors.ParsedPurple;
rectColor.X -= timingHandle.Depth * 0.12f;
rectColor.Y -= timingHandle.Depth * 0.12f;
rectColor.Z -= timingHandle.Depth * 0.12f;
Vector4 rectColor;
if (this.occupied[depth].Count % 2 == 0)
rectColor = timingHandle.IsMainThread ? ImGuiColors.ParsedBlue : ImGuiColors.ParsedOrange;
else
rectColor = timingHandle.IsMainThread ? ImGuiColors.ParsedPurple : ImGuiColors.ParsedGold;
rectColor.X -= timingHandle.IdChain.Length * 0.12f;
rectColor.Y -= timingHandle.IdChain.Length * 0.12f;
rectColor.Z -= timingHandle.IdChain.Length * 0.12f;
if (maxRectDept < timingHandle.Depth)
maxRectDept = timingHandle.Depth;
if (maxRectDept < depth)
maxRectDept = (uint)depth;
if (startX == endX)
{
continue;
}
var minPos = pos + new Vector2((uint)startX, 20 * timingHandle.Depth);
var maxPos = pos + new Vector2((uint)endX, 20 * (timingHandle.Depth + 1));
var minPos = pos + new Vector2((uint)startX, 20 * depth);
var maxPos = pos + new Vector2((uint)endX, 20 * (depth + 1));
rects[timingHandle.Id] = new RectInfo
{
Hover = mousePos.X >= minPos.X && mousePos.X < maxPos.X &&
mousePos.Y >= minPos.Y && mousePos.Y < maxPos.Y,
Timing = timingHandle,
MinPos = minPos,
MaxPos = maxPos,
RectColor = rectColor,
};
}
foreach (var hoveredItem in rects.Values.Where(x => x.Hover))
{
foreach (var rectInfo in rects.Values)
{
if (rectInfo == hoveredItem)
rectInfo.RectColor = new Vector4(255, 255, 255, 255);
else if (rectInfo.Timing.IdChain.Contains(hoveredItem.Timing.Id))
rectInfo.RectColor = ImGuiColors.ParsedGreen;
else if (hoveredItem.Timing.IdChain.Contains(rectInfo.Timing.Id))
rectInfo.RectColor = ImGuiColors.ParsedPink;
}
}
foreach (var rectInfo in rects.Values)
{
ImGui.GetWindowDrawList().AddRectFilled(
minPos,
maxPos,
ImGui.GetColorU32(rectColor));
rectInfo.MinPos,
rectInfo.MaxPos,
ImGui.GetColorU32(rectInfo.RectColor));
ImGui.GetWindowDrawList().AddTextClippedEx(minPos, maxPos, timingHandle.Name, null, Vector2.Zero, null);
if (rectInfo.Hover)
ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(0, 0, 0, 255));
ImGui.GetWindowDrawList().AddTextClippedEx(
rectInfo.MinPos,
rectInfo.MaxPos,
rectInfo.Timing.Name,
null,
Vector2.Zero,
null);
if (rectInfo.Hover)
ImGui.PopStyleColor();
// Show tooltip when hovered
var mousePos = ImGui.GetMousePos();
if (mousePos.X > pos.X + startX && mousePos.X < pos.X + endX &&
mousePos.Y > pos.Y + (20 * timingHandle.Depth) &&
mousePos.Y < pos.Y + (20 * (timingHandle.Depth + 1)))
if (rectInfo.Hover)
{
ImGui.BeginTooltip();
ImGui.Text(timingHandle.Name);
ImGui.Text(timingHandle.MemberName);
ImGui.Text($"{timingHandle.FileName}:{timingHandle.LineNumber}");
ImGui.Text($"Duration: {timingHandle.Duration}ms");
ImGui.TextUnformatted(rectInfo.Timing.Name);
ImGui.TextUnformatted(rectInfo.Timing.MemberName);
ImGui.TextUnformatted($"{rectInfo.Timing.FileName}:{rectInfo.Timing.LineNumber}");
ImGui.TextUnformatted($"Duration: {rectInfo.Timing.Duration}ms");
if (rectInfo.Timing.Parent != null)
ImGui.TextUnformatted($"Parent: {rectInfo.Timing.Parent.Name}");
ImGui.EndTooltip();
}
}

View file

@ -5,6 +5,7 @@ using System.Reflection;
using System.Runtime.Serialization;
using System.Threading.Tasks;
using Dalamud.Logging.Internal;
using Dalamud.Utility.Timing;
namespace Dalamud.IoC.Internal
{
@ -60,7 +61,7 @@ namespace Dalamud.IoC.Internal
var parameterType = p.ParameterType;
var requiredVersion = p.GetCustomAttribute(typeof(RequiredVersionAttribute)) as RequiredVersionAttribute;
return (parameterType, requiredVersion);
});
}).ToList();
var versionCheck = parameters.All(p => CheckInterfaceVersion(p.requiredVersion, p.parameterType));

View file

@ -314,13 +314,13 @@ internal partial class PluginManager : IDisposable
// Dev plugins should load first.
pluginDefs.InsertRange(0, devPluginDefs);
Task LoadPluginOnBoot(string logPrefix, PluginDef pluginDef)
async Task LoadPluginOnBoot(string logPrefix, PluginDef pluginDef)
{
using (Timings.Start($"{pluginDef.DllFile.Name}: {logPrefix}Boot"))
{
try
{
return this.LoadPluginAsync(
await this.LoadPluginAsync(
pluginDef.DllFile,
pluginDef.Manifest,
PluginLoadReason.Boot,
@ -336,8 +336,6 @@ internal partial class PluginManager : IDisposable
Log.Error(ex, "{0}: During boot plugin load, an unexpected error occurred", logPrefix);
}
}
return Task.CompletedTask;
}
void LoadPluginsSync(string logPrefix, IEnumerable<PluginDef> pluginDefsList)
@ -350,7 +348,8 @@ internal partial class PluginManager : IDisposable
{
return Task.WhenAll(
pluginDefsList
.Select(pluginDef => LoadPluginOnBoot(logPrefix, pluginDef))
.Select(pluginDef => Task.Run(Timings.AttachTimingHandle(
() => LoadPluginOnBoot(logPrefix, pluginDef))))
.ToArray());
}
@ -394,15 +393,15 @@ internal partial class PluginManager : IDisposable
TaskContinuationOptions.RunContinuationsAsynchronously)
.Unwrap()
.ContinueWith(
_ => Service<Framework>.Get().RunOnTick(() =>
{
LoadPluginsSync(
_ => Service<Framework>.Get().RunOnTick(
() => LoadPluginsSync(
"DrawAvailableSync",
syncPlugins.Where(def => def.Manifest?.LoadRequiredState is 0 or null));
return LoadPluginsAsync(
"DrawAvailableAsync",
asyncPlugins.Where(def => def.Manifest?.LoadRequiredState is 0 or null));
}))
syncPlugins.Where(def => def.Manifest?.LoadRequiredState is 0 or null))))
.Unwrap()
.ContinueWith(
_ => LoadPluginsAsync(
"DrawAvailableAsync",
asyncPlugins.Where(def => def.Manifest?.LoadRequiredState is 0 or null)))
.Unwrap());
// Save signatures when all plugins are done loading, successful or not.
@ -427,33 +426,16 @@ internal partial class PluginManager : IDisposable
/// <summary>
/// Reload all loaded plugins.
/// </summary>
public void ReloadAllPlugins()
/// <returns>A task.</returns>
public Task ReloadAllPluginsAsync()
{
var aggregate = new List<Exception>();
lock (this.pluginListLock)
{
foreach (var plugin in this.InstalledPlugins)
{
if (plugin.IsLoaded)
{
try
{
plugin.Reload();
}
catch (Exception ex)
{
Log.Error(ex, "Error during reload all");
aggregate.Add(ex);
}
}
}
}
if (aggregate.Any())
{
throw new AggregateException(aggregate);
return Task.WhenAll(this.InstalledPlugins
.Where(x => x.IsLoaded)
.ToList()
.Select(x => Task.Run(async () => await x.ReloadAsync()))
.ToList());
}
}
@ -706,10 +688,7 @@ internal partial class PluginManager : IDisposable
if (plugin.IsDisabled)
plugin.Enable();
// Await for things that plugin just require
_ = await Service<SigScanner>.GetAsync();
plugin.Load(reason);
await plugin.LoadAsync(reason);
}
catch (InvalidPluginException)
{
@ -1004,7 +983,7 @@ internal partial class PluginManager : IDisposable
Thread.Sleep(500);
// Let's indicate "installer" here since this is supposed to be a fresh install
plugin.Load(PluginLoadReason.Installer);
plugin.LoadAsync(PluginLoadReason.Installer).Wait();
}
/// <summary>

View file

@ -124,7 +124,7 @@ internal class LocalDevPlugin : LocalPlugin, IDisposable
var current = Interlocked.Increment(ref this.reloadCounter);
Task.Delay(500).ContinueWith(
_ =>
async _ =>
{
if (this.fileWatcherTokenSource.IsCancellationRequested)
{
@ -148,7 +148,7 @@ internal class LocalDevPlugin : LocalPlugin, IDisposable
try
{
this.Reload();
await this.ReloadAsync();
notificationManager.AddNotification($"The DevPlugin '{this.Name} was reloaded successfully.", "Plugin reloaded!", NotificationType.Success);
}
catch (Exception ex)

View file

@ -2,7 +2,7 @@ using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Dalamud.Configuration.Internal;
using Dalamud.Game;
using Dalamud.Game.Gui.Dtr;
@ -160,7 +160,7 @@ internal class LocalPlugin : IDisposable
public PluginState State { get; protected set; }
/// <summary>
/// Gets the AssemblyName plugin, populated during <see cref="Load(PluginLoadReason, bool)"/>.
/// Gets the AssemblyName plugin, populated during <see cref="LoadAsync"/>.
/// </summary>
/// <returns>Plugin type.</returns>
public AssemblyName? AssemblyName { get; private set; }
@ -225,7 +225,8 @@ internal class LocalPlugin : IDisposable
/// </summary>
/// <param name="reason">The reason why this plugin is being loaded.</param>
/// <param name="reloading">Load while reloading.</param>
public void Load(PluginLoadReason reason, bool reloading = false)
/// <returns>A task.</returns>
public async Task LoadAsync(PluginLoadReason reason, bool reloading = false)
{
var startInfo = Service<DalamudStartInfo>.Get();
var configuration = Service<DalamudConfiguration>.Get();
@ -334,7 +335,7 @@ internal class LocalPlugin : IDisposable
this.DalamudInterface = new DalamudPluginInterface(this.pluginAssembly.GetName().Name!, this.DllFile, reason, this.IsDev);
var ioc = Service<ServiceContainer>.Get();
this.instance = ioc.CreateAsync(this.pluginType, this.DalamudInterface).GetAwaiter().GetResult() as IDalamudPlugin;
this.instance = await ioc.CreateAsync(this.pluginType, this.DalamudInterface) as IDalamudPlugin;
if (this.instance == null)
{
this.State = PluginState.LoadError;
@ -423,7 +424,8 @@ internal class LocalPlugin : IDisposable
/// <summary>
/// Reload this plugin.
/// </summary>
public void Reload()
/// <returns>A task.</returns>
public async Task ReloadAsync()
{
this.Unload(true);
@ -431,7 +433,7 @@ internal class LocalPlugin : IDisposable
var dtr = Service<DtrBar>.Get();
dtr.HandleRemovedNodes();
this.Load(PluginLoadReason.Reload, true);
await this.LoadAsync(PluginLoadReason.Reload, true);
}
/// <summary>

View file

@ -1,10 +1,15 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Dalamud.Configuration.Internal;
using Dalamud.Game;
using Dalamud.Interface.Internal;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Utility.Timing;
using JetBrains.Annotations;
namespace Dalamud
@ -26,16 +31,43 @@ namespace Dalamud
/// </summary>
public static Task BlockingResolved { get; } = BlockingServicesLoadedTaskCompletionSource.Task;
/// <summary>
/// Initializes Provided Services and FFXIVClientStructs.
/// </summary>
/// <param name="dalamud">Instance of <see cref="Dalamud"/>.</param>
/// <param name="startInfo">Instance of <see cref="DalamudStartInfo"/>.</param>
/// <param name="configuration">Instance of <see cref="DalamudConfiguration"/>.</param>
public static void InitializeProvidedServicesAndClientStructs(Dalamud dalamud, DalamudStartInfo startInfo, DalamudConfiguration configuration)
{
Service<Dalamud>.Provide(dalamud);
Service<DalamudStartInfo>.Provide(startInfo);
Service<DalamudConfiguration>.Provide(configuration);
Service<ServiceContainer>.Provide(new ServiceContainer());
// Initialize the process information.
var cacheDir = new DirectoryInfo(Path.Combine(startInfo.WorkingDirectory!, "cachedSigs"));
if (!cacheDir.Exists)
cacheDir.Create();
Service<SigScanner>.Provide(new SigScanner(true, new FileInfo(Path.Combine(cacheDir.FullName, $"{startInfo.GameVersion}.json"))));
using (Timings.Start("CS Resolver Init"))
{
FFXIVClientStructs.Resolver.InitializeParallel(new FileInfo(Path.Combine(cacheDir.FullName, $"{startInfo.GameVersion}_cs.json")));
}
}
/// <summary>
/// Kicks off construction of services that can handle early loading.
/// </summary>
/// <returns>Task for initializing all services.</returns>
public static async Task InitializeEarlyLoadableServices()
{
Service<ServiceContainer>.Provide(new ServiceContainer());
using var serviceInitializeTimings = Timings.Start("Services Init");
var service = typeof(Service<>);
var blockingEarlyLoadingServices = new List<Task>();
var earlyLoadingServices = new HashSet<Type>();
var blockingEarlyLoadingServices = new HashSet<Type>();
var afterDrawingEarlyLoadedServices = new HashSet<Type>();
var dependencyServicesMap = new Dictionary<Type, List<Type>>();
var getAsyncTaskMap = new Dictionary<Type, Task>();
@ -52,10 +84,19 @@ namespace Dalamud
null,
null,
null);
if (attr.IsAssignableTo(typeof(BlockingEarlyLoadedService)))
{
getAsyncTaskMap[serviceType] = getTask;
blockingEarlyLoadingServices.Add(getTask);
blockingEarlyLoadingServices.Add(serviceType);
}
else if (attr.IsAssignableTo(typeof(AfterDrawingEarlyLoadedService)))
{
afterDrawingEarlyLoadedServices.Add(serviceType);
}
else
{
earlyLoadingServices.Add(serviceType);
}
dependencyServicesMap[serviceType] =
@ -69,50 +110,70 @@ namespace Dalamud
null);
}
_ = Task.WhenAll(blockingEarlyLoadingServices).ContinueWith(x =>
_ = Task.Run(async () =>
{
try
{
if (x.IsFaulted)
BlockingServicesLoadedTaskCompletionSource.SetException(x.Exception!);
else
BlockingServicesLoadedTaskCompletionSource.SetResult();
using var blockingServiceInitializeTimings = Timings.Start("BlockingServices Init");
await Task.WhenAll(blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x]));
BlockingServicesLoadedTaskCompletionSource.SetResult();
}
catch (Exception)
catch (Exception e)
{
// don't care, as this means task result/exception has already been set
BlockingServicesLoadedTaskCompletionSource.SetException(e);
}
}).ConfigureAwait(false);
try
{
var tasks = new List<Task>();
while (dependencyServicesMap.Any())
for (var i = 0; i < 2; i++)
{
tasks.Clear();
foreach (var (serviceType, dependencies) in dependencyServicesMap.ToList())
var tasks = new List<Task>();
var servicesToLoad = new HashSet<Type>();
if (i == 0)
{
if (!dependencies.All(
x => !getAsyncTaskMap.ContainsKey(x) || getAsyncTaskMap[x].IsCompleted))
continue;
tasks.Add((Task)service.MakeGenericType(serviceType).InvokeMember(
"StartLoader",
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
null,
null,
null));
dependencyServicesMap.Remove(serviceType);
servicesToLoad.UnionWith(earlyLoadingServices);
servicesToLoad.UnionWith(blockingEarlyLoadingServices);
}
else
{
servicesToLoad.UnionWith(afterDrawingEarlyLoadedServices);
await (await Service<InterfaceManager>.GetAsync()).SceneInitializeTask;
}
if (!tasks.Any())
throw new InvalidOperationException("Unresolvable dependency cycle detected");
await Task.WhenAll(tasks);
foreach (var task in tasks)
while (servicesToLoad.Any())
{
if (task.IsFaulted)
throw task.Exception!;
foreach (var serviceType in servicesToLoad)
{
if (dependencyServicesMap[serviceType].Any(
x => getAsyncTaskMap.GetValueOrDefault(x)?.IsCompleted == false))
continue;
tasks.Add((Task)service.MakeGenericType(serviceType).InvokeMember(
"StartLoader",
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
null,
null,
null));
servicesToLoad.Remove(serviceType);
}
if (!tasks.Any())
throw new InvalidOperationException("Unresolvable dependency cycle detected");
if (servicesToLoad.Any())
{
await Task.WhenAny(tasks);
var faultedTasks = tasks.Where(x => x.IsFaulted).Select(x => (Exception)x.Exception!).ToArray();
if (faultedTasks.Any())
throw new AggregateException(faultedTasks);
}
else
{
await Task.WhenAll(tasks);
}
tasks.RemoveAll(x => x.IsCompleted);
}
}
}
@ -173,5 +234,14 @@ namespace Dalamud
public class BlockingEarlyLoadedService : EarlyLoadedService
{
}
/// <summary>
/// Indicates that the class is a service, and will be instantiated automatically on startup,
/// when drawing becomes available.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class AfterDrawingEarlyLoadedService : EarlyLoadedService
{
}
}
}

View file

@ -23,9 +23,6 @@ namespace Dalamud
// ReSharper disable once StaticMemberInGenericType
private static readonly TaskCompletionSource<T> InstanceTcs = new();
// ReSharper disable once StaticMemberInGenericType
private static bool startLoaderInvoked = false;
static Service()
{
var exposeToPlugins = typeof(T).GetCustomAttribute<PluginInterfaceAttribute>() != null;
@ -45,37 +42,29 @@ namespace Dalamud
[UsedImplicitly]
public static Task<T> StartLoader()
{
if (startLoaderInvoked)
throw new InvalidOperationException("StartLoader has already been called.");
var attr = typeof(T).GetCustomAttribute<ServiceManager.Service>(true)?.GetType();
if (attr?.IsAssignableTo(typeof(ServiceManager.EarlyLoadedService)) != true)
throw new InvalidOperationException($"{typeof(T).Name} is not an EarlyLoadedService");
startLoaderInvoked = true;
return Task.Run(async () =>
return Task.Run(Timings.AttachTimingHandle(async () =>
{
using (Timings.Start($"{typeof(T).Namespace} Enable"))
ServiceManager.Log.Debug("Service<{0}>: Begin construction", typeof(T).Name);
try
{
var x = await ConstructObject();
if (attr?.IsAssignableTo(typeof(ServiceManager.BlockingEarlyLoadedService)) == true)
ServiceManager.Log.Debug("Service<{0}>: Begin construction", typeof(T).Name);
try
{
var x = await ConstructObject();
if (attr?.IsAssignableTo(typeof(ServiceManager.BlockingEarlyLoadedService)) == true)
ServiceManager.Log.Debug("Service<{0}>: Construction complete", typeof(T).Name);
InstanceTcs.SetResult(x);
return x;
}
catch (Exception e)
{
InstanceTcs.SetException(e);
if (attr?.IsAssignableTo(typeof(ServiceManager.BlockingEarlyLoadedService)) == true)
ServiceManager.Log.Error(e, "Service<{0}>: Construction failure", typeof(T).Name);
throw;
}
ServiceManager.Log.Debug("Service<{0}>: Construction complete", typeof(T).Name);
InstanceTcs.SetResult(x);
return x;
}
});
catch (Exception e)
{
InstanceTcs.SetException(e);
if (attr?.IsAssignableTo(typeof(ServiceManager.BlockingEarlyLoadedService)) == true)
ServiceManager.Log.Error(e, "Service<{0}>: Construction failure", typeof(T).Name);
throw;
}
}));
}
/// <summary>
@ -165,7 +154,10 @@ namespace Dalamud
var ctor = GetServiceConstructor();
var args = await Task.WhenAll(
ctor.GetParameters().Select(x => GetServiceObjectConstructArgument(x.ParameterType)));
return (T)ctor.Invoke(args)!;
using (Timings.Start($"{typeof(T).Name} Construct"))
{
return (T)ctor.Invoke(args)!;
}
}
}
}

View file

@ -1,18 +1,27 @@
using System.Threading;
namespace Dalamud.Utility.Timing;
public class TimingEvent
{
private static long IdCounter = 0;
/// <summary>
/// Id of this timing event.
/// </summary>
public readonly long Id = Interlocked.Increment(ref IdCounter);
internal TimingEvent(string name)
{
this.Name = name;
this.StartTime = Timings.Stopwatch.Elapsed.TotalMilliseconds;
}
/// <summary>
/// Gets the time this timing started.
/// </summary>
public double StartTime { get; private set; }
/// <summary>
/// Gets the name of the timing.
/// </summary>

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
@ -8,7 +9,7 @@ namespace Dalamud.Utility.Timing;
/// Class used for tracking a time interval taken.
/// </summary>
[DebuggerDisplay("{Name} - {Duration}")]
public sealed class TimingHandle : TimingEvent, IDisposable
public sealed class TimingHandle : TimingEvent, IDisposable, IComparable<TimingHandle>
{
/// <summary>
/// Initializes a new instance of the <see cref="TimingHandle"/> class.
@ -16,28 +17,33 @@ public sealed class TimingHandle : TimingEvent, IDisposable
/// <param name="name">The name of this timing.</param>
internal TimingHandle(string name) : base(name)
{
this.Parent = Timings.Current.Value;
Timings.Current.Value = this;
this.Stack = Timings.TaskTimingHandles;
lock (Timings.AllTimings)
this.Parent = this.Stack.LastOrDefault();
if (this.Parent != null)
{
if (this.Parent != null)
{
this.ChildCount++;
}
this.EndTime = this.StartTime;
this.IsMainThread = ThreadSafety.IsMainThread;
if (Timings.ActiveTimings.Count > 0)
{
this.Depth = Timings.ActiveTimings.Max(x => x.Depth) + 1;
}
Timings.ActiveTimings.Add(this);
this.Parent.ChildCount++;
this.IdChain = new long[this.Parent.IdChain.Length + 1];
Array.Copy(this.Parent.IdChain, this.IdChain, this.Parent.IdChain.Length);
}
else
{
this.IdChain = new long[1];
}
this.IdChain[^1] = this.Id;
this.EndTime = this.StartTime;
this.IsMainThread = ThreadSafety.IsMainThread;
this.Stack.Add(this);
}
/// <summary>
/// Gets the id chain.
/// </summary>
public long[] IdChain { get; init; }
/// <summary>
/// Gets the time this timing ended.
/// </summary>
@ -48,16 +54,16 @@ public sealed class TimingHandle : TimingEvent, IDisposable
/// </summary>
public double Duration => Math.Floor(this.EndTime - this.StartTime);
/// <summary>
/// Gets the attached timing handle stack.
/// </summary>
public List<TimingHandle> Stack { get; private set; }
/// <summary>
/// Gets the parent timing.
/// </summary>
public TimingHandle? Parent { get; private set; }
/// <summary>
/// Gets a value indicating whether or not this timing has already returned to its parent.
/// </summary>
public bool Returned { get; private set; }
/// <summary>
/// Gets a value indicating whether or not this timing was started on the main thread.
/// </summary>
@ -68,26 +74,39 @@ public sealed class TimingHandle : TimingEvent, IDisposable
/// </summary>
public uint ChildCount { get; private set; }
/// <summary>
/// Gets the depth of this timing.
/// </summary>
public uint Depth { get; private set; }
/// <inheritdoc/>
public void Dispose()
{
this.EndTime = Timings.Stopwatch.Elapsed.TotalMilliseconds;
Timings.Current.Value = this.Parent;
lock (Timings.AllTimings)
this.Stack.Remove(this);
if (this.Duration > 1 || this.ChildCount > 0)
{
if (this.Duration > 1 || this.ChildCount > 0)
lock (Timings.AllTimings)
{
Timings.AllTimings.Add(this);
this.Returned = this.Parent != null && Timings.ActiveTimings.Contains(this.Parent);
Timings.AllTimings.Add(this, this);
}
Timings.ActiveTimings.Remove(this);
}
}
/// <inheritdoc/>
public int CompareTo(TimingHandle? other)
{
if (other == null)
return -1;
var i = 0;
for (; i < this.IdChain.Length && i < other.IdChain.Length; i++)
{
if (this.IdChain[i] < other.IdChain[i])
return -1;
if (this.IdChain[i] > other.IdChain[i])
return 1;
}
if (this.IdChain.Length < other.IdChain.Length)
return -1;
if (this.IdChain.Length > other.IdChain.Length)
return 1;
return 0;
}
}

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace Dalamud.Utility.Timing;
@ -19,19 +20,75 @@ public static class Timings
/// <summary>
/// All concluded timings.
/// </summary>
internal static readonly List<TimingHandle> AllTimings = new();
internal static readonly SortedList<TimingHandle, TimingHandle> AllTimings = new();
/// <summary>
/// All active timings.
/// </summary>
internal static readonly List<TimingHandle> ActiveTimings = new();
internal static readonly List<TimingEvent> Events = new();
private static readonly AsyncLocal<Tuple<int?, List<TimingHandle>>> taskTimingHandleStorage = new();
/// <summary>
/// Current active timing entry.
/// Gets or sets all active timings of current thread.
/// </summary>
internal static readonly AsyncLocal<TimingHandle> Current = new();
internal static List<TimingHandle> TaskTimingHandles
{
get
{
if (taskTimingHandleStorage.Value == null || taskTimingHandleStorage.Value.Item1 != Task.CurrentId)
taskTimingHandleStorage.Value = Tuple.Create<int?, List<TimingHandle>>(Task.CurrentId, new());
return taskTimingHandleStorage.Value!.Item2!;
}
set => taskTimingHandleStorage.Value = Tuple.Create(Task.CurrentId, value);
}
/// <summary>
/// Attaches timing handle to a Func{T}.
/// </summary>
/// <param name="task">Task to attach.</param>
/// <typeparam name="T">Return type.</typeparam>
/// <returns>Attached task.</returns>
public static Func<T> AttachTimingHandle<T>(Func<T> task)
{
var outerTimingHandle = TaskTimingHandles;
return () =>
{
T res = default(T);
var prev = TaskTimingHandles;
TaskTimingHandles = outerTimingHandle;
try
{
res = task();
}
finally
{
TaskTimingHandles = prev;
}
return res;
};
}
/// <summary>
/// Attaches timing handle to an Action.
/// </summary>
/// <param name="task">Task to attach.</param>
/// <returns>Attached task.</returns>
public static Action AttachTimingHandle(Action task)
{
var outerTimingHandle = TaskTimingHandles;
return () =>
{
var prev = TaskTimingHandles;
TaskTimingHandles = outerTimingHandle;
try
{
task();
}
finally
{
TaskTimingHandles = prev;
}
};
}
/// <summary>
/// Start a new timing.
@ -50,7 +107,7 @@ public static class Timings
LineNumber = sourceLineNumber,
};
}
/// <summary>
/// Record a one-time event.
/// </summary>