diff --git a/src/app_network.cpp b/src/app_network.cpp index 62bc5f1..75915cc 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -716,6 +716,11 @@ void App::setCurrentPage(ui::NavPage page) bool App::shouldRefreshTransactions() const { + // NOTE: this is block-height / dirty driven, NOT interval-gated. It returns true only when a new + // block arrived (currentBlocks != last_tx_block_height_), the history was never fetched, or + // something marked it dirty (tab entry, a send, a reorg, etc.). The Transactions timer only + // controls how often this CHECK runs; between blocks the lightweight recent-poll + // (shouldRefreshRecentTransactions / TxAge) handles mempool + unconfirmed deltas instead. const int currentBlocks = state_.sync.blocks; return network_refresh_.shouldRefreshTransactions(last_tx_block_height_, currentBlocks, diff --git a/src/ui/windows/transactions_tab.cpp b/src/ui/windows/transactions_tab.cpp index 1ace8f5..66aa85b 100644 --- a/src/ui/windows/transactions_tab.cpp +++ b/src/ui/windows/transactions_tab.cpp @@ -21,9 +21,11 @@ #include "imgui.h" #include #include +#include #include #include #include +#include namespace dragonx { namespace ui { @@ -369,8 +371,37 @@ void RenderTransactionsTab(App* app) // Two entries sharing the same txid where one is "send" and // one is "receive" to a z-address are combined into a single // "shield" entry. We first index by txid, then build the list. - std::vector display_txns; + // + // This merge + sort is O(N log N) with several heap allocations, so it is MEMOIZED: it only + // rebuilds when the underlying transactions actually change, not every frame. The cache key + // is a cheap, allocation-free FNV-1a fingerprint over the fields that affect the displayed + // rows (count, last update time, and each tx's confirmations / timestamp / type+address + // first char). A new block bumps every confirmation, so the key changes and we rebuild; + // between changes (the common case while the user reads/scrolls) we reuse the cache. The + // result is already sorted newest-first by the refresh service, but we re-sort here to apply + // the "pending first" ordering — also folded into the memoized build. + static std::vector s_display_cache; + static std::uint64_t s_display_cache_key = 0; + static bool s_display_cache_valid = false; + + std::uint64_t displayKey; { + std::uint64_t h = 1469598103934665603ULL; // FNV-1a offset basis + auto mix = [&h](std::uint64_t v) { h = (h ^ v) * 1099511628211ULL; }; + mix(state.transactions.size()); + mix(static_cast(state.last_tx_update)); + for (const auto& t : state.transactions) { + mix(static_cast(t.confirmations)); + mix(static_cast(t.timestamp)); + mix(t.type.empty() ? 0u : static_cast(t.type[0])); + mix(t.address.empty() ? 0u : static_cast(t.address[0])); + } + displayKey = h; + } + + if (!s_display_cache_valid || displayKey != s_display_cache_key) { + s_display_cache.clear(); + std::vector& display_txns = s_display_cache; // Map txid -> indices in state.transactions std::unordered_map> txid_map; for (size_t i = 0; i < state.transactions.size(); i++) { @@ -445,8 +476,16 @@ void RenderTransactionsTab(App* app) if (aPending != bPending) return aPending; return a.timestamp > b.timestamp; }); + + s_display_cache_key = displayKey; + s_display_cache_valid = true; } + // The merged + sorted list (rebuilt above only on change). Filtering/search/pagination below + // run per-frame over this cache — they're cheap (a linear scan) and depend on interactive + // filter/search state, so they are intentionally not memoized. + const std::vector& display_txns = s_display_cache; + // Apply type + search filters std::vector filtered_indices; for (size_t i = 0; i < display_txns.size(); i++) {