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
+13 -8
View File
@@ -370,20 +370,25 @@ static void RenderRecentReceived(const AddressInfo& /* addr */,
S.drawElement("tabs.balance", "recent-tx-icon-size").size * hs);
ImU32 recvCol = Success();
// Collect matching transactions
std::vector<const TransactionInfo*> recvs;
for (const auto& tx : state.transactions) {
if (tx.type != "receive" && tx.type != "mined") continue;
recvs.push_back(&tx);
}
// Grow the list to fill the dead space beneath the (fixed-height) receive card:
// fit as many newest-first rows as the remaining region can show, instead of a
// fixed 4. The child scrolls if the real history exceeds what fits, so nothing is
// lost. A floor of 4 keeps the section substantial when the region is short.
// Compute maxRows BEFORE the collect loop so the scan can stop early (below).
float listH = std::max(rowH, ImGui::GetContentRegionAvail().y);
size_t maxRows = std::max<size_t>(4, (size_t)std::floor(listH / rowH));
if (recvs.size() > maxRows) recvs.resize(maxRows); // newest-first
// Collect matching transactions. state.transactions is newest-first, so the first
// maxRows matches ARE exactly the rows we render — stop scanning once we have them
// instead of filtering the entire history every frame (mirrors RenderSharedRecentTx
// in balance_components.cpp).
std::vector<const TransactionInfo*> recvs;
recvs.reserve(maxRows);
for (const auto& tx : state.transactions) {
if (tx.type != "receive" && tx.type != "mined") continue;
recvs.push_back(&tx);
if (recvs.size() >= maxRows) break; // newest-first: prefix is all we show
}
ImGui::BeginChild("##RecentReceivedRows", ImVec2(width, listH), false,
ImGuiWindowFlags_NoBackground);