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

65
src/util/text_format.cpp Normal file
View File

@@ -0,0 +1,65 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "text_format.h"
#include "i18n.h"
#include <cctype>
#include <cstdio>
#include <ctime>
namespace dragonx {
namespace util {
namespace {
std::int64_t secondsAgo(std::int64_t timestamp)
{
std::int64_t now = static_cast<std::int64_t>(std::time(nullptr));
std::int64_t diff = now - timestamp;
return diff < 0 ? 0 : diff;
}
} // namespace
std::string formatTimeAgo(std::int64_t timestamp)
{
if (timestamp <= 0) return {};
const std::int64_t diff = secondsAgo(timestamp);
char buf[48];
if (diff < 60) std::snprintf(buf, sizeof(buf), tr("time_seconds_ago"), static_cast<int>(diff));
else if (diff < 3600) std::snprintf(buf, sizeof(buf), tr("time_minutes_ago"), static_cast<int>(diff / 60));
else if (diff < 86400) std::snprintf(buf, sizeof(buf), tr("time_hours_ago"), static_cast<int>(diff / 3600));
else std::snprintf(buf, sizeof(buf), tr("time_days_ago"), static_cast<int>(diff / 86400));
return buf;
}
std::string formatTimeAgoShort(std::int64_t timestamp)
{
if (timestamp <= 0) return {};
const std::int64_t diff = secondsAgo(timestamp);
char buf[32];
if (diff < 60) std::snprintf(buf, sizeof(buf), "%llds ago", static_cast<long long>(diff));
else if (diff < 3600) std::snprintf(buf, sizeof(buf), "%lldm ago", static_cast<long long>(diff / 60));
else if (diff < 86400) std::snprintf(buf, sizeof(buf), "%lldh ago", static_cast<long long>(diff / 3600));
else std::snprintf(buf, sizeof(buf), "%lldd ago", static_cast<long long>(diff / 86400));
return buf;
}
bool containsIgnoreCase(std::string_view haystack, std::string_view needle)
{
if (needle.empty()) return true;
if (needle.size() > haystack.size()) return false;
const auto low = [](char c) { return static_cast<char>(std::tolower(static_cast<unsigned char>(c))); };
const std::size_t last = haystack.size() - needle.size();
for (std::size_t i = 0; i <= last; ++i) {
std::size_t j = 0;
for (; j < needle.size(); ++j) {
if (low(haystack[i + j]) != low(needle[j])) break;
}
if (j == needle.size()) return true;
}
return false;
}
} // namespace util
} // namespace dragonx

28
src/util/text_format.h Normal file
View File

@@ -0,0 +1,28 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// Small shared text helpers used across the immediate-mode UI. Centralised here so the per-frame
// hot paths use one allocation-free implementation instead of several copies.
#pragma once
#include <cstdint>
#include <string>
#include <string_view>
namespace dragonx {
namespace util {
// Localized relative time, e.g. "5 minutes ago" (via i18n). Empty when timestamp <= 0.
std::string formatTimeAgo(std::int64_t timestamp);
// Compact, non-localized relative time for tight UI: "5s ago" / "3m ago" / "2h ago" / "4d ago".
std::string formatTimeAgoShort(std::int64_t timestamp);
// Case-insensitive substring test that does NOT allocate (unlike lower-casing copies + find).
// An empty needle matches everything. Safe for per-frame filtering of large lists.
bool containsIgnoreCase(std::string_view haystack, std::string_view needle);
} // namespace util
} // namespace dragonx