perf: memoize per-frame render hot paths (console, transactions, recent lists)

Immediate-mode render functions re-run every frame; these rebuilt O(N)
state each time even when nothing changed. From a 6-lens perf audit, each
finding verified on a hot-path basis:

- Console: ConsoleModel gains revision(); the full-model filter scan and the
  glyph-by-glyph text-layout pass (BuildConsoleLayout) rebuild only when the
  model / filter / wrap-width / zoom change — previously it re-shaped up to
  10,000 lines every frame even when idle/scrolled. clear() force-invalidates
  the memo mid-render (no OOB on the just-emptied visible set).
- Transactions: the summary-card totals memoize behind the tab's existing
  FNV-1a fingerprint (also folds away a now-duplicate O(N) display-key pass).
- Send / Receive recent lists: early-exit the prefix scan (state.transactions
  is kept newest-first) instead of filtering the whole tx history every frame.
- network_refresh_service: O(new x total) txid find-and-replace -> hash map.
- Sidebar unconfirmed-tx badge cached on last_tx_update + tx count; the
  daemon-memory probe (/proc scan on Linux, popen on macOS) throttled to ~1.5s.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-01 22:55:04 -05:00
co-authored by Claude Opus 4.8
parent 0a042df8e0
commit d9fa00bb38
9 changed files with 238 additions and 104 deletions
+28 -16
View File
@@ -1108,6 +1108,22 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectRe
? &hushChatReceivedOutputs
: nullptr;
// Index result.transactions by (txid, type) once so the two replace-loops below
// can find-and-replace in O(1) instead of a nested linear scan over the full
// (potentially thousands-large) tx list on every 'recent' refresh cycle. The map
// holds the index of the FIRST occurrence of each key (preserving the linear
// scan's break-on-first-match), and is kept in sync on every append so a later
// entry in the same cycle still finds an earlier appended one — exactly as the
// re-scanned vector did before.
auto txKey = [](const TransactionInfo& tx) {
return tx.txid + '\x1f' + tx.type;
};
std::unordered_map<std::string, std::size_t> byKey;
byKey.reserve(result.transactions.size());
for (std::size_t i = 0; i < result.transactions.size(); ++i) {
byKey.emplace(txKey(result.transactions[i]), i); // keep first-occurrence index
}
try {
std::set<std::string> recentTxids;
std::vector<TransactionInfo> recentTransactions;
@@ -1115,15 +1131,13 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectRe
appendTransparentTransactions(recentTransactions, recentTxids, transactions, snapshot.miningAddresses);
for (auto& recent : recentTransactions) {
bool replaced = false;
for (auto& existing : result.transactions) {
if (existing.txid == recent.txid && existing.type == recent.type) {
existing = recent;
replaced = true;
break;
}
auto it = byKey.find(txKey(recent));
if (it != byKey.end()) {
result.transactions[it->second] = recent;
} else {
byKey.emplace(txKey(recent), result.transactions.size());
result.transactions.push_back(std::move(recent));
}
if (!replaced) result.transactions.push_back(std::move(recent));
}
} catch (const std::exception& e) {
DEBUG_LOGF("recent listtransactions error: %s\n", e.what());
@@ -1149,15 +1163,13 @@ NetworkRefreshService::TransactionRefreshResult NetworkRefreshService::collectRe
snapshot.miningAddresses,
hushChatReceivedOutputsPtr);
for (auto& scanned : scannedTransactions) {
bool replaced = false;
for (auto& existing : result.transactions) {
if (existing.txid == scanned.txid && existing.type == scanned.type) {
existing = scanned;
replaced = true;
break;
}
auto it = byKey.find(txKey(scanned));
if (it != byKey.end()) {
result.transactions[it->second] = scanned;
} else {
byKey.emplace(txKey(scanned), result.transactions.size());
result.transactions.push_back(std::move(scanned));
}
if (!replaced) result.transactions.push_back(std::move(scanned));
}
if (currentBlockHeight >= 0) result.shieldedScanHeights[address] = currentBlockHeight;
++result.shieldedAddressesScanned;