perf(tabs): share Plugin.TabsListLock across AutoTellTabsService + MessageManager refilter + SaveConfig (B3)
This commit is contained in:
+22
-9
@@ -189,6 +189,12 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
internal readonly object RetentionSweepLock = new();
|
||||
internal volatile bool RetentionSweepRunning;
|
||||
|
||||
// B3: neutral owner of the Config.Tabs LIST-structure lock so both the
|
||||
// worker-thread mutator (AutoTellTabsService) and the framework-thread
|
||||
// refilter (MessageManager) share ONE monitor. Lock order: this outer,
|
||||
// MessageList's SemaphoreSlim inner — never the reverse.
|
||||
internal readonly object TabsListLock = new();
|
||||
|
||||
internal DateTime GameStarted { get; }
|
||||
|
||||
// Couples "current tab" to the real UI selection. The chat hooks are
|
||||
@@ -976,11 +982,10 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
{
|
||||
Log.Information($"Retention sweep deleted {deleted} expired messages.");
|
||||
// Schedule on the next framework tick to avoid the ~194ms
|
||||
// hitch from blocking with .Wait() while the framework
|
||||
// finishes the current frame. Tabs-list mutation must
|
||||
// stay on the framework thread because Plugin.Config.Tabs
|
||||
// (Configuration.cs:222) is not lock-protected and
|
||||
// AutoTellTabsService can mutate it from background paths.
|
||||
// hitch from blocking with .Wait() while the frame finishes.
|
||||
// The Config.Tabs enumeration in ClearAllTabs/FilterAllTabs is
|
||||
// now guarded by the shared Plugin.TabsListLock (B3), so this
|
||||
// tick scheduling is purely hitch-avoidance, not safety.
|
||||
// Pattern reference: SimpleTweaks
|
||||
// Tweaks/Chat/CaseInsensitiveCommands.cs:45.
|
||||
Framework.RunOnTick(() =>
|
||||
@@ -1091,12 +1096,20 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
// Config.Tabs across the save so JSON includes them. Cloning only the
|
||||
// unpinned subset keeps the allocation proportional to
|
||||
// AutoTellTabsLimit (<=15) instead of the full tab list.
|
||||
var unpinnedTempTabs = Config.Tabs.Where(TabLifecycleHelpers.IsInUnpinnedPool).ToList();
|
||||
Config.Tabs.RemoveAll(TabLifecycleHelpers.ShouldStripOnSave);
|
||||
// B3: the strip/restore mutates the tab LIST, so it shares TabsListLock
|
||||
// with the worker add/remove and the refilter snapshot. Re-entrant: the
|
||||
// one worker caller (HandleTell) already holds it; framework callers take
|
||||
// it here. SavePluginConfig runs inside (short, in-memory) — the §8 fallback
|
||||
// (serialize a copy outside the lock) is a tracked pre-beta to-do.
|
||||
lock (TabsListLock)
|
||||
{
|
||||
var unpinnedTempTabs = Config.Tabs.Where(TabLifecycleHelpers.IsInUnpinnedPool).ToList();
|
||||
Config.Tabs.RemoveAll(TabLifecycleHelpers.ShouldStripOnSave);
|
||||
|
||||
Interface.SavePluginConfig(Config);
|
||||
Interface.SavePluginConfig(Config);
|
||||
|
||||
Config.Tabs.AddRange(unpinnedTempTabs);
|
||||
Config.Tabs.AddRange(unpinnedTempTabs);
|
||||
}
|
||||
}
|
||||
|
||||
internal void LanguageChanged(string langCode)
|
||||
|
||||
Reference in New Issue
Block a user