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.
|
// PerformanceBaselineStep so the hot path stays allocation-free.
|
||||||
internal double LastDrawMs;
|
internal double LastDrawMs;
|
||||||
|
|
||||||
internal int DeferredSaveFrames = -1;
|
|
||||||
|
|
||||||
// Cancels the v1.4.8 FTS5 bulk-insert worker on plugin teardown. The
|
// 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
|
// worker runs off the framework thread on its own SqliteConnection, so a
|
||||||
// Dispose mid-rebuild must signal cancellation before MessageManager
|
// Dispose mid-rebuild must signal cancellation before MessageManager
|
||||||
@@ -277,8 +275,6 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
|
|
||||||
ImGuiUtil.Initialize(this);
|
ImGuiUtil.Initialize(this);
|
||||||
|
|
||||||
DeferredSaveFrames = -1;
|
|
||||||
|
|
||||||
// Custom themes dir + seed run before the container builds so the
|
// Custom themes dir + seed run before the container builds so the
|
||||||
// ThemeRegistry factory lambda finds the directory ready.
|
// ThemeRegistry factory lambda finds the directory ready.
|
||||||
var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes");
|
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.
|
// Framework-thread cleanup the container does not reach.
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -1134,9 +1117,6 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
|
|
||||||
private void FrameworkUpdate(IFramework framework)
|
private void FrameworkUpdate(IFramework framework)
|
||||||
{
|
{
|
||||||
if (DeferredSaveFrames >= 0 && DeferredSaveFrames-- == 0)
|
|
||||||
SaveConfig();
|
|
||||||
|
|
||||||
if (!Config.HideChat)
|
if (!Config.HideChat)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@@ -116,10 +116,11 @@ internal sealed class ChannelsTab
|
|||||||
{
|
{
|
||||||
var current = get();
|
var current = get();
|
||||||
ImGui.SetNextItemWidth(200);
|
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"))
|
if (ImGui.SliderInt(label, ref current, min, max, "%d"))
|
||||||
{
|
|
||||||
set(current);
|
set(current);
|
||||||
|
if (ImGui.IsItemDeactivatedAfterEdit())
|
||||||
_plugin.SaveConfig();
|
_plugin.SaveConfig();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,10 +108,11 @@ internal sealed class DataPrivacyTab
|
|||||||
{
|
{
|
||||||
var current = get();
|
var current = get();
|
||||||
ImGui.SetNextItemWidth(200);
|
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"))
|
if (ImGui.SliderInt(label, ref current, min, max, "%d"))
|
||||||
{
|
|
||||||
set(current);
|
set(current);
|
||||||
|
if (ImGui.IsItemDeactivatedAfterEdit())
|
||||||
_plugin.SaveConfig();
|
_plugin.SaveConfig();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,12 +80,13 @@ internal sealed class GeneralTab
|
|||||||
{
|
{
|
||||||
var current = get();
|
var current = get();
|
||||||
ImGui.SetNextItemWidth(200);
|
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"))
|
if (ImGui.SliderFloat(label, ref current, min, max, "%.2f"))
|
||||||
{
|
|
||||||
set(current);
|
set(current);
|
||||||
|
if (ImGui.IsItemDeactivatedAfterEdit())
|
||||||
_plugin.SaveConfig();
|
_plugin.SaveConfig();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Wires the already-present ImGuiUtil.KeybindInput capture widget (dead/unwired
|
// Wires the already-present ImGuiUtil.KeybindInput capture widget (dead/unwired
|
||||||
// since the v1.6.0 rewrite) back into the settings, so ChatTabForward/Backward
|
// since the v1.6.0 rewrite) back into the settings, so ChatTabForward/Backward
|
||||||
|
|||||||
@@ -134,21 +134,23 @@ internal sealed class WindowTab
|
|||||||
{
|
{
|
||||||
var current = get();
|
var current = get();
|
||||||
ImGui.SetNextItemWidth(200);
|
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"))
|
if (ImGui.SliderFloat(label, ref current, min, max, "%.2f"))
|
||||||
{
|
|
||||||
set(current);
|
set(current);
|
||||||
|
if (ImGui.IsItemDeactivatedAfterEdit())
|
||||||
_plugin.SaveConfig();
|
_plugin.SaveConfig();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawSliderInt(string label, Func<int> get, Action<int> set, int min, int max)
|
private void DrawSliderInt(string label, Func<int> get, Action<int> set, int min, int max)
|
||||||
{
|
{
|
||||||
var current = get();
|
var current = get();
|
||||||
ImGui.SetNextItemWidth(200);
|
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"))
|
if (ImGui.SliderInt(label, ref current, min, max, "%d"))
|
||||||
{
|
|
||||||
set(current);
|
set(current);
|
||||||
|
if (ImGui.IsItemDeactivatedAfterEdit())
|
||||||
_plugin.SaveConfig();
|
_plugin.SaveConfig();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ namespace HellionChat.Ui.Components;
|
|||||||
// state and reaches the live Config/Plugin through Plugin.Instance/Plugin.Config.
|
// state and reaches the live Config/Plugin through Plugin.Instance/Plugin.Config.
|
||||||
internal static class TabContextMenu
|
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
|
// MUST be called immediately after the row-carrying ImGui item (Sidebar
|
||||||
// "row" InvisibleButton / TopTabBar Selectable). popupId only names the
|
// "row" InvisibleButton / TopTabBar Selectable). popupId only names the
|
||||||
// popup; the open trigger is a right-click on the LAST submitted item
|
// 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)
|
public static void Draw(Tab tab, string popupId, Windows.ChannelPopoutPool pool)
|
||||||
{
|
{
|
||||||
if (!ImGui.BeginPopupContextItem(popupId))
|
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;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Rename: focus the field the first frame the popup appears.
|
// Rename: focus the field the first frame the popup appears.
|
||||||
if (ImGui.IsWindowAppearing())
|
if (ImGui.IsWindowAppearing())
|
||||||
@@ -28,7 +45,19 @@ internal static class TabContextMenu
|
|||||||
ImGui.SetNextItemWidth(250f * ImGuiHelpers.GlobalScale);
|
ImGui.SetNextItemWidth(250f * ImGuiHelpers.GlobalScale);
|
||||||
var name = tab.Name;
|
var name = tab.Name;
|
||||||
if (ImGui.InputText("##tab-name", ref name, 512) && ApplyTabRename(tab, name))
|
if (ImGui.InputText("##tab-name", ref name, 512) && ApplyTabRename(tab, name))
|
||||||
|
{
|
||||||
|
_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();
|
Plugin.Instance.SaveConfig();
|
||||||
|
ClearPendingRename();
|
||||||
|
}
|
||||||
|
|
||||||
// Per-tab notification sound (B3-3). The checkbox gates the picker so
|
// Per-tab notification sound (B3-3). The checkbox gates the picker so
|
||||||
// tabs that never want a sound keep the popup short.
|
// tabs that never want a sound keep the popup short.
|
||||||
@@ -49,6 +78,15 @@ internal static class TabContextMenu
|
|||||||
ImGui.EndPopup();
|
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
|
// 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
|
// 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
|
// preview reuses the entry label scheme so the current pick reads the same
|
||||||
|
|||||||
Reference in New Issue
Block a user