perf(chat): look up chat txids through a revision-keyed index
The History tab copies every chat txid into a fresh set each frame to badge and filter chat transactions: M string allocations for M chat messages. ChatStore gains hasChatTxid(), an O(1) lookup in a txid index that is rebuilt only after the store changed (append, delivery update, erase, clear) and never built for an empty store. clear() also drops the index, so no chat txids linger after a lock. The History tab moves to it in the next commit. The chat note-buffer pump, which runs every frame while chat is idle, built the whole conversation-id list just to see whether the store was empty; it now asks store().empty(), O(1) instead of O(M). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8473,6 +8473,90 @@ void testTransactionRefreshRevision()
|
||||
EXPECT_EQ(state.last_tx_update, static_cast<int64_t>(300));
|
||||
}
|
||||
|
||||
// ChatStore::hasChatTxid (History "Message" badge / Chat filter): a lazy txid index keyed on the monotonic
|
||||
// revision(), so every mutation (append / updateDelivery / eraseConversation / clear) is seen on the next lookup;
|
||||
// clear() also drops it. store.empty() must equal conversationIds().empty() (shouldSplitChatBuffer relies on it).
|
||||
void testChatStoreTxidIndex()
|
||||
{
|
||||
using namespace dragonx::chat;
|
||||
ChatStore store;
|
||||
auto mk = [](const std::string& txid, std::size_t pos, const std::string& cid) {
|
||||
ChatMessage m;
|
||||
m.txid = txid; m.payload_position = pos; m.conversation_id = cid; m.body = "b";
|
||||
return m;
|
||||
};
|
||||
auto emptyMatches = [&store]() { return store.empty() == store.conversationIds().empty(); };
|
||||
|
||||
EXPECT_FALSE(store.hasChatTxid("t1"));
|
||||
EXPECT_FALSE(store.hasChatTxid(""));
|
||||
EXPECT_TRUE(emptyMatches());
|
||||
|
||||
EXPECT_TRUE(store.append(mk("t1", 0, "a")));
|
||||
EXPECT_TRUE(emptyMatches());
|
||||
EXPECT_TRUE(store.hasChatTxid("t1"));
|
||||
EXPECT_FALSE(store.hasChatTxid("t2"));
|
||||
|
||||
// One txid backing two messages in two conversations: erasing one keeps the txid reported.
|
||||
EXPECT_TRUE(store.append(mk("t1", 1, "b")));
|
||||
EXPECT_TRUE(emptyMatches());
|
||||
EXPECT_TRUE(store.hasChatTxid("t1"));
|
||||
EXPECT_EQ(store.eraseConversation("a").size(), static_cast<size_t>(1));
|
||||
EXPECT_TRUE(emptyMatches());
|
||||
EXPECT_TRUE(store.hasChatTxid("t1"));
|
||||
EXPECT_EQ(store.eraseConversation("b").size(), static_cast<size_t>(1));
|
||||
EXPECT_TRUE(emptyMatches());
|
||||
EXPECT_TRUE(store.empty());
|
||||
EXPECT_FALSE(store.hasChatTxid("t1"));
|
||||
|
||||
// A lookup after the index was built still sees a later append (revision moved).
|
||||
EXPECT_TRUE(store.append(mk("t3", 0, "c")));
|
||||
EXPECT_TRUE(store.hasChatTxid("t3"));
|
||||
EXPECT_TRUE(store.append(mk("t2", 0, "c")));
|
||||
EXPECT_TRUE(emptyMatches());
|
||||
EXPECT_TRUE(store.hasChatTxid("t2"));
|
||||
const std::uint64_t revBeforeClear = store.revision();
|
||||
store.clear();
|
||||
EXPECT_TRUE(emptyMatches());
|
||||
EXPECT_TRUE(store.revision() > revBeforeClear);
|
||||
EXPECT_FALSE(store.hasChatTxid("t2"));
|
||||
EXPECT_FALSE(store.hasChatTxid("t3"));
|
||||
|
||||
// An empty txid is never reported, even when a message carries one.
|
||||
EXPECT_TRUE(store.append(mk("", 0, "d")));
|
||||
EXPECT_TRUE(emptyMatches());
|
||||
EXPECT_FALSE(store.hasChatTxid(""));
|
||||
|
||||
// An outgoing echo stays reported across a delivery change.
|
||||
ChatMessage echo = mk("out:ab", 0, "e");
|
||||
echo.direction = ChatDirection::Outgoing;
|
||||
echo.delivery = ChatDelivery::Sending;
|
||||
EXPECT_TRUE(store.append(echo));
|
||||
EXPECT_TRUE(emptyMatches());
|
||||
EXPECT_TRUE(store.hasChatTxid("out:ab"));
|
||||
const std::uint64_t revBeforeUpdate = store.revision();
|
||||
EXPECT_TRUE(store.updateDelivery("out:ab", ChatDelivery::Failed) != nullptr);
|
||||
EXPECT_TRUE(store.revision() > revBeforeUpdate);
|
||||
EXPECT_TRUE(store.hasChatTxid("out:ab"));
|
||||
EXPECT_FALSE(store.hasChatTxid("out:zz"));
|
||||
|
||||
store.clear();
|
||||
EXPECT_TRUE(store.empty());
|
||||
EXPECT_TRUE(emptyMatches());
|
||||
EXPECT_FALSE(store.hasChatTxid("out:ab"));
|
||||
|
||||
// Erasing a conversation while the store stays non-empty (so the empty-store early return can't hide a
|
||||
// stale index): with the index already built, only the erased conversation's txid disappears.
|
||||
EXPECT_TRUE(store.append(mk("t1", 0, "a")));
|
||||
EXPECT_TRUE(store.append(mk("t2", 0, "c")));
|
||||
EXPECT_TRUE(store.hasChatTxid("t1"));
|
||||
EXPECT_TRUE(store.hasChatTxid("t2"));
|
||||
EXPECT_EQ(store.eraseConversation("a").size(), static_cast<size_t>(1));
|
||||
EXPECT_FALSE(store.empty());
|
||||
EXPECT_TRUE(emptyMatches());
|
||||
EXPECT_FALSE(store.hasChatTxid("t1"));
|
||||
EXPECT_TRUE(store.hasChatTxid("t2"));
|
||||
}
|
||||
|
||||
// M2: a parsed lite refresh bundle maps through to the app's WalletState (the last hop
|
||||
// the Balance/Receive/Transactions tabs read), with zatoshi->DRGX conversion, z/t address
|
||||
// split, transaction typing, confirmations, and sync progress.
|
||||
@@ -14545,6 +14629,7 @@ int main()
|
||||
testHushChatService();
|
||||
testHushChatDatabase();
|
||||
testHushChatDeleteConversation();
|
||||
testChatStoreTxidIndex();
|
||||
testHushChatOutgoing();
|
||||
testHushChatTransport();
|
||||
testHushChatShuffledReceive();
|
||||
|
||||
Reference in New Issue
Block a user