diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index dbe95ea23f..6a102d7789 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Threading.Tasks; using Dalamud.Game.Text; using Dalamud.Interface.Internal.Windows.PluginInstaller; @@ -39,6 +40,8 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable [JsonIgnore] private bool isSaveQueued; + private Task? writeTask; + /// /// Delegate for the event that occurs when the dalamud configuration is saved. /// @@ -488,6 +491,9 @@ public void Dispose() { // Make sure that we save, if a save is queued while we are shutting down this.Update(); + + // Wait for the write to finish + this.writeTask?.Wait(); } /// @@ -508,7 +514,8 @@ private void Save() { ThreadSafety.AssertMainThread(); - Service.Get().WriteAllText( + this.writeTask?.Wait(); + this.writeTask = Service.Get().WriteAllTextAsync( this.configPath, JsonConvert.SerializeObject(this, SerializerSettings)); this.DalamudConfigurationSaved?.Invoke(this); } diff --git a/Dalamud/Configuration/PluginConfigurations.cs b/Dalamud/Configuration/PluginConfigurations.cs index de5e071c11..a7509dee6c 100644 --- a/Dalamud/Configuration/PluginConfigurations.cs +++ b/Dalamud/Configuration/PluginConfigurations.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Threading.Tasks; using Dalamud.Storage; using Newtonsoft.Json; @@ -11,6 +12,7 @@ namespace Dalamud.Configuration; public sealed class PluginConfigurations { private readonly DirectoryInfo configDirectory; + private Task? currentWriteTask; /// /// Initializes a new instance of the class. @@ -32,10 +34,32 @@ public PluginConfigurations(string storageFolder) /// Plugin configuration. /// Plugin name. /// WorkingPluginId of the plugin. + [Obsolete("Use SaveAsync instead.")] public void Save(IPluginConfiguration config, string pluginName, Guid workingPluginId) { - Service.Get() - .WriteAllText(this.GetConfigFile(pluginName).FullName, SerializeConfig(config), workingPluginId); + // TODO(api10): This API should be async-only. Remove the sync version! + // Yes, this means that all plugins will be blocking each other when writing configs for now, but that's fine until + // we can make this API async + this.currentWriteTask?.Wait(); + this.currentWriteTask = Service.Get() + .WriteAllTextAsync(this.GetConfigFile(pluginName).FullName, SerializeConfig(config), workingPluginId); + } + + /// + /// Save/Load plugin configuration. + /// NOTE: Save/Load are still using Type information for now, + /// despite LoadForType superseding Load and not requiring or using it. + /// It might be worth removing the Type info from Save, to strip it from all future saved configs, + /// and then Load() can probably be removed entirely. + /// + /// Plugin configuration. + /// Plugin name. + /// WorkingPluginId of the plugin. + /// A representing the asynchronous operation. + public async Task SaveAsync(IPluginConfiguration config, string pluginName, Guid workingPluginId) + { + await Service.Get() + .WriteAllTextAsync(this.GetConfigFile(pluginName).FullName, SerializeConfig(config), workingPluginId); } /// diff --git a/Dalamud/Game/Text/SeStringHandling/SeString.cs b/Dalamud/Game/Text/SeStringHandling/SeString.cs index 8cce5c2864..47c38b227f 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeString.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeString.cs @@ -368,17 +368,6 @@ public static SeString CreateMapLinkWithInstance(uint territoryId, uint mapId, i return null; } - private static string GetMapLinkNameString(string placeName, int? instance, string coordinateString) - { - var instanceString = string.Empty; - if (instance is > 0 and < 10) - { - instanceString = (SeIconChar.Instance1 + instance.Value - 1).ToIconString(); - } - - return $"{placeName}{instanceString} {coordinateString}"; - } - /// /// Creates an SeString representing an entire payload chain that can be used to link party finder listings in the chat log. /// @@ -512,4 +501,15 @@ public override string ToString() { return this.TextValue; } + + private static string GetMapLinkNameString(string placeName, int? instance, string coordinateString) + { + var instanceString = string.Empty; + if (instance is > 0 and < 10) + { + instanceString = (SeIconChar.Instance1 + instance.Value - 1).ToIconString(); + } + + return $"{placeName}{instanceString} {coordinateString}"; + } } diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index a6c4e243c0..816352d808 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -249,7 +249,6 @@ public void OpenPluginStats() /// /// Opens the on the plugin installed. /// - /// The page of the installer to open. public void OpenPluginInstaller() { this.pluginWindow.OpenTo(this.configuration.PluginInstallerOpen); @@ -394,6 +393,7 @@ public void ToggleDataWindow(string dataKind = null) /// /// Toggles the . /// + /// The page of the installer to open. public void TogglePluginInstallerWindowTo(PluginInstallerWindow.PluginInstallerOpenKind kind) => this.pluginWindow.ToggleTo(kind); /// diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs index ba47d2c8e4..5b03ecf7e7 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -50,6 +50,7 @@ internal class DataWindow : Window new DataShareWidget(), new NetworkMonitorWidget(), new IconBrowserWidget(), + new VfsWidget(), }; private readonly IOrderedEnumerable orderedModules; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/VfsWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/VfsWidget.cs new file mode 100644 index 0000000000..dc01cc8473 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/VfsWidget.cs @@ -0,0 +1,102 @@ +using System.Diagnostics; +using System.IO; + +using Dalamud.Configuration.Internal; +using Dalamud.Storage; +using ImGuiNET; +using Serilog; + +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; + +/// +/// Widget for displaying configuration info. +/// +internal class VfsWidget : IDataWindowWidget +{ + private int numBytes = 1024; + private int reps = 1; + + /// + public string[]? CommandShortcuts { get; init; } = { "vfs" }; + + /// + public string DisplayName { get; init; } = "VFS"; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + var service = Service.Get(); + var dalamud = Service.Get(); + + ImGui.InputInt("Num bytes", ref this.numBytes); + ImGui.InputInt("Reps", ref this.reps); + + var path = Path.Combine(dalamud.StartInfo.WorkingDirectory!, "test.bin"); + + if (ImGui.Button("Write")) + { + Log.Information("=== WRITING ==="); + var data = new byte[this.numBytes]; + var stopwatch = new Stopwatch(); + var acc = 0L; + + for (var i = 0; i < this.reps; i++) + { + stopwatch.Restart(); + service.WriteAllBytesAsync(path, data).GetAwaiter().GetResult(); + stopwatch.Stop(); + acc += stopwatch.ElapsedMilliseconds; + Log.Information("Turn {Turn} took {Ms}ms", i, stopwatch.ElapsedMilliseconds); + } + + Log.Information("Took {Ms}ms in total", acc); + } + + if (ImGui.Button("Read")) + { + Log.Information("=== READING ==="); + var stopwatch = new Stopwatch(); + var acc = 0L; + + for (var i = 0; i < this.reps; i++) + { + stopwatch.Restart(); + service.ReadAllBytes(path); + stopwatch.Stop(); + acc += stopwatch.ElapsedMilliseconds; + Log.Information("Turn {Turn} took {Ms}ms", i, stopwatch.ElapsedMilliseconds); + } + + Log.Information("Took {Ms}ms in total", acc); + } + + if (ImGui.Button("Test Config")) + { + var config = Service.Get(); + + Log.Information("=== READING ==="); + var stopwatch = new Stopwatch(); + var acc = 0L; + + for (var i = 0; i < this.reps; i++) + { + stopwatch.Restart(); + config.ForceSave(); + stopwatch.Stop(); + acc += stopwatch.ElapsedMilliseconds; + Log.Information("Turn {Turn} took {Ms}ms", i, stopwatch.ElapsedMilliseconds); + } + + Log.Information("Took {Ms}ms in total", acc); + } + } +} diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 82f19aa492..b098717bd7 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -6,6 +5,7 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Threading.Tasks; using Dalamud.Configuration; using Dalamud.Configuration.Internal; @@ -338,12 +338,30 @@ public ICallGateSubscriber GetIpcSubscribe /// Save a plugin configuration(inheriting IPluginConfiguration). /// /// The current configuration. + [Obsolete("Prefer SavePluginConfigAsync() to avoid blocking the main thread.")] public void SavePluginConfig(IPluginConfiguration? currentConfig) { if (currentConfig == null) return; +#pragma warning disable CS0618 // Type or member is obsolete this.configs.Save(currentConfig, this.plugin.InternalName, this.plugin.Manifest.WorkingPluginId); +#pragma warning restore CS0618 // Type or member is obsolete + } + + /// + /// Save a plugin configuration(inheriting IPluginConfiguration). + /// + /// The current configuration. + /// A representing the asynchronous operation. + public async Task SavePluginConfigAsync(IPluginConfiguration? currentConfig) + { + if (currentConfig == null) + return; + +#pragma warning disable CS0618 // Type or member is obsolete + await this.configs.SaveAsync(currentConfig, this.plugin.InternalName, this.plugin.Manifest.WorkingPluginId); +#pragma warning restore CS0618 // Type or member is obsolete } /// diff --git a/Dalamud/Storage/ReliableFileStorage.cs b/Dalamud/Storage/ReliableFileStorage.cs index 9feb17c0d5..38ec7b2f01 100644 --- a/Dalamud/Storage/ReliableFileStorage.cs +++ b/Dalamud/Storage/ReliableFileStorage.cs @@ -1,5 +1,6 @@ using System.IO; using System.Text; +using System.Threading.Tasks; using Dalamud.Logging.Internal; using Dalamud.Utility; @@ -91,8 +92,9 @@ public bool Exists(string path, Guid containerId = default) /// Path to write to. /// The contents of the file. /// Container to write to. - public void WriteAllText(string path, string? contents, Guid containerId = default) - => this.WriteAllText(path, contents, Encoding.UTF8, containerId); + /// A representing the asynchronous operation. + public async Task WriteAllTextAsync(string path, string? contents, Guid containerId = default) + => await this.WriteAllTextAsync(path, contents, Encoding.UTF8, containerId); /// /// Write all text to a file. @@ -101,19 +103,21 @@ public void WriteAllText(string path, string? contents, Guid containerId = defau /// The contents of the file. /// The encoding to write with. /// Container to write to. - public void WriteAllText(string path, string? contents, Encoding encoding, Guid containerId = default) + /// A representing the asynchronous operation. + public async Task WriteAllTextAsync(string path, string? contents, Encoding encoding, Guid containerId = default) { var bytes = encoding.GetBytes(contents ?? string.Empty); - this.WriteAllBytes(path, bytes, containerId); + await this.WriteAllBytesAsync(path, bytes, containerId); } - + /// /// Write all bytes to a file. /// /// Path to write to. /// The contents of the file. /// Container to write to. - public void WriteAllBytes(string path, byte[] bytes, Guid containerId = default) + /// A representing the asynchronous operation. + public Task WriteAllBytesAsync(string path, byte[] bytes, Guid containerId = default) { ArgumentException.ThrowIfNullOrEmpty(path); @@ -122,7 +126,7 @@ public void WriteAllBytes(string path, byte[] bytes, Guid containerId = default) if (this.db == null) { Util.WriteAllBytesSafe(path, bytes); - return; + return Task.CompletedTask; } this.db.RunInTransaction(() => @@ -148,6 +152,8 @@ public void WriteAllBytes(string path, byte[] bytes, Guid containerId = default) Util.WriteAllBytesSafe(path, bytes); }); } + + return Task.CompletedTask; } /// @@ -258,6 +264,8 @@ public byte[] ReadAllBytes(string path, bool forceBackup = false, Guid container if (forceBackup) { + Log.Information("Reading from db"); + // If the db failed to load, act as if the file does not exist if (this.db == null) throw new FileNotFoundException("Backup database was not available");