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:
+20
-2
@@ -86,6 +86,15 @@ inline bool sameTransactionInfo(const TransactionInfo& a, const TransactionInfo&
|
||||
a.txid == b.txid && a.type == b.type && a.address == b.address &&
|
||||
a.from_address == b.from_address && a.memo == b.memo;
|
||||
}
|
||||
// Same row except the confirmation COUNT: the pending (confirmations==0) state must still match because it drives the
|
||||
// History tab's pending-first sort and shield-leg merge. A delta for which every row passes this changes nothing the
|
||||
// tab's merged/sorted display list is built from, so it can patch confirmations in place instead of rebuilding.
|
||||
inline bool sameTransactionLayout(const TransactionInfo& a, const TransactionInfo& b) {
|
||||
return a.timestamp == b.timestamp && (a.confirmations == 0) == (b.confirmations == 0) &&
|
||||
std::memcmp(&a.amount, &b.amount, sizeof(double)) == 0 &&
|
||||
a.txid == b.txid && a.type == b.type && a.address == b.address &&
|
||||
a.from_address == b.from_address && a.memo == b.memo;
|
||||
}
|
||||
inline bool sameTransactionList(const std::vector<TransactionInfo>& a, const std::vector<TransactionInfo>& b) {
|
||||
if (a.size() != b.size()) return false;
|
||||
for (std::size_t i = 0; i < a.size(); ++i)
|
||||
@@ -370,12 +379,21 @@ struct WalletState {
|
||||
// touchTransactions() afterwards. UI caches (History tab, ...) key on this, NOT on last_tx_update (a
|
||||
// wall-clock refresh stamp that lite never sets) nor App::tx_dirty_generation_ (a refresh-needed flag).
|
||||
std::uint64_t tx_revision = 1;
|
||||
void touchTransactions() { ++tx_revision; }
|
||||
// Bumps with tx_revision EXCEPT for a pure confirmation-count change (see sameTransactionLayout): the History tab's
|
||||
// O(N) merge+sort cache keys on this and just patches confirmations when only tx_revision moved. Every writer that
|
||||
// is not setTransactionsIfChanged goes through touchTransactions()/replaceTransactions() and bumps both (safe default).
|
||||
std::uint64_t tx_structure_revision = 1;
|
||||
void touchTransactions() { ++tx_revision; ++tx_structure_revision; }
|
||||
void replaceTransactions(std::vector<TransactionInfo>&& next) { transactions = std::move(next); touchTransactions(); }
|
||||
// Replace only when the content differs; returns true (and bumps) if it did. Keeps the old list when identical.
|
||||
bool setTransactionsIfChanged(std::vector<TransactionInfo>&& next) {
|
||||
if (sameTransactionList(transactions, next)) return false;
|
||||
replaceTransactions(std::move(next));
|
||||
bool layoutSame = transactions.size() == next.size();
|
||||
for (std::size_t i = 0; layoutSame && i < next.size(); ++i)
|
||||
layoutSame = sameTransactionLayout(transactions[i], next[i]);
|
||||
transactions = std::move(next);
|
||||
++tx_revision;
|
||||
if (!layoutSame) ++tx_structure_revision;
|
||||
return true;
|
||||
}
|
||||
int64_t last_peer_update = 0;
|
||||
|
||||
@@ -219,10 +219,11 @@ void RenderTransactionsTab(App* app)
|
||||
// txid grouping shared by the summary pass and the display pass. Indices only, rebuilt once per tx_revision
|
||||
// (a sort-combo change re-sorts without regrouping).
|
||||
static history::TxidGroups s_groups;
|
||||
static std::uint64_t s_groups_key = 0; // 0 never matches: tx_revision starts at 1 and only grows
|
||||
if (s_groups_key != contentKey) {
|
||||
static std::uint64_t s_groups_key = 0; // 0 never matches: tx_structure_revision starts at 1 and only grows
|
||||
const std::uint64_t structKey = state.tx_structure_revision; // ignores pure confirmation-count bumps
|
||||
if (s_groups_key != structKey) {
|
||||
s_groups = history::groupByTxid(state.transactions);
|
||||
s_groups_key = contentKey;
|
||||
s_groups_key = structKey;
|
||||
}
|
||||
|
||||
// Summary-card totals: Received / Sent / Mined (counts + amounts). These are three static
|
||||
@@ -415,13 +416,14 @@ void RenderTransactionsTab(App* app)
|
||||
// case while the user reads/scrolls) the cache is reused. The result is already sorted newest-first by
|
||||
// the refresh service, but we re-sort here to apply the "pending first" ordering.
|
||||
static std::vector<DisplayTx> s_display_cache;
|
||||
static std::uint64_t s_display_cache_key = 0; // tx_revision the cache was built from
|
||||
static std::uint64_t s_display_cache_key = 0; // tx_structure_revision the cache was built from
|
||||
static std::uint64_t s_display_conf_rev = 0; // tx_revision the cached confirmations were last synced to
|
||||
static int s_display_cache_sort = 0; // sort mode the cache was built with
|
||||
static bool s_display_cache_valid = false;
|
||||
static std::uint64_t s_display_gen = 0; // bumped on every rebuild; keys the filtered-index cache below
|
||||
|
||||
// Two separate fields, not a mixed hash: small revision/sort values would collide under xor-mixing.
|
||||
if (!s_display_cache_valid || contentKey != s_display_cache_key || s_sort_mode != s_display_cache_sort) {
|
||||
if (!s_display_cache_valid || structKey != s_display_cache_key || s_sort_mode != s_display_cache_sort) {
|
||||
++s_display_gen;
|
||||
s_display_cache.clear();
|
||||
std::vector<DisplayTx>& display_txns = s_display_cache;
|
||||
@@ -524,9 +526,27 @@ void RenderTransactionsTab(App* app)
|
||||
}
|
||||
});
|
||||
|
||||
s_display_cache_key = contentKey;
|
||||
s_display_cache_key = structKey;
|
||||
s_display_conf_rev = contentKey; // freshly built from the current list
|
||||
s_display_cache_sort = s_sort_mode;
|
||||
s_display_cache_valid = true;
|
||||
} else if (s_display_conf_rev != contentKey) {
|
||||
// Confirmation-count-only delta (setTransactionsIfChanged kept tx_structure_revision): the merged rows, their
|
||||
// order and the indices into state.transactions are unchanged, so refresh just the count (same rule as the
|
||||
// build above: a shield shows the larger of its two legs). Sort order cannot change: mode 0 only reads
|
||||
// conf==0, which the layout check holds constant, and the other modes ignore confirmations.
|
||||
for (DisplayTx& dtx : s_display_cache) {
|
||||
if (dtx.is_shield) {
|
||||
dtx.confirmations = std::max(state.transactions[dtx.send_idx].confirmations,
|
||||
state.transactions[dtx.recv_idx].confirmations);
|
||||
} else {
|
||||
dtx.confirmations = state.transactions[dtx.orig_idx].confirmations;
|
||||
}
|
||||
}
|
||||
// The filtered-index cache keys on s_display_gen (unchanged, correct: filters ignore confirmations) and
|
||||
// the snapshot's indices remain valid; only re-stamp it so 'export current filter' stays usable.
|
||||
if (s_filter_snapshot.valid) s_filter_snapshot.txRevision = contentKey;
|
||||
s_display_conf_rev = contentKey;
|
||||
}
|
||||
|
||||
// The merged + sorted list (rebuilt above only on change). Pagination below runs per-frame (it only
|
||||
|
||||
Reference in New Issue
Block a user