perf(dbviewer): dispatch FTS filter to worker thread

FullTextSearch + LoadByGuids could stall the draw thread for 100-300 ms
on large databases with a popular search term. The two hot trigger sites
(FTS toggle, search input) now route via TriggerFilterRefresh, which
dispatches the FTS path to Task.Run; the in-memory page-filter path
stays inline because it is sub-ms on the loaded page array.

_ftsFilterSeq is bumped per trigger so a late worker recognises itself
as stale and drops its result instead of overwriting a newer one. The
date/channel and history workers already lived on Task.Run and are
untouched.

Surfaced during the v1.4.8 pre-tag review.
This commit is contained in:
2026-05-14 11:48:40 +02:00
parent 7b36763359
commit 7542d48983
+35 -2
View File
@@ -2,6 +2,7 @@
using System.Globalization;
using System.Numerics;
using System.Text;
using System.Threading;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface;
@@ -44,6 +45,10 @@ public class DbViewer : Window
private bool IsProcessing;
private long ProcessingStart = Environment.TickCount64;
// Bumped per trigger so a late worker drops itself instead of overwriting
// a newer result.
private long _ftsFilterSeq;
private (DateTime Min, DateTime Max, int Page, bool Local, int ChannelCount) LastProcessed;
private string MinDateString = "";
@@ -249,7 +254,7 @@ public class DbViewer : Window
using (ImRaii.Disabled(!ftsReady))
{
if (ImGui.Checkbox(HellionStrings.DbViewer_FullTextToggle, ref UseFullTextSearch))
Filtered = Filter(Messages);
TriggerFilterRefresh();
}
ImGuiUtil.HelpMarker(
ftsReady
@@ -267,7 +272,7 @@ public class DbViewer : Window
30
)
)
Filtered = Filter(Messages);
TriggerFilterRefresh();
// Third row
@@ -471,6 +476,34 @@ public class DbViewer : Window
}
}
// FTS path hits SQLite per keystroke -- dispatch to a worker, drop stale
// results via _ftsFilterSeq. Page-filter path is in-memory LINQ, stays
// inline.
private void TriggerFilterRefresh()
{
if (!UseFullTextSearch || !Plugin.MessageManager.Store.IsFtsIndexBuilt)
{
Filtered = Filter(Messages);
return;
}
var snapshot = Messages;
var mySeq = Interlocked.Increment(ref _ftsFilterSeq);
Task.Run(() =>
{
try
{
var result = Filter(snapshot);
if (Interlocked.Read(ref _ftsFilterSeq) == mySeq)
Filtered = result;
}
catch (Exception ex)
{
Plugin.LogProxy.Error(ex, "FTS filter worker failed");
}
});
}
private ConcurrentStack<Message> Filter(Message[] messages)
{
if (SimpleSearchTerm == "")