perf(history): group transaction rows by txid once per change

The History tab built a txid-to-indices map twice when its inputs
changed, once for the summary-card totals and once for the display list,
each a full pass over every transaction with string-keyed hashing.

- The rows are now grouped by txid a single time whenever the
  transaction list changes, into an index-only structure (first/next
  chains with no string copies), and both the summary pass and the
  display pass reuse it. A sort-combo change re-sorts without regrouping.

Adds a pure, unit-testable groupByTxid helper in
transactions_tab_helpers.h.

Tests: the chains match a reference map over a random id stream, and the
empty and single-row cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 09:24:51 -05:00
co-authored by Claude Opus 4.8
parent 01b59d24db
commit a5e4fd5c02
3 changed files with 116 additions and 18 deletions
+44
View File
@@ -63,6 +63,7 @@
#include "ui/windows/console_tab_helpers.h"
#include "ui/windows/console_text_layout.h"
#include "ui/windows/mining_benchmark.h"
#include "ui/windows/transactions_tab_helpers.h"
#include "ui/windows/mining_pool_panel.h"
#include "ui/windows/mining_tab_helpers.h"
#include "ui/material/scroll_edge_fade.h"
@@ -10527,6 +10528,48 @@ void testSameTransactionList()
}
}
void testHistoryTxidGroups()
{
using dragonx::ui::history::groupByTxid;
auto mk = [](const std::vector<std::string>& ids) {
std::vector<dragonx::TransactionInfo> v;
for (const auto& id : ids) { dragonx::TransactionInfo t; t.txid = id; v.push_back(t); }
return v;
};
{
auto g = groupByTxid(mk({"A", "B", "A", "C", "B", "A"}));
EXPECT_TRUE((g.first == std::vector<int>{0, 1, 0, 3, 1, 0}));
EXPECT_TRUE((g.next == std::vector<int>{2, 4, 5, -1, -1, -1}));
EXPECT_TRUE(g.hasSiblings(0) && g.hasSiblings(1) && g.hasSiblings(2) && g.hasSiblings(5));
EXPECT_TRUE(!g.hasSiblings(3));
}
{ // property check against a reference map
unsigned seed = 12345;
std::vector<std::string> ids;
for (int i = 0; i < 500; ++i) { seed = seed * 1103515245u + 12345u; ids.push_back("t" + std::to_string((seed >> 16) % 40)); }
auto txs = mk(ids);
auto g = groupByTxid(txs);
std::unordered_map<std::string, std::vector<size_t>> ref;
for (size_t i = 0; i < txs.size(); ++i) ref[txs[i].txid].push_back(i);
bool ok = true;
for (const auto& kv : ref) {
int cur = (int)kv.second[0];
for (size_t k = 0; k < kv.second.size(); ++k) {
if (cur != (int)kv.second[k] || g.first[kv.second[k]] != (int)kv.second[0]) { ok = false; break; }
cur = g.next[cur];
}
if (cur != -1) ok = false;
}
EXPECT_TRUE(ok);
}
{
auto g = groupByTxid(mk({}));
EXPECT_TRUE(g.first.empty() && g.next.empty());
auto s = groupByTxid(mk({"X"}));
EXPECT_TRUE(s.first.size() == 1 && s.first[0] == 0 && s.next[0] == -1 && !s.hasSiblings(0));
}
}
// tx_revision starts at >=1 (a UI cache's 0 sentinel never matches), each helper adds exactly 1, the helpers
// never touch last_tx_update, and clear() bumps without resetting, so no earlier revision ever recurs.
void testWalletStateTxRevision()
@@ -18016,6 +18059,7 @@ int main()
testLiteRefreshModelAppliesToWalletState();
testLiteTransactionsAreNewestFirst();
testSameTransactionList();
testHistoryTxidGroups();
testWalletStateTxRevision();
testTransactionRefreshRevision();
testLiteSendShowsRecipientFromOutgoing();