From 89c66e0d3d38e54f506b162c497d1a9dc0e63eab Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Mon, 17 Aug 2026 06:50:04 +0200 Subject: [PATCH] perf(store): index (Receiver, Date) so tell history streams sorted GetTellHistoryWithSender filters on Receiver and orders by Date DESC. Without a matching index SQLite sorted the whole receiver history into a temp b-tree before yielding row one -- measured 10 ms to first row against 9621 tells, all of it under TabsListLock, which defeats the early break in the caller. (Receiver, ChatType, Date) does not help: the ChatType IN filter sits between the equality prefix and the sort column, so the temp b-tree stays. Verified on a real database: plan now reads SEARCH ... USING INDEX idx_messages_receiver_date with no sort step. No SQL LIMIT -- an earlier 500-row cap was removed in v1.4.10 because it cut less-frequent partners off the back of the window, and 83% of partners have fewer than 21 tells in total. The migration dispatcher is cumulative, so Migrate5 is appended to every existing case, not just the new one. --- HellionChat/MessageStore.cs | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/HellionChat/MessageStore.cs b/HellionChat/MessageStore.cs index 0cfe180..1ec4372 100644 --- a/HellionChat/MessageStore.cs +++ b/HellionChat/MessageStore.cs @@ -279,18 +279,25 @@ internal class MessageStore : IDisposable migrationsToDo.Add(Migrate2); migrationsToDo.Add(Migrate3); migrationsToDo.Add(Migrate4); + migrationsToDo.Add(Migrate5); break; case 1: migrationsToDo.Add(Migrate2); migrationsToDo.Add(Migrate3); migrationsToDo.Add(Migrate4); + migrationsToDo.Add(Migrate5); break; case 2: migrationsToDo.Add(Migrate3); migrationsToDo.Add(Migrate4); + migrationsToDo.Add(Migrate5); break; case 3: migrationsToDo.Add(Migrate4); + migrationsToDo.Add(Migrate5); + break; + case 4: + migrationsToDo.Add(Migrate5); break; } @@ -430,6 +437,23 @@ internal class MessageStore : IDisposable SetMigrationVersion(4); } + private void Migrate5() + { + _logger.LogInformation("Running migration 5: Add (Receiver, Date) index for tell history"); + + // GetTellHistoryWithSender filters on Receiver and orders by Date DESC. + // Without a matching index SQLite sorts the whole receiver history into a + // temp b-tree before returning row one, which defeats the early break in + // the caller. (Receiver, ChatType, Date) does NOT help: the ChatType IN + // filter sits between the equality prefix and the sort column. + using var cmd = Connection.CreateCommand(); + cmd.CommandText = + "CREATE INDEX IF NOT EXISTS idx_messages_receiver_date ON messages (Receiver, Date);"; + cmd.ExecuteNonQuery(); + + SetMigrationVersion(5); + } + private void SetMigrationVersion(int version) { _logger.LogInformation($"Setting version {version}"); @@ -1014,7 +1038,8 @@ internal class MessageStore : IDisposable } // Returns up to `limit` tells exchanged with the named player, oldest-first. - // SQL narrows by Receiver + ChatType via the (Receiver, Date) index, then + // SQL narrows by Receiver + ChatType via the (Receiver, Date) index (migration + // 5; before that the ordering fell back to a temp b-tree over all rows), then // the client-side loop runs PlayerPayload comparison and breaks once // `limit` partner matches accumulate. Earlier versions had a hardcoded // 500-row scan cap that cut less-frequent pinned partners off the back of