perf: reduce chat list and message handling latency

This commit is contained in:
liu weikai
2026-03-27 17:35:47 +08:00
parent 988827a3bc
commit df76526b5d
14 changed files with 376 additions and 45 deletions
+1
View File
@@ -976,6 +976,7 @@ class MinimalAppFacade final : public app::IAppFacade
if (chat_service_)
{
chat_service_->processIncoming();
chat_service_->flushStore();
}
if (team_pairing_)
@@ -615,6 +615,7 @@ void AppFacadeRuntime::updateCoreServices()
if (chat_service_)
{
chat_service_->processIncoming();
chat_service_->flushStore();
}
if (ble_manager_)
{
@@ -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
@@ -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);
@@ -202,6 +202,11 @@ void ChatService::processIncoming()
}
}
void ChatService::flushStore()
{
store_.flush();
}
void ChatService::addIncomingMessageObserver(IncomingMessageObserver* observer)
{
if (!observer)
@@ -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; }
@@ -140,6 +140,8 @@ class ChatMessageListScreen
message_list::input::Controller input_controller_{};
void rebuildList();
bool updateListInPlace(const std::vector<chat::ConversationMeta>& convs);
void updateListItem(size_t index, const chat::ConversationMeta& conv);
void updateFilterHighlight();
void setFilterMode(FilterMode mode);
@@ -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<chat::ConversationMeta>& 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<chat::ConversationMeta> 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);
@@ -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)
@@ -156,6 +156,23 @@ static bool conversation_list_equal(const std::vector<chat::ConversationMeta>& l
return true;
}
static bool conversation_identity_list_equal(const std::vector<chat::ConversationMeta>& lhs,
const std::vector<chat::ConversationMeta>& 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<chat::Conversatio
team_visibility_changed ? 1 : 0,
(unsigned)convs.size());
const std::vector<chat::ConversationMeta> previous_convs = convs_;
convs_ = convs;
if (team_btn_)
{
@@ -354,7 +372,11 @@ void ChatMessageListScreen::setConversations(const std::vector<chat::Conversatio
filter_mode_ = FilterMode::Broadcast;
}
updateFilterHighlight();
rebuildList();
if (!conversation_identity_list_equal(previous_convs, convs_) ||
!updateListInPlace(convs_))
{
rebuildList();
}
}
void ChatMessageListScreen::setSelected(int index)
@@ -617,6 +639,95 @@ void ChatMessageListScreen::rebuildList()
chat::ui::message_list::input::on_ui_refreshed(&input_controller_);
}
bool ChatMessageListScreen::updateListInPlace(const std::vector<chat::ConversationMeta>& 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<chat::ConversationMeta> 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 =
@@ -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<chat::ConversationMeta>& 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);
@@ -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)
@@ -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
@@ -5,6 +5,7 @@
#include <algorithm>
#include <cstring>
#include <string>
#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()