diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp index 5e0dda0..b0051da 100644 --- a/src/ui-touch/UITask.cpp +++ b/src/ui-touch/UITask.cpp @@ -41914,6 +41914,7 @@ bool UITask::loadMsgsFromStorage() { f.close(); _ui_msg_count = 0; _ui_msg_head = 0; + _thread_hist_dirty = true; // ring replaced — recompute "has history" on next read _msgcount = static_cast(hdr.msgcount); return true; } @@ -41954,6 +41955,7 @@ bool UITask::loadMsgsFromStorage() { _ui_msg_count = n_keep; // ring is now linear: slots 0..n_keep-1, oldest first _ui_msg_head = n_keep % _ui_msg_cap; + _thread_hist_dirty = true; // ring replaced — recompute "has history" on next read _msgcount = static_cast(hdr.msgcount); return true; #else @@ -42075,6 +42077,7 @@ bool UITask::loadMsgsFromSegments() { _ui_msg_count = n_keep; _ui_msg_head = n_keep % _ui_msg_cap; + _thread_hist_dirty = true; // ring replaced — recompute "has history" on next read _ui_seq_next = max_seq + 1; s_seg_flushed_seq = max_seq; // everything loaded IS on disk by definition // _msgcount isn't stored in segments; the threads-file header carries it and @@ -43115,6 +43118,7 @@ int UITask::findOrCreateThread(const char* name, bool channel) { bool UITask::removeThread(int idx) { if (idx < 0 || idx >= MAX_UI_THREADS) return false; if (!_ui_threads[idx].used) return false; + _thread_hist_dirty = true; // slot is being freed + its ring records dropped // Match the thread by stored name (channel/DM) before wiping the slot so // we can drop any ring messages that belonged to it. Without this the // entries would still occupy the bounded _ui_msgs ring and confuse a @@ -43387,6 +43391,11 @@ int UITask::appendMessage(const char* thread, const char* sender, const char* te m.sender[MAX_SENDER_NAME] = '\0'; strncpy(m.text, text ? text : "", MAX_MSG_TEXT); m.text[MAX_MSG_TEXT] = '\0'; + // An append is the ONLY mutation that needs no scan: this thread now demonstrably has + // history. A full ring also EVICTED the oldest record above, which may have been some other + // thread's last message, so the cached flags have to be rebuilt before they are trusted again. + if (_ui_msg_count == _ui_msg_cap) _thread_hist_dirty = true; + _thread_hist[t_idx] = true; if (_ui_msg_count < _ui_msg_cap) ++_ui_msg_count; _ui_msg_head = (_ui_msg_head + 1) % _ui_msg_cap; _ui_threads[t_idx].last_ts = m.ts; @@ -44651,10 +44660,33 @@ int UITask::getThreadCount(bool channel_mode, int out_indexes[], int max_out) co return copy_n; } +// One ring pass that answers "has any message?" for EVERY thread at once (see the note on +// _thread_hist). Stops as soon as every live thread is accounted for, so a device whose +// threads all have recent traffic barely reads the ring at all. +void UITask::rebuildThreadHistoryFlags() const { + for (int i = 0; i < MAX_UI_THREADS; ++i) _thread_hist[i] = false; + int unresolved = 0; + for (int i = 0; i < MAX_UI_THREADS; ++i) if (_ui_threads[i].used) ++unresolved; + for (int i = 0; i < _ui_msg_count && unresolved > 0; ++i) { + const int idx = (_ui_msg_head - 1 - i + _ui_msg_cap) % _ui_msg_cap; + const UIMessage& m = _ui_msgs[idx]; + for (int t = 0; t < MAX_UI_THREADS; ++t) { + if (!_ui_threads[t].used || _thread_hist[t]) continue; + if (m.channel == _ui_threads[t].channel && + strncmp(m.thread, _ui_threads[t].name, MAX_THREAD_NAME) == 0) { + _thread_hist[t] = true; + --unresolved; + break; + } + } + } + _thread_hist_dirty = false; +} + bool UITask::threadHasMessageHistory(int thread_idx) const { if (thread_idx < 0 || thread_idx >= MAX_UI_THREADS || !_ui_threads[thread_idx].used) return false; - int tmp[8]; - return getThreadMessageIndexes(thread_idx, tmp, 8, false) > 0; + if (_thread_hist_dirty) rebuildThreadHistoryFlags(); + return _thread_hist[thread_idx]; } int UITask::getCombinedInboxCount(int out_indexes[], int max_out) const { @@ -44736,6 +44768,7 @@ int UITask::clearThreadHistory(int thread_idx) { } _ui_threads[thread_idx].unread = 0; _ui_threads[thread_idx].has_mention = false; + _thread_hist[thread_idx] = false; // emptied, and no other thread was touched _threads_dirty = true; if (cleared) _msgs_dirty = true; return cleared; diff --git a/src/ui-touch/UITask.h b/src/ui-touch/UITask.h index a2ec856..b432495 100644 --- a/src/ui-touch/UITask.h +++ b/src/ui-touch/UITask.h @@ -147,6 +147,23 @@ private: * else MAX_UI_MESSAGES. Fixed for the whole boot (chosen before the PSRAM * alloc in begin()); the loader linearizes files written under another cap. */ int _ui_msg_cap = MAX_UI_MESSAGES; + /** "Does this thread have any stored message?", answered in O(1). + * + * threadHasMessageHistory() used to answer it by walking the message ring looking for a + * match, and the inbox/unread paths call it once PER THREAD. A thread that HAS recent + * messages exits that scan early; a thread with none scans every record in the ring doing + * a strncmp each — and "no history" is exactly what those callers are testing for. With a + * busy public channel filling the ring (a field report: 3800 messages) each inbox refresh + * became tens of thousands of string compares, which is the reported "interface slows down + * drastically", and why it was still felt at only ~300 messages. + * + * Appends set the owning thread's flag directly, so the common path never scans. Anything + * that REMOVES messages (ring eviction, delete, clear, a fresh load) can only invalidate + * the flags, so it just marks them dirty and the next reader rebuilds all threads in ONE + * ring pass instead of one pass per thread. */ + mutable bool _thread_hist[MAX_UI_THREADS] = { false }; + mutable bool _thread_hist_dirty = true; + void rebuildThreadHistoryFlags() const; unsigned long _next_thread_seed; UIMessage* _ui_msgs = nullptr; // ring of recent messages — PSRAM-allocated in begin() UIThread* _ui_threads = nullptr; // thread table — PSRAM-allocated in begin()