From df76526b5d8a775eac2d045ea6aa66a2ff4a8698 Mon Sep 17 00:00:00 2001 From: liu weikai Date: Fri, 27 Mar 2026 17:35:47 +0800 Subject: [PATCH] perf: reduce chat list and message handling latency --- apps/esp_idf/src/app_facade_runtime.cpp | 1 + .../src/app_facade_runtime.cpp | 1 + .../include/chat/ports/i_chat_store.h | 7 + .../include/chat/usecase/chat_service.h | 1 + .../core_chat/src/usecase/chat_service.cpp | 5 + .../chat/chat_conversation_components.h | 1 + .../chat/chat_message_list_components.h | 2 + .../ui/screens/chat/chat_ui_controller.h | 8 + .../chat/chat_conversation_components.cpp | 38 ++++ .../chat/chat_message_list_components.cpp | 113 +++++++++++- .../ui/screens/chat/chat_ui_controller.cpp | 169 ++++++++++++++---- .../src/app_runtime_support.cpp | 1 + .../chat/infra/store/internal_fs_store.h | 10 ++ .../chat/infra/store/internal_fs_store.cpp | 64 ++++++- 14 files changed, 376 insertions(+), 45 deletions(-) diff --git a/apps/esp_idf/src/app_facade_runtime.cpp b/apps/esp_idf/src/app_facade_runtime.cpp index c02e4792..583f393d 100644 --- a/apps/esp_idf/src/app_facade_runtime.cpp +++ b/apps/esp_idf/src/app_facade_runtime.cpp @@ -976,6 +976,7 @@ class MinimalAppFacade final : public app::IAppFacade if (chat_service_) { chat_service_->processIncoming(); + chat_service_->flushStore(); } if (team_pairing_) diff --git a/apps/gat562_mesh_evb_pro/src/app_facade_runtime.cpp b/apps/gat562_mesh_evb_pro/src/app_facade_runtime.cpp index 9820cb6d..1394595f 100644 --- a/apps/gat562_mesh_evb_pro/src/app_facade_runtime.cpp +++ b/apps/gat562_mesh_evb_pro/src/app_facade_runtime.cpp @@ -615,6 +615,7 @@ void AppFacadeRuntime::updateCoreServices() if (chat_service_) { chat_service_->processIncoming(); + chat_service_->flushStore(); } if (ble_manager_) { diff --git a/modules/core_chat/include/chat/ports/i_chat_store.h b/modules/core_chat/include/chat/ports/i_chat_store.h index bc054089..5d803f79 100644 --- a/modules/core_chat/include/chat/ports/i_chat_store.h +++ b/modules/core_chat/include/chat/ports/i_chat_store.h @@ -77,6 +77,13 @@ class IChatStore * @return true if updated */ virtual bool updateMessageStatus(MessageId msg_id, MessageStatus status) = 0; + + /** + * @brief Flush pending buffered writes to persistent storage + * + * Default implementation is a no-op for stores that do not buffer. + */ + virtual void flush() {} }; } // namespace chat diff --git a/modules/core_chat/include/chat/usecase/chat_service.h b/modules/core_chat/include/chat/usecase/chat_service.h index 4272f75b..5cb05da9 100644 --- a/modules/core_chat/include/chat/usecase/chat_service.h +++ b/modules/core_chat/include/chat/usecase/chat_service.h @@ -96,6 +96,7 @@ class ChatService * @brief Process incoming messages (call from mesh task) */ void processIncoming(); + void flushStore(); void addIncomingTextObserver(IncomingTextObserver* observer); void removeIncomingTextObserver(IncomingTextObserver* observer); diff --git a/modules/core_chat/src/usecase/chat_service.cpp b/modules/core_chat/src/usecase/chat_service.cpp index 75d40485..c5121616 100644 --- a/modules/core_chat/src/usecase/chat_service.cpp +++ b/modules/core_chat/src/usecase/chat_service.cpp @@ -202,6 +202,11 @@ void ChatService::processIncoming() } } +void ChatService::flushStore() +{ + store_.flush(); +} + void ChatService::addIncomingMessageObserver(IncomingMessageObserver* observer) { if (!observer) diff --git a/modules/ui_shared/include/ui/screens/chat/chat_conversation_components.h b/modules/ui_shared/include/ui/screens/chat/chat_conversation_components.h index ce0b767a..568319b7 100644 --- a/modules/ui_shared/include/ui/screens/chat/chat_conversation_components.h +++ b/modules/ui_shared/include/ui/screens/chat/chat_conversation_components.h @@ -33,6 +33,7 @@ class ChatConversationScreen void addMessage(const chat::ChatMessage& msg); void clearMessages(); void scrollToBottom(); + bool updateMessageStatus(chat::MessageId msg_id, chat::MessageStatus status); void setActionCallback(void (*cb)(ActionIntent intent, void*), void* user_data); bool isAlive() const { return guard_ && guard_->alive; } diff --git a/modules/ui_shared/include/ui/screens/chat/chat_message_list_components.h b/modules/ui_shared/include/ui/screens/chat/chat_message_list_components.h index 8d34f466..be9ef955 100644 --- a/modules/ui_shared/include/ui/screens/chat/chat_message_list_components.h +++ b/modules/ui_shared/include/ui/screens/chat/chat_message_list_components.h @@ -140,6 +140,8 @@ class ChatMessageListScreen message_list::input::Controller input_controller_{}; void rebuildList(); + bool updateListInPlace(const std::vector& convs); + void updateListItem(size_t index, const chat::ConversationMeta& conv); void updateFilterHighlight(); void setFilterMode(FilterMode mode); diff --git a/modules/ui_shared/include/ui/screens/chat/chat_ui_controller.h b/modules/ui_shared/include/ui/screens/chat/chat_ui_controller.h index f39055ab..fafa6522 100644 --- a/modules/ui_shared/include/ui/screens/chat/chat_ui_controller.h +++ b/modules/ui_shared/include/ui/screens/chat/chat_ui_controller.h @@ -75,8 +75,14 @@ class UiController : public IChatUiRuntime void handleChannelSelected(const chat::ConversationId& conv); void handleSendMessage(const std::string& text); void refreshUnreadCounts(); + void refreshUnreadCounts(bool force_reload); void cleanupComposeIme(); bool isTeamConversation(const chat::ConversationId& conv) const; + void syncConversationListFromStore(); + void normalizeConversationNames(std::vector& convs) const; + void applyConversationListToUi(); + void updateConversationMetaForMessage(const chat::ChatMessage& msg, bool increment_unread); + bool updateConversationViewForIncoming(const chat::ChatMessage& msg); void refreshTeamConversation(); void startTeamConversationTimer(); void stopTeamConversationTimer(); @@ -121,6 +127,8 @@ class UiController : public IChatUiRuntime uint64_t key_verify_nonce_ = 0; bool key_verify_expects_number_ = false; bool key_verify_can_trust_ = false; + std::vector cached_conversations_; + bool conversation_list_dirty_ = true; static void team_position_icon_event_cb(lv_event_t* e); static void team_position_cancel_event_cb(lv_event_t* e); diff --git a/modules/ui_shared/src/ui/screens/chat/chat_conversation_components.cpp b/modules/ui_shared/src/ui/screens/chat/chat_conversation_components.cpp index 1ee6dc2d..ee8b9bd7 100644 --- a/modules/ui_shared/src/ui/screens/chat/chat_conversation_components.cpp +++ b/modules/ui_shared/src/ui/screens/chat/chat_conversation_components.cpp @@ -344,6 +344,44 @@ void ChatConversationScreen::scrollToBottom() } } +bool ChatConversationScreen::updateMessageStatus(const chat::MessageId msg_id, + const chat::MessageStatus status) +{ + if (!guard_ || !guard_->alive || msg_id == 0) + { + return false; + } + + for (auto& item : messages_) + { + if (item.msg.msg_id != msg_id) + { + continue; + } + item.msg.status = status; + if (!item.status_label) + { + return true; + } + + if (status == MessageStatus::Failed) + { + lv_label_set_text(item.status_label, "Failed"); + ::ui::fonts::apply_ui_chrome_font(item.status_label); + lv_obj_clear_flag(item.status_label, LV_OBJ_FLAG_HIDDEN); + } + else + { + lv_label_set_text(item.status_label, ""); + ::ui::fonts::apply_ui_chrome_font(item.status_label); + lv_obj_add_flag(item.status_label, LV_OBJ_FLAG_HIDDEN); + } + return true; + } + + return false; +} + void ChatConversationScreen::setActionCallback(void (*cb)(ActionIntent intent, void*), void* user_data) { if (!guard_ || !guard_->alive) diff --git a/modules/ui_shared/src/ui/screens/chat/chat_message_list_components.cpp b/modules/ui_shared/src/ui/screens/chat/chat_message_list_components.cpp index 4507bd73..68daeb16 100644 --- a/modules/ui_shared/src/ui/screens/chat/chat_message_list_components.cpp +++ b/modules/ui_shared/src/ui/screens/chat/chat_message_list_components.cpp @@ -156,6 +156,23 @@ static bool conversation_list_equal(const std::vector& l return true; } +static bool conversation_identity_list_equal(const std::vector& lhs, + const std::vector& rhs) +{ + if (lhs.size() != rhs.size()) + { + return false; + } + for (size_t index = 0; index < lhs.size(); ++index) + { + if (!(lhs[index].id == rhs[index].id)) + { + return false; + } + } + return true; +} + static const char* touch_event_name(lv_event_code_t code) { switch (code) @@ -337,6 +354,7 @@ void ChatMessageListScreen::setConversations(const std::vector previous_convs = convs_; convs_ = convs; if (team_btn_) { @@ -354,7 +372,11 @@ void ChatMessageListScreen::setConversations(const std::vector& convs) +{ + if (!guard_ || !guard_->alive || !list_panel_ || !lv_obj_is_valid(list_panel_)) + { + return false; + } + if (!conversation_identity_list_equal(convs_, convs)) + { + return false; + } + + std::vector filtered; + filtered.reserve(convs.size()); + for (const auto& conv : convs) + { + if (is_team_conversation(conv.id)) + { + if (filter_mode_ == FilterMode::Team) + { + filtered.push_back(conv); + } + continue; + } + if (filter_mode_ == FilterMode::Direct && conv.id.peer != 0) + { + filtered.push_back(conv); + } + else if (filter_mode_ == FilterMode::Broadcast && conv.id.peer == 0) + { + filtered.push_back(conv); + } + } + + if (filtered.size() != items_.size()) + { + return false; + } + + for (size_t index = 0; index < filtered.size(); ++index) + { + if (!(items_[index].conv == filtered[index].id)) + { + return false; + } + } + + for (size_t index = 0; index < filtered.size(); ++index) + { + updateListItem(index, filtered[index]); + } + return true; +} + +void ChatMessageListScreen::updateListItem(const size_t index, + const chat::ConversationMeta& conv) +{ + if (index >= items_.size()) + { + return; + } + + MessageItem& item = items_[index]; + std::string title = "[" + std::string(protocol_short_label(conv.id.protocol)) + "] " + conv.name; + lv_label_set_text(item.name_label, title.c_str()); + ::ui::fonts::apply_chat_content_font(item.name_label, title.c_str()); + + std::string preview = truncate_preview(conv.preview); + lv_label_set_text(item.preview_label, preview.c_str()); + ::ui::fonts::apply_chat_content_font(item.preview_label, preview.c_str()); + + char time_buf[16]; + format_time_hhmm(time_buf, conv.last_timestamp); + lv_label_set_text(item.time_label, time_buf); + ::ui::fonts::apply_ui_chrome_font(item.time_label); + + if (conv.unread > 0) + { + char unread_str[16]; + snprintf(unread_str, sizeof(unread_str), "%d", conv.unread); + lv_label_set_text(item.unread_label, unread_str); + } + else + { + lv_label_set_text(item.unread_label, ""); + } + ::ui::fonts::apply_ui_chrome_font(item.unread_label); + item.unread_count = conv.unread; +} + void ChatMessageListScreen::item_event_cb(lv_event_t* e) { auto* screen = diff --git a/modules/ui_shared/src/ui/screens/chat/chat_ui_controller.cpp b/modules/ui_shared/src/ui/screens/chat/chat_ui_controller.cpp index 32cd0032..4d7c5660 100644 --- a/modules/ui_shared/src/ui/screens/chat/chat_ui_controller.cpp +++ b/modules/ui_shared/src/ui/screens/chat/chat_ui_controller.cpp @@ -333,12 +333,10 @@ void UiController::update() { // Process incoming messages service_.processIncoming(); + service_.flushStore(); - // Refresh UI if needed - if (state_ == State::ChannelList && channel_list_) - { - refreshUnreadCounts(); - } + // Refresh UI only when an event marks the conversation list dirty. + refreshUnreadCounts(false); } void UiController::onChannelClicked(chat::ConversationId conv) @@ -416,19 +414,25 @@ void UiController::onChatEvent(sys::Event* event) // Note: Haptic feedback is now handled by the app runtime event pump // No need to call vibrator() here - if (state_ == State::Conversation && - (uint8_t)current_channel_ == msg_event->channel) + const ChatMessage* latest = service_.getMessage(msg_event->msg_id); + if (latest) { - CHAT_UI_LOG("[UiController::onChatEvent] Updating conversation UI...\n"); - auto messages = service_.getRecentMessages(current_conv_, 50); - conversation_->clearMessages(); - for (const auto& m : messages) + const bool is_current_conversation = + (state_ == State::Conversation) && (current_conv_ == chat::ConversationId(latest->channel, + latest->peer, + latest->protocol)); + updateConversationMetaForMessage(*latest, !is_current_conversation); + if (is_current_conversation) { - conversation_->addMessage(m); + (void)updateConversationViewForIncoming(*latest); + service_.markConversationRead(current_conv_); + } + else + { + conversation_list_dirty_ = true; } - conversation_->scrollToBottom(); } - refreshUnreadCounts(); + refreshUnreadCounts(false); break; } @@ -437,13 +441,17 @@ void UiController::onChatEvent(sys::Event* event) sys::ChatSendResultEvent* result_event = (sys::ChatSendResultEvent*)event; if (state_ == State::Conversation && conversation_) { - auto messages = service_.getRecentMessages(current_conv_, 50); - conversation_->clearMessages(); - for (const auto& m : messages) + const ChatMessage* msg = service_.getMessage(result_event->msg_id); + if (!msg || !conversation_->updateMessageStatus(result_event->msg_id, msg->status)) { - conversation_->addMessage(m); + auto messages = service_.getRecentMessages(current_conv_, 50); + conversation_->clearMessages(); + for (const auto& m : messages) + { + conversation_->addMessage(m); + } + conversation_->scrollToBottom(); } - conversation_->scrollToBottom(); } (void)result_event; break; @@ -451,7 +459,8 @@ void UiController::onChatEvent(sys::Event* event) case sys::EventType::ChatUnreadChanged: { - refreshUnreadCounts(); + conversation_list_dirty_ = true; + refreshUnreadCounts(false); break; } @@ -519,7 +528,7 @@ void UiController::switchToChannelList() } service_.setModelEnabled(true); - refreshUnreadCounts(); + refreshUnreadCounts(true); } void UiController::switchToConversation(chat::ConversationId conv) @@ -767,28 +776,29 @@ void UiController::handleSendMessage(const std::string& text) } void UiController::refreshUnreadCounts() +{ + refreshUnreadCounts(true); +} + +void UiController::refreshUnreadCounts(const bool force_reload) { if (!channel_list_) { return; } - size_t total = 0; - auto convs = service_.getConversations(0, 0, &total); - - // Update conversation names with contact nicknames - for (auto& conv : convs) + if (force_reload || conversation_list_dirty_ || cached_conversations_.empty()) { - if (conv.id.peer != 0) - { - std::string contact_name = app::messagingFacade().getContactService().getContactName(conv.id.peer); - if (!contact_name.empty()) - { - conv.name = contact_name; - } - // Otherwise keep the short_name from ConversationMeta - } + syncConversationListFromStore(); } + applyConversationListToUi(); +} + +void UiController::syncConversationListFromStore() +{ + size_t total = 0; + cached_conversations_ = service_.getConversations(0, 0, &total); + normalizeConversationNames(cached_conversations_); team::ui::TeamUiSnapshot team_snap; if (team::ui::team_ui_get_store().load(team_snap) && team_snap.has_team_id) @@ -811,16 +821,97 @@ void UiController::refreshUnreadCounts() { team_conv.preview = "No messages"; } - convs.insert(convs.begin(), team_conv); + cached_conversations_.insert(cached_conversations_.begin(), team_conv); } - channel_list_->setConversations(convs); - channel_list_->setSelectedConversation(current_conv_); + conversation_list_dirty_ = false; +} - // Update header status (battery only, with icon) +void UiController::normalizeConversationNames(std::vector& convs) const +{ + for (auto& conv : convs) + { + if (conv.id.peer == 0) + { + continue; + } + std::string contact_name = app::messagingFacade().getContactService().getContactName(conv.id.peer); + if (!contact_name.empty()) + { + conv.name = contact_name; + } + } +} + +void UiController::applyConversationListToUi() +{ + if (!channel_list_) + { + return; + } + + channel_list_->setConversations(cached_conversations_); + channel_list_->setSelectedConversation(current_conv_); channel_list_->updateBatteryFromBoard(); } +void UiController::updateConversationMetaForMessage(const chat::ChatMessage& msg, + const bool increment_unread) +{ + if (isTeamConversation(chat::ConversationId(msg.channel, msg.peer, msg.protocol))) + { + conversation_list_dirty_ = true; + return; + } + + chat::ConversationMeta meta; + meta.id = chat::ConversationId(msg.channel, msg.peer, msg.protocol); + meta.name = (msg.peer == 0) ? "Broadcast" : resolve_contact_name(msg.peer); + meta.preview = msg.text; + meta.last_timestamp = msg.timestamp; + meta.unread = (increment_unread && msg.status == chat::MessageStatus::Incoming) ? 1 : 0; + + bool found = false; + for (auto it = cached_conversations_.begin(); it != cached_conversations_.end(); ++it) + { + if (!(it->id == meta.id)) + { + continue; + } + found = true; + meta.unread += it->unread; + if (!increment_unread && msg.status == chat::MessageStatus::Incoming) + { + meta.unread = 0; + } + cached_conversations_.erase(it); + break; + } + + if (!found && msg.peer == 0) + { + meta.name = "Broadcast"; + } + + cached_conversations_.insert(cached_conversations_.begin(), meta); +} + +bool UiController::updateConversationViewForIncoming(const chat::ChatMessage& msg) +{ + if (!conversation_) + { + return false; + } + + if (!(current_conv_ == chat::ConversationId(msg.channel, msg.peer, msg.protocol))) + { + return false; + } + + conversation_->addMessage(msg); + return true; +} + bool UiController::isTeamConversation(const chat::ConversationId& conv) const { return isTeamConversationId(conv); diff --git a/platform/esp/arduino_common/src/app_runtime_support.cpp b/platform/esp/arduino_common/src/app_runtime_support.cpp index 34b3b8e9..e0697224 100644 --- a/platform/esp/arduino_common/src/app_runtime_support.cpp +++ b/platform/esp/arduino_common/src/app_runtime_support.cpp @@ -54,6 +54,7 @@ void updateCoreServices(app::IAppFacade& app_context) hostlink::process_pending_commands(); app_context.getChatService().processIncoming(); + app_context.getChatService().flushStore(); team::TeamService* team_service = app_context.getTeamService(); if (team_service) diff --git a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/store/internal_fs_store.h b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/store/internal_fs_store.h index ab238c04..39000208 100644 --- a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/store/internal_fs_store.h +++ b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/store/internal_fs_store.h @@ -26,6 +26,7 @@ class InternalFsStore final : public ::chat::IChatStore void clearConversation(const ::chat::ConversationId& conv) override; void clearAll() override; bool updateMessageStatus(::chat::MessageId msg_id, ::chat::MessageStatus status) override; + void flush() override; private: struct StoredMessageEntry @@ -107,6 +108,8 @@ class InternalFsStore final : public ::chat::IChatStore bool ensureFs() const; bool loadFromFs(); bool saveToFs() const; + void markDirty(); + void maybeSave(bool force = false); void evictOldestMessage(); ConversationStorage& getConversationStorage(const ::chat::ConversationId& conv); const ConversationStorage& getConversationStorage(const ::chat::ConversationId& conv) const; @@ -115,6 +118,13 @@ class InternalFsStore final : public ::chat::IChatStore std::map<::chat::ConversationId, ConversationStorage> conversations_; uint32_t next_sequence_ = 1; size_t total_message_count_ = 0; + mutable uint32_t last_save_ms_ = 0; + mutable uint32_t dirty_since_ms_ = 0; + mutable size_t pending_write_count_ = 0; + mutable bool dirty_ = false; + + static constexpr uint32_t kSaveIntervalMs = 1500; + static constexpr size_t kMaxPendingWrites = 4; }; } // namespace platform::nrf52::arduino_common::chat::infra::store diff --git a/platform/nrf52/arduino_common/src/chat/infra/store/internal_fs_store.cpp b/platform/nrf52/arduino_common/src/chat/infra/store/internal_fs_store.cpp index 83785c5b..7c72ae83 100644 --- a/platform/nrf52/arduino_common/src/chat/infra/store/internal_fs_store.cpp +++ b/platform/nrf52/arduino_common/src/chat/infra/store/internal_fs_store.cpp @@ -5,6 +5,7 @@ #include #include #include +#include "sys/clock.h" namespace platform::nrf52::arduino_common::chat::infra::store { @@ -39,7 +40,8 @@ void InternalFsStore::append(const ::chat::ChatMessage& msg) { storage.unread_count++; } - (void)saveToFs(); + markDirty(); + maybeSave(); } std::vector<::chat::ChatMessage> InternalFsStore::loadRecent(const ::chat::ConversationId& conv, size_t n) @@ -119,7 +121,8 @@ std::vector<::chat::ConversationMeta> InternalFsStore::loadConversationPage(size void InternalFsStore::setUnread(const ::chat::ConversationId& conv, int unread) { getConversationStorage(conv).unread_count = unread; - (void)saveToFs(); + markDirty(); + maybeSave(); } int InternalFsStore::getUnread(const ::chat::ConversationId& conv) const @@ -139,7 +142,8 @@ void InternalFsStore::clearConversation(const ::chat::ConversationId& conv) it->second.messages.clear(); it->second.unread_count = 0; conversations_.erase(it); - (void)saveToFs(); + markDirty(); + maybeSave(true); } void InternalFsStore::clearAll() @@ -147,6 +151,9 @@ void InternalFsStore::clearAll() conversations_.clear(); total_message_count_ = 0; next_sequence_ = 1; + dirty_ = false; + pending_write_count_ = 0; + dirty_since_ms_ = 0; if (ensureFs() && path_ && InternalFS.exists(path_)) { InternalFS.remove(path_); @@ -172,7 +179,8 @@ bool InternalFsStore::updateMessageStatus(::chat::MessageId msg_id, ::chat::Mess continue; } msg->status = status; - (void)saveToFs(); + markDirty(); + maybeSave(); return true; } } @@ -312,6 +320,10 @@ bool InternalFsStore::loadFromFs() { evictOldestMessage(); } + dirty_ = false; + pending_write_count_ = 0; + dirty_since_ms_ = 0; + last_save_ms_ = sys::millis_now(); return true; } @@ -403,7 +415,49 @@ bool InternalFsStore::saveToFs() const { InternalFS.remove(path_); } - return InternalFS.rename(temp_path.c_str(), path_); + const bool renamed = InternalFS.rename(temp_path.c_str(), path_); + if (renamed) + { + dirty_ = false; + pending_write_count_ = 0; + dirty_since_ms_ = 0; + last_save_ms_ = sys::millis_now(); + } + return renamed; +} + +void InternalFsStore::flush() +{ + maybeSave(true); +} + +void InternalFsStore::markDirty() +{ + if (!dirty_) + { + dirty_ = true; + dirty_since_ms_ = sys::millis_now(); + } + ++pending_write_count_; +} + +void InternalFsStore::maybeSave(bool force) +{ + if (!dirty_) + { + return; + } + + const uint32_t now_ms = sys::millis_now(); + const bool interval_elapsed = + (dirty_since_ms_ != 0) && ((now_ms - dirty_since_ms_) >= kSaveIntervalMs); + const bool too_many_pending = pending_write_count_ >= kMaxPendingWrites; + if (!force && !interval_elapsed && !too_many_pending) + { + return; + } + + (void)saveToFs(); } void InternalFsStore::evictOldestMessage()