perf(history): don't rebuild the O(N) tx display cache on every block for a confirmation-only change

Performance audit: tx_revision bumps on every new block because each tx's confirmation count rises, so
the History tab's groupByTxid + merge + sort + deep-copy display cache fully rebuilt once per ~36s block
on the UI thread, even when nothing but the counts changed.

Add WalletState::tx_structure_revision, bumped alongside tx_revision EXCEPT when setTransactionsIfChanged
sees a pure confirmation-count delta (new sameTransactionLayout: every row equal ignoring the confirmation
COUNT, but the pending/confirmations==0 state — which drives the pending-first sort and shield merge — must
still match). Every other writer bumps both (conservative default). The History tab's groups cache and
display cache now key on tx_structure_revision; on a confirmation-only delta it patches dtx.confirmations
in place (rows, order and indices into state.transactions are unchanged) instead of rebuilding, and re-stamps
the filter snapshot so "export current filter" stays valid. Displayed confirmations still update every block.

No fund path touched; display/caching only. Test: testTxStructureRevision.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 14:44:13 -05:00
co-authored by Claude Opus 4.8
parent 6d58802127
commit df98f98669
3 changed files with 112 additions and 8 deletions
+66
View File
@@ -11286,6 +11286,71 @@ void testSameTransactionList()
}
}
// tx_structure_revision: a pure confirmation-count bump moves tx_revision only; any other change, a pending<->confirmed
// flip, or a size change moves both; legacy writers (replace/touch) always move both.
void testTxStructureRevision()
{
auto row = [](const char* txid, const char* type, double amount, std::int64_t ts, int conf) {
dragonx::TransactionInfo t;
t.txid = txid; t.type = type; t.amount = amount; t.timestamp = ts; t.confirmations = conf;
t.address = "zs-a"; t.from_address = ""; t.memo = "m";
return t;
};
const std::vector<dragonx::TransactionInfo> base = {
row("tx-a", "receive", 1.5, 1000, 12),
row("tx-b", "send", -0.25, 1001, 3),
row("tx-c", "mined", 3.0, 1002, 120),
};
EXPECT_TRUE(dragonx::sameTransactionLayout(base[0], base[0]));
{ // confirmation count only: tx_revision +1, structure unchanged, new count stored
dragonx::WalletState state;
state.replaceTransactions(std::vector<dragonx::TransactionInfo>(base));
const std::uint64_t rev = state.tx_revision, srev = state.tx_structure_revision;
auto next = base;
next[0].confirmations += 1;
next[1].confirmations += 5;
EXPECT_TRUE(state.setTransactionsIfChanged(std::move(next)));
EXPECT_EQ(state.tx_revision, rev + 1);
EXPECT_EQ(state.tx_structure_revision, srev);
EXPECT_EQ(state.transactions[1].confirmations, 8);
}
{ // pending (0) -> confirmed (1) flips the sort state: structural
dragonx::WalletState state;
auto pend = base;
pend[1].confirmations = 0;
state.replaceTransactions(std::move(pend));
const std::uint64_t rev = state.tx_revision, srev = state.tx_structure_revision;
EXPECT_TRUE(state.setTransactionsIfChanged(std::vector<dragonx::TransactionInfo>(base)));
EXPECT_EQ(state.tx_revision, rev + 1);
EXPECT_EQ(state.tx_structure_revision, srev + 1);
}
{ // a non-confirmation field change, and a size change, are structural
dragonx::WalletState state;
state.replaceTransactions(std::vector<dragonx::TransactionInfo>(base));
std::uint64_t srev = state.tx_structure_revision;
auto next = base;
next[2].memo = "changed";
EXPECT_TRUE(state.setTransactionsIfChanged(std::move(next)));
EXPECT_EQ(state.tx_structure_revision, srev + 1);
srev = state.tx_structure_revision;
auto shorter = base;
shorter.pop_back();
EXPECT_TRUE(state.setTransactionsIfChanged(std::move(shorter)));
EXPECT_EQ(state.tx_structure_revision, srev + 1);
}
{ // identical list: neither counter moves; legacy touch moves both
dragonx::WalletState state;
state.replaceTransactions(std::vector<dragonx::TransactionInfo>(base));
const std::uint64_t rev = state.tx_revision, srev = state.tx_structure_revision;
EXPECT_FALSE(state.setTransactionsIfChanged(std::vector<dragonx::TransactionInfo>(base)));
EXPECT_EQ(state.tx_revision, rev);
EXPECT_EQ(state.tx_structure_revision, srev);
state.touchTransactions();
EXPECT_EQ(state.tx_revision, rev + 1);
EXPECT_EQ(state.tx_structure_revision, srev + 1);
}
}
void testHistoryTxidGroups()
{
using dragonx::ui::history::groupByTxid;
@@ -19432,6 +19497,7 @@ int main()
testLiteRefreshModelAppliesToWalletState();
testLiteTransactionsAreNewestFirst();
testSameTransactionList();
testTxStructureRevision();
testHistoryTxidGroups();
testHistoryFilterInputs();
testWalletStateTxRevision();