perf(messagemanager): switch pending queue to linked list, quiet privacy log

PendingSync läuft jetzt als LinkedList (O(1) Last statt O(n) Linq-Last
im ContentIdResolverHook); Privacy-Filter-Drop-Log auf Verbose runter,
sodass der Default-xllog-Stream nicht mehr pro Nachricht spammt.
This commit is contained in:
2026-05-05 08:23:54 +02:00
parent e7c8667497
commit f093d93761
2 changed files with 17 additions and 8 deletions
+13 -7
View File
@@ -34,7 +34,10 @@ internal class MessageManager : IAsyncDisposable
// After that, the message is enqueued in the PendingAsync queue, which will // After that, the message is enqueued in the PendingAsync queue, which will
// be consumed in a separate thread and perform more processing (emotes, // be consumed in a separate thread and perform more processing (emotes,
// URLs) as well as inserting the message into the database. // URLs) as well as inserting the message into the database.
private Queue<PendingMessage> PendingSync { get; } = []; // LinkedList instead of Queue: ContentIdResolver hits PendingSync.Last
// every hook call. Queue<T>.Last() is the LINQ extension and walks the
// whole queue (O(n)); LinkedList<T>.Last is an O(1) node reference.
private LinkedList<PendingMessage> PendingSync { get; } = [];
private ConcurrentQueue<PendingMessage> PendingAsync { get; } = []; private ConcurrentQueue<PendingMessage> PendingAsync { get; } = [];
private readonly Thread PendingMessageThread; private readonly Thread PendingMessageThread;
private readonly CancellationTokenSource PendingThreadCancellationToken = new(); private readonly CancellationTokenSource PendingThreadCancellationToken = new();
@@ -117,8 +120,11 @@ internal class MessageManager : IAsyncDisposable
LastContentId = contentId; LastContentId = contentId;
// Drain the PendingSync queue into the PendingAsync queue. // Drain the PendingSync queue into the PendingAsync queue.
while (PendingSync.TryDequeue(out var pending)) while (PendingSync.First is { } first)
PendingAsync.Enqueue(pending); {
PendingSync.RemoveFirst();
PendingAsync.Enqueue(first.Value);
}
} }
private void ProcessPendingMessages(CancellationToken token) private void ProcessPendingMessages(CancellationToken token)
@@ -223,7 +229,7 @@ internal class MessageManager : IAsyncDisposable
// We delay messages to be handed off to the async processing thread // We delay messages to be handed off to the async processing thread
// in the next tick, otherwise we can't get the content ID from the hook // in the next tick, otherwise we can't get the content ID from the hook
// below. // below.
PendingSync.Enqueue(pendingMessage); PendingSync.AddLast(pendingMessage);
} }
// This hook is called immediately after receiving a message with the // This hook is called immediately after receiving a message with the
@@ -235,11 +241,11 @@ internal class MessageManager : IAsyncDisposable
try try
{ {
ContentIdResolverHook?.Original(agent, contentId, accountId, messageIndex, worldId, chatType); ContentIdResolverHook?.Original(agent, contentId, accountId, messageIndex, worldId, chatType);
if (PendingSync.Count == 0) if (PendingSync.Last is not { } last)
return; return;
PendingSync.Last().ContentId = contentId; last.Value.ContentId = contentId;
PendingSync.Last().AccountId = accountId; last.Value.AccountId = accountId;
} }
catch (Exception ex) catch (Exception ex)
{ {
+4 -1
View File
@@ -452,7 +452,10 @@ internal class MessageStore : IDisposable
// covers any future write paths e.g. webinterface backfill). // covers any future write paths e.g. webinterface backfill).
if (!Plugin.Config.IsAllowedForStorage(message.Code.Type)) if (!Plugin.Config.IsAllowedForStorage(message.Code.Type))
{ {
Plugin.Log.Debug($"Privacy filter dropped message: ChatType={message.Code.Type}"); // Verbose-only: this fires for every dropped message, which is
// the common case for users with a tight privacy whitelist. Keep
// it for diagnostics but stay out of the default xllog stream.
Plugin.Log.Verbose($"Privacy filter dropped message: ChatType={message.Code.Type}");
return; return;
} }