perf(settings): write config on release instead of every slider frame
ImGui sliders report a change in every frame the value moves, so dragging one rewrote the full 31 KB config to disk per frame -- serialize, fsync and rename, synchronously on the draw thread. Measured on Linux/Wine that showed up as a 114 ms frame while the plugin itself only drew for 2.9 ms; the rest was waiting on the write. The five shared slider helpers now defer SaveConfig to IsItemDeactivatedAfterEdit, matching what ChatColourPicker already did for the colour wheel. Renaming a tab needed its own path: the input lives inside a popup, and ImGui never re-submits it when the popup is dismissed by clicking outside, so IsItemDeactivatedAfterEdit would not fire and the new name would be lost. A pending-rename marker scoped to the owning tab flushes it when the popup is gone -- scoped, because every other tab's Draw reaches that branch too. DeferredSaveFrames is removed: the debounce was fully wired but never armed, and this approach makes it redundant.
This commit is contained in:
@@ -175,8 +175,6 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
// PerformanceBaselineStep so the hot path stays allocation-free.
|
||||
internal double LastDrawMs;
|
||||
|
||||
internal int DeferredSaveFrames = -1;
|
||||
|
||||
// Cancels the v1.4.8 FTS5 bulk-insert worker on plugin teardown. The
|
||||
// worker runs off the framework thread on its own SqliteConnection, so a
|
||||
// Dispose mid-rebuild must signal cancellation before MessageManager
|
||||
@@ -277,8 +275,6 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
|
||||
ImGuiUtil.Initialize(this);
|
||||
|
||||
DeferredSaveFrames = -1;
|
||||
|
||||
// Custom themes dir + seed run before the container builds so the
|
||||
// ThemeRegistry factory lambda finds the directory ready.
|
||||
var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes");
|
||||
@@ -641,19 +637,6 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
}
|
||||
);
|
||||
|
||||
// Flush a pending DeferredSave — FrameworkUpdate won't fire it anymore.
|
||||
failure = CaptureFailure(
|
||||
failure,
|
||||
() =>
|
||||
{
|
||||
if (DeferredSaveFrames >= 0)
|
||||
{
|
||||
SaveConfig();
|
||||
DeferredSaveFrames = -1;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Framework-thread cleanup the container does not reach.
|
||||
try
|
||||
{
|
||||
@@ -1134,9 +1117,6 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
|
||||
private void FrameworkUpdate(IFramework framework)
|
||||
{
|
||||
if (DeferredSaveFrames >= 0 && DeferredSaveFrames-- == 0)
|
||||
SaveConfig();
|
||||
|
||||
if (!Config.HideChat)
|
||||
return;
|
||||
|
||||
|
||||
@@ -116,10 +116,11 @@ internal sealed class ChannelsTab
|
||||
{
|
||||
var current = get();
|
||||
ImGui.SetNextItemWidth(200);
|
||||
// Sliders report a change every frame while dragging; deferring the write
|
||||
// to release turns ~30 full-config disk writes per second into one.
|
||||
if (ImGui.SliderInt(label, ref current, min, max, "%d"))
|
||||
{
|
||||
set(current);
|
||||
if (ImGui.IsItemDeactivatedAfterEdit())
|
||||
_plugin.SaveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,10 +108,11 @@ internal sealed class DataPrivacyTab
|
||||
{
|
||||
var current = get();
|
||||
ImGui.SetNextItemWidth(200);
|
||||
// Sliders report a change every frame while dragging; deferring the write
|
||||
// to release turns ~30 full-config disk writes per second into one.
|
||||
if (ImGui.SliderInt(label, ref current, min, max, "%d"))
|
||||
{
|
||||
set(current);
|
||||
if (ImGui.IsItemDeactivatedAfterEdit())
|
||||
_plugin.SaveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,11 +80,12 @@ internal sealed class GeneralTab
|
||||
{
|
||||
var current = get();
|
||||
ImGui.SetNextItemWidth(200);
|
||||
// Sliders report a change every frame while dragging; deferring the write
|
||||
// to release turns ~30 full-config disk writes per second into one.
|
||||
if (ImGui.SliderFloat(label, ref current, min, max, "%.2f"))
|
||||
{
|
||||
set(current);
|
||||
if (ImGui.IsItemDeactivatedAfterEdit())
|
||||
_plugin.SaveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
// Wires the already-present ImGuiUtil.KeybindInput capture widget (dead/unwired
|
||||
|
||||
@@ -134,21 +134,23 @@ internal sealed class WindowTab
|
||||
{
|
||||
var current = get();
|
||||
ImGui.SetNextItemWidth(200);
|
||||
// Sliders report a change every frame while dragging; deferring the write
|
||||
// to release turns ~30 full-config disk writes per second into one.
|
||||
if (ImGui.SliderFloat(label, ref current, min, max, "%.2f"))
|
||||
{
|
||||
set(current);
|
||||
if (ImGui.IsItemDeactivatedAfterEdit())
|
||||
_plugin.SaveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawSliderInt(string label, Func<int> get, Action<int> set, int min, int max)
|
||||
{
|
||||
var current = get();
|
||||
ImGui.SetNextItemWidth(200);
|
||||
// Sliders report a change every frame while dragging; deferring the write
|
||||
// to release turns ~30 full-config disk writes per second into one.
|
||||
if (ImGui.SliderInt(label, ref current, min, max, "%d"))
|
||||
{
|
||||
set(current);
|
||||
if (ImGui.IsItemDeactivatedAfterEdit())
|
||||
_plugin.SaveConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,12 @@ namespace HellionChat.Ui.Components;
|
||||
// state and reaches the live Config/Plugin through Plugin.Instance/Plugin.Config.
|
||||
internal static class TabContextMenu
|
||||
{
|
||||
// Pending rename, scoped to one tab. ImGui never re-submits the input when the
|
||||
// popup is dismissed by clicking outside, so IsItemDeactivatedAfterEdit never
|
||||
// fires there — without this the rename would be lost.
|
||||
private static Guid _renamingTab;
|
||||
private static bool _renameDirty;
|
||||
|
||||
// MUST be called immediately after the row-carrying ImGui item (Sidebar
|
||||
// "row" InvisibleButton / TopTabBar Selectable). popupId only names the
|
||||
// popup; the open trigger is a right-click on the LAST submitted item
|
||||
@@ -20,7 +26,18 @@ internal static class TabContextMenu
|
||||
public static void Draw(Tab tab, string popupId, Windows.ChannelPopoutPool pool)
|
||||
{
|
||||
if (!ImGui.BeginPopupContextItem(popupId))
|
||||
{
|
||||
// Popup gone: flush a pending rename. Scoped to the OWNING tab — every
|
||||
// other tab's Draw lands here too and would flush foreign state.
|
||||
if (_renamingTab == tab.Identifier)
|
||||
{
|
||||
if (_renameDirty)
|
||||
Plugin.Instance.SaveConfig();
|
||||
ClearPendingRename();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Rename: focus the field the first frame the popup appears.
|
||||
if (ImGui.IsWindowAppearing())
|
||||
@@ -28,7 +45,19 @@ internal static class TabContextMenu
|
||||
ImGui.SetNextItemWidth(250f * ImGuiHelpers.GlobalScale);
|
||||
var name = tab.Name;
|
||||
if (ImGui.InputText("##tab-name", ref name, 512) && ApplyTabRename(tab, name))
|
||||
Plugin.Instance.SaveConfig();
|
||||
{
|
||||
_renamingTab = tab.Identifier;
|
||||
_renameDirty = true;
|
||||
}
|
||||
|
||||
// Covers leaving the field while the popup stays open; the dismissed-popup
|
||||
// case is handled above.
|
||||
if (ImGui.IsItemDeactivatedAfterEdit() && _renamingTab == tab.Identifier)
|
||||
{
|
||||
if (_renameDirty)
|
||||
Plugin.Instance.SaveConfig();
|
||||
ClearPendingRename();
|
||||
}
|
||||
|
||||
// Per-tab notification sound (B3-3). The checkbox gates the picker so
|
||||
// tabs that never want a sound keep the popup short.
|
||||
@@ -49,6 +78,15 @@ internal static class TabContextMenu
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
|
||||
// The flush depends on Draw running once more for this tab. If it never does —
|
||||
// LRU eviction, logout, window closed or collapsed, plugin unload, game exit —
|
||||
// the name only lives in memory until some other SaveConfig happens to run.
|
||||
private static void ClearPendingRename()
|
||||
{
|
||||
_renamingTab = Guid.Empty;
|
||||
_renameDirty = false;
|
||||
}
|
||||
|
||||
// Sound picker: 16 numbered game sounds, a separator, then the 3 bundled
|
||||
// Hellion clips stored as ids 17-19 (1.5.6 parity order). The collapsed
|
||||
// preview reuses the entry label scheme so the current pick reads the same
|
||||
|
||||
Reference in New Issue
Block a user