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:
2026-10-10 09:50:03 -05:00
co-authored by Claude Opus 5.5
parent e45f4c7d90
commit a69380817e
4 changed files with 62 additions and 21 deletions
+17 -21
View File
@@ -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;
}