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
+37 -5
View File
@@ -820,11 +820,23 @@ void ConsoleTab::renderOutput()
// segment records which bytes of the source text appear on that visual row, so
// hit-testing and selection highlight can map screen positions to exact char offsets.
float wrap_width = ClampConsoleWrapWidth(ImGui::GetContentRegionAvail().x, padX);
ImFontConsoleMeasure measure(ImGui::GetFont(), ImGui::GetFontSize());
layout_ = BuildConsoleLayout(
static_cast<int>(visible_indices_.size()),
[this](int vi) -> const std::string& { return model_[visible_indices_[vi]].text; },
wrap_width, line_height, interLineGap, measure);
// Memoize the text-shaping pass: rebuild only when the visible set, wrap width, line height, gap or
// zoom changed. Otherwise this re-wrapped and re-measured every visible line (glyph-by-glyph) every
// frame, even while idle/scrolled. layout_ is a member, so the cached geometry stays valid for
// drawVisibleLines / screenToTextPos when we skip the rebuild.
std::uint64_t layoutKey = vis_generation_ * 1000003ull;
layoutKey = layoutKey * 131ull + static_cast<std::uint64_t>(wrap_width * 16.0f);
layoutKey = layoutKey * 131ull + static_cast<std::uint64_t>(line_height * 16.0f);
layoutKey = layoutKey * 131ull + static_cast<std::uint64_t>(interLineGap * 16.0f);
layoutKey = layoutKey * 131ull + static_cast<std::uint64_t>(s_console_zoom * 1000.0f);
if (layoutKey != layout_key_) {
ImFontConsoleMeasure measure(ImGui::GetFont(), ImGui::GetFontSize());
layout_ = BuildConsoleLayout(
static_cast<int>(visible_indices_.size()),
[this](int vi) -> const std::string& { return model_[visible_indices_[vi]].text; },
wrap_width, line_height, interLineGap, measure);
layout_key_ = layoutKey;
}
// Mouse/keyboard interaction (wheel-up detach, selection drag, Ctrl+C/A). Raw IO bypasses
// the child window's event consumption.
@@ -891,6 +903,20 @@ void ConsoleTab::renderOutput()
void ConsoleTab::computeVisibleLines(bool& hasTextFilter, std::string& filterLower)
{
// Memoize: rebuild the visible set only when the model changed or the filter state changed.
// Otherwise this scanned the entire (up to 10k-line) model with a per-line filter predicate every
// frame. The out-params + filter_match_count_/folding_active_ are members that stay valid until the
// key moves, so an early return leaves last frame's (still-correct) results in place.
std::uint64_t visKey = model_.revision() * 1000003ull;
for (const char* p = filter_text_; *p; ++p) visKey = visKey * 131ull + static_cast<unsigned char>(*p);
visKey = visKey * 2ull + (s_daemon_messages_enabled ? 1u : 0u);
visKey = visKey * 2ull + (s_errors_only_enabled ? 1u : 0u);
visKey = visKey * 2ull + (s_rpc_trace_enabled ? 1u : 0u);
visKey = visKey * 2ull + (s_app_messages_enabled ? 1u : 0u);
if (visKey == vis_key_) return; // nothing that affects the visible set changed
vis_key_ = visKey;
++vis_generation_; // the layout pass keys off this
ConsoleOutputFilter outputFilter{filter_text_, s_daemon_messages_enabled,
s_errors_only_enabled, s_rpc_trace_enabled,
s_app_messages_enabled};
@@ -2079,6 +2105,12 @@ void ConsoleTab::clear()
// line indices) here to avoid an out-of-bounds crash. computeVisibleLines() rebuilds them next frame.
visible_indices_.clear();
selection_.clear();
// Force both memoized passes to rebuild: renderOutput() runs later THIS frame against the now-empty
// visible_indices_, and computeVisibleLines() recomputes next frame — without these resets the layout
// memo would skip the rebuild and keep stale geometry for the just-emptied set.
vis_key_ = ~0ull;
layout_key_ = ~0ull;
++vis_generation_;
stop_confirm_pending_ = false; // a pending 'stop' confirmation is cancelled by clearing
addLine(TR("console_cleared"), ConsoleChannel::Info);
}