perf(chat): use reentrant local time for chat timestamps
The open chat thread called std::localtime at least twice per message per frame (two day keys for grouping), and more for day separators and time labels. On glibc every call re-checks the timezone with a stat of /etc/localtime. The chat time helpers now use util::localTime() from the new util/local_time.h: localtime_r (localtime_s on Windows), with no per-call timezone check and no shared static result buffer. glibc's localtime_r reads the timezone only once, so local_time.h also adds util::refreshTimezone() (tzset) for caches to call when they rebuild. Nothing calls it yet; its first caller is the chat thread's grouping-flags cache in the next commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -713,6 +713,7 @@ set(APP_HEADERS
|
||||
src/util/serial_job_lane.h
|
||||
src/util/fetch_backoff.h
|
||||
src/util/coalescing_writer.h
|
||||
src/util/local_time.h
|
||||
src/util/background_sampler.h
|
||||
src/util/amount_format.h
|
||||
src/util/base64.h
|
||||
|
||||
+17
-21
@@ -10,6 +10,7 @@
|
||||
#include "../../data/address_book.h"
|
||||
#include "../../chat/chat_service.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../util/local_time.h" // reentrant localtime (chat day keys / time labels)
|
||||
#include "../../util/address_validation.h" // isShieldedAddress — chat requires a z-address recipient
|
||||
#include "../../util/platform.h" // getConfigDir + writeFileAtomically — conversation export (Q11)
|
||||
#include "../../config/settings.h" // per-conversation mute (Q10)
|
||||
@@ -134,11 +135,10 @@ std::string shorten(const std::string& s, std::size_t head = 12, std::size_t tai
|
||||
|
||||
std::string formatTime(std::int64_t ts) {
|
||||
if (ts <= 0) return "";
|
||||
std::time_t t = static_cast<std::time_t>(ts);
|
||||
std::tm* tm = std::localtime(&t); // UI thread only
|
||||
if (!tm) return "";
|
||||
std::tm tmv;
|
||||
if (!util::localTime(static_cast<std::time_t>(ts), tmv)) return "";
|
||||
char buf[40];
|
||||
std::strftime(buf, sizeof(buf), s_chat_time_12h ? "%Y-%m-%d %I:%M %p" : "%Y-%m-%d %H:%M", tm);
|
||||
std::strftime(buf, sizeof(buf), s_chat_time_12h ? "%Y-%m-%d %I:%M %p" : "%Y-%m-%d %H:%M", &tmv);
|
||||
return buf;
|
||||
}
|
||||
|
||||
@@ -146,21 +146,19 @@ std::string formatTime(std::int64_t ts) {
|
||||
// now lives in a per-day separator, so the group header doesn't repeat it). UI thread only.
|
||||
std::string chatTimeOnly(std::int64_t ts) {
|
||||
if (ts <= 0) return "";
|
||||
std::time_t t = static_cast<std::time_t>(ts);
|
||||
std::tm* tm = std::localtime(&t);
|
||||
if (!tm) return "";
|
||||
std::tm tmv;
|
||||
if (!util::localTime(static_cast<std::time_t>(ts), tmv)) return "";
|
||||
char buf[24];
|
||||
std::strftime(buf, sizeof(buf), s_chat_time_12h ? "%I:%M %p" : "%H:%M", tm);
|
||||
std::strftime(buf, sizeof(buf), s_chat_time_12h ? "%I:%M %p" : "%H:%M", &tmv);
|
||||
return buf;
|
||||
}
|
||||
|
||||
// Local calendar-day key (YYYYMMDD) for detecting day boundaries between messages. UI thread only.
|
||||
std::int64_t chatDayKey(std::int64_t ts) {
|
||||
std::time_t t = static_cast<std::time_t>(ts);
|
||||
std::tm* tm = std::localtime(&t);
|
||||
if (!tm) return -1;
|
||||
return static_cast<std::int64_t>(tm->tm_year + 1900) * 10000 +
|
||||
static_cast<std::int64_t>(tm->tm_mon + 1) * 100 + tm->tm_mday;
|
||||
std::tm tmv;
|
||||
if (!util::localTime(static_cast<std::time_t>(ts), tmv)) return -1;
|
||||
return static_cast<std::int64_t>(tmv.tm_year + 1900) * 10000 +
|
||||
static_cast<std::int64_t>(tmv.tm_mon + 1) * 100 + tmv.tm_mday;
|
||||
}
|
||||
|
||||
// Date-separator label: "Today" / "Yesterday" / "Jul 16, 2026". UI thread only.
|
||||
@@ -168,11 +166,10 @@ std::string chatDaySeparator(std::int64_t ts, std::int64_t todayKey, std::int64_
|
||||
const std::int64_t k = chatDayKey(ts);
|
||||
if (k == todayKey) return TR("chat_today");
|
||||
if (k == yestKey) return TR("chat_yesterday");
|
||||
std::time_t t = static_cast<std::time_t>(ts);
|
||||
std::tm* tm = std::localtime(&t);
|
||||
if (!tm) return "";
|
||||
std::tm tmv;
|
||||
if (!util::localTime(static_cast<std::time_t>(ts), tmv)) return "";
|
||||
char buf[32];
|
||||
std::strftime(buf, sizeof(buf), "%b %d, %Y", tm);
|
||||
std::strftime(buf, sizeof(buf), "%b %d, %Y", &tmv);
|
||||
return buf;
|
||||
}
|
||||
|
||||
@@ -185,11 +182,10 @@ std::string relativeTime(std::int64_t ts) {
|
||||
if (d < 3600) return std::to_string(d / 60) + "m";
|
||||
if (d < 86400) return std::to_string(d / 3600) + "h";
|
||||
if (d < 7 * 86400) return std::to_string(d / 86400) + "d";
|
||||
std::time_t t = static_cast<std::time_t>(ts);
|
||||
std::tm* tm = std::localtime(&t);
|
||||
if (!tm) return "";
|
||||
std::tm tmv;
|
||||
if (!util::localTime(static_cast<std::time_t>(ts), tmv)) return "";
|
||||
char buf[16];
|
||||
std::strftime(buf, sizeof(buf), "%b %d", tm);
|
||||
std::strftime(buf, sizeof(buf), "%b %d", &tmv);
|
||||
return buf;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// DragonX Wallet - reentrant local-time helper (no per-call stat of /etc/localtime on glibc).
|
||||
#pragma once
|
||||
#include <ctime>
|
||||
namespace dragonx::util {
|
||||
// Fills `out` with the local broken-down time of `t`. Returns false on failure. Thread-safe.
|
||||
inline bool localTime(std::time_t t, std::tm& out) {
|
||||
#ifdef _WIN32
|
||||
return localtime_s(&out, &t) == 0;
|
||||
#else
|
||||
return localtime_r(&t, &out) != nullptr;
|
||||
#endif
|
||||
}
|
||||
// Re-read the TZ database / TZ env (glibc localtime_r does not do this on its own after the first call).
|
||||
// Call once per cache rebuild, never per item.
|
||||
inline void refreshTimezone() {
|
||||
#ifdef _WIN32
|
||||
_tzset();
|
||||
#else
|
||||
tzset();
|
||||
#endif
|
||||
}
|
||||
} // namespace dragonx::util
|
||||
@@ -64,6 +64,7 @@
|
||||
#include "data/seed_migration_resume.h"
|
||||
#include "util/address_validation.h"
|
||||
#include "util/seed_phrase.h"
|
||||
#include "util/local_time.h"
|
||||
#include "util/image_decode_core.h"
|
||||
#include "daemon/daemon_startup_diagnosis.h"
|
||||
#include "util/amount_format.h"
|
||||
@@ -14408,6 +14409,26 @@ void testConsolidationRunSimulation()
|
||||
EXPECT_TRUE(runVerdict(sum) == RunVerdict::Clean);
|
||||
}
|
||||
|
||||
namespace {
|
||||
void testLocalTime()
|
||||
{
|
||||
#ifndef _WIN32
|
||||
const char* prev = std::getenv("TZ");
|
||||
const bool hadTz = prev != nullptr;
|
||||
const std::string savedTz = hadTz ? prev : "";
|
||||
setenv("TZ", "XXX-3", 1); dragonx::util::refreshTimezone(); // POSIX TZ: UTC+3, no tzdata needed
|
||||
std::tm t{};
|
||||
EXPECT_TRUE(dragonx::util::localTime(0, t));
|
||||
EXPECT_EQ(t.tm_year, 70); EXPECT_EQ(t.tm_mday, 1); EXPECT_EQ(t.tm_hour, 3);
|
||||
setenv("TZ", "YYY5", 1); dragonx::util::refreshTimezone(); // UTC-5: refreshTimezone() must pick it up
|
||||
EXPECT_TRUE(dragonx::util::localTime(0, t));
|
||||
EXPECT_EQ(t.tm_mday, 31); EXPECT_EQ(t.tm_hour, 19);
|
||||
if (hadTz) setenv("TZ", savedTz.c_str(), 1); else unsetenv("TZ");
|
||||
dragonx::util::refreshTimezone();
|
||||
#endif
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
testConnectionConfig();
|
||||
@@ -14644,6 +14665,7 @@ int main()
|
||||
testImageDecodeCore();
|
||||
testImageDecodeJobs();
|
||||
testGeneratedResourceBehavior();
|
||||
testLocalTime();
|
||||
|
||||
if (g_failures != 0) {
|
||||
std::cerr << g_failures << " test assertion(s) failed\n";
|
||||
|
||||
Reference in New Issue
Block a user