perf(ui): dedupe time-ago + allocation-free case-insensitive filter (audit #1-3)

Per-frame hot paths in the immediate-mode UI were allocating needlessly:

- Address filtering in the balance tab rebuilt a std::string filter per address AND
  containsIgnoreCase() lower-cased two fresh copies per call — ~6×N allocations/frame
  on large wallets. New util::containsIgnoreCase(string_view, string_view) is
  allocation-free, and the filter is now built once outside the loop.
- Four duplicated "time ago" implementations (balance_tab_helpers, balance_recent_tx,
  send_tab, transactions_tab) are consolidated into util::formatTimeAgo (localized long
  form) + util::formatTimeAgoShort (compact "5s ago"), preserving each call site's exact
  display style. Both use snprintf, no per-row string concatenation.
- The send-tab address-suggestion scan (a walk over the whole tx list) is memoized on the
  typed text + tx count, so it no longer recomputes every frame while the user pauses.

New src/util/text_format.{h,cpp}; the two existing containsIgnoreCase/timeAgo definitions
now delegate to it. Added to both the app and test targets (test target also gains i18n.cpp,
which text_format's localized path needs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 13:54:27 -05:00
parent 63b3a04716
commit e40962cdf2
8 changed files with 134 additions and 60 deletions

View File

@@ -13,6 +13,7 @@
#include "../../data/wallet_state.h"
#include "../../util/address_validation.h"
#include "../../util/i18n.h"
#include "../../util/text_format.h"
#include "../notifications.h"
#include "../layout.h"
#include "../schema/ui_schema.h"
@@ -163,14 +164,7 @@ static bool IsValidTransparentAddr(const char* a) {
}
static std::string timeAgo(int64_t timestamp) {
if (timestamp <= 0) return "";
int64_t now = (int64_t)std::time(nullptr);
int64_t diff = now - timestamp;
if (diff < 0) diff = 0;
if (diff < 60) return std::to_string(diff) + "s ago";
if (diff < 3600) return std::to_string(diff / 60) + "m ago";
if (diff < 86400) return std::to_string(diff / 3600) + "h ago";
return std::to_string(diff / 86400) + "d ago";
return dragonx::util::formatTimeAgoShort(timestamp);
}
static void DrawTxIcon(ImDrawList* dl, const std::string& type,
@@ -324,14 +318,24 @@ static void RenderAddressSuggestions(const WalletState& state, float width, cons
bool is_valid_t = IsValidTransparentAddr(s_to_address);
if (is_valid_z || is_valid_t) return;
std::vector<std::string> suggestions;
for (const auto& tx : state.transactions) {
if (tx.type != "send" || tx.address.empty()) continue;
if (tx.address.find(partial) != std::string::npos) {
bool dup = false;
for (const auto& s : suggestions) { if (s == tx.address) { dup = true; break; } }
if (!dup) suggestions.push_back(tx.address);
if (suggestions.size() >= (size_t)schema::UI().drawElement("tabs.send", "max-suggestions").size) break;
// Memoize the suggestion scan: it walks the whole transaction list, so rebuilding it every
// frame (the user pauses on a partial address for many frames) is wasted work. Recompute only
// when the typed text or the transaction count changes.
static std::vector<std::string> suggestions;
static std::string s_sugKey;
const std::string sugKey = partial + "#" + std::to_string(state.transactions.size());
if (sugKey != s_sugKey) {
s_sugKey = sugKey;
suggestions.clear();
const size_t maxSug = (size_t)schema::UI().drawElement("tabs.send", "max-suggestions").size;
for (const auto& tx : state.transactions) {
if (tx.type != "send" || tx.address.empty()) continue;
if (tx.address.find(partial) != std::string::npos) {
bool dup = false;
for (const auto& s : suggestions) { if (s == tx.address) { dup = true; break; } }
if (!dup) suggestions.push_back(tx.address);
if (suggestions.size() >= maxSug) break;
}
}
}
if (suggestions.empty()) return;