From f093d93761d041b46af2d15f3c82f54698359c6c Mon Sep 17 00:00:00 2001 From: JonKazama-Hellion Date: Tue, 5 May 2026 08:23:54 +0200 Subject: [PATCH] perf(messagemanager): switch pending queue to linked list, quiet privacy log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- HellionChat/MessageManager.cs | 20 +++++++++++++------- HellionChat/MessageStore.cs | 5 ++++- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/HellionChat/MessageManager.cs b/HellionChat/MessageManager.cs index a7b60e5..177d343 100644 --- a/HellionChat/MessageManager.cs +++ b/HellionChat/MessageManager.cs @@ -34,7 +34,10 @@ internal class MessageManager : IAsyncDisposable // After that, the message is enqueued in the PendingAsync queue, which will // be consumed in a separate thread and perform more processing (emotes, // URLs) as well as inserting the message into the database. - private Queue PendingSync { get; } = []; + // LinkedList instead of Queue: ContentIdResolver hits PendingSync.Last + // every hook call. Queue.Last() is the LINQ extension and walks the + // whole queue (O(n)); LinkedList.Last is an O(1) node reference. + private LinkedList PendingSync { get; } = []; private ConcurrentQueue PendingAsync { get; } = []; private readonly Thread PendingMessageThread; private readonly CancellationTokenSource PendingThreadCancellationToken = new(); @@ -117,8 +120,11 @@ internal class MessageManager : IAsyncDisposable LastContentId = contentId; // Drain the PendingSync queue into the PendingAsync queue. - while (PendingSync.TryDequeue(out var pending)) - PendingAsync.Enqueue(pending); + while (PendingSync.First is { } first) + { + PendingSync.RemoveFirst(); + PendingAsync.Enqueue(first.Value); + } } 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 // in the next tick, otherwise we can't get the content ID from the hook // below. - PendingSync.Enqueue(pendingMessage); + PendingSync.AddLast(pendingMessage); } // This hook is called immediately after receiving a message with the @@ -235,11 +241,11 @@ internal class MessageManager : IAsyncDisposable try { ContentIdResolverHook?.Original(agent, contentId, accountId, messageIndex, worldId, chatType); - if (PendingSync.Count == 0) + if (PendingSync.Last is not { } last) return; - PendingSync.Last().ContentId = contentId; - PendingSync.Last().AccountId = accountId; + last.Value.ContentId = contentId; + last.Value.AccountId = accountId; } catch (Exception ex) { diff --git a/HellionChat/MessageStore.cs b/HellionChat/MessageStore.cs index 0517cfc..a257f3a 100644 --- a/HellionChat/MessageStore.cs +++ b/HellionChat/MessageStore.cs @@ -452,7 +452,10 @@ internal class MessageStore : IDisposable // covers any future write paths e.g. webinterface backfill). 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; }