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

@@ -8,6 +8,7 @@
#include "../../app.h"
#include "../../util/i18n.h"
#include "../../util/platform.h"
#include "../../util/text_format.h"
#include "../../config/settings.h"
#include "../../config/version.h"
#include "../theme.h"
@@ -45,12 +46,7 @@ static std::string truncateString(const std::string& str, int maxLen = 16) {
// Case-insensitive string search
static bool containsIgnoreCase(const std::string& str, const std::string& search) {
if (search.empty()) return true;
std::string str_lower = str;
std::string search_lower = search;
std::transform(str_lower.begin(), str_lower.end(), str_lower.begin(), ::tolower);
std::transform(search_lower.begin(), search_lower.end(), search_lower.begin(), ::tolower);
return str_lower.find(search_lower) != std::string::npos;
return dragonx::util::containsIgnoreCase(str, search);
}
// A display-ready transaction that may be a merged autoshield pair.
@@ -81,17 +77,9 @@ std::string DisplayTx::getTimeString() const {
return buf;
}
// Relative time string
// Relative time string (localized long form, e.g. "5 minutes ago")
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;
char buf[32];
if (diff < 60) { snprintf(buf, sizeof(buf), TR("time_seconds_ago"), (long long)diff); return buf; }
if (diff < 3600) { snprintf(buf, sizeof(buf), TR("time_minutes_ago"), (long long)(diff / 60)); return buf; }
if (diff < 86400) { snprintf(buf, sizeof(buf), TR("time_hours_ago"), (long long)(diff / 3600)); return buf; }
snprintf(buf, sizeof(buf), TR("time_days_ago"), (long long)(diff / 86400)); return buf;
return dragonx::util::formatTimeAgo(timestamp);
}
// Draw a small transaction-type icon