perf: 深度优化消息内存占用
- 消息缓冲 Vec → VecDeque:原来溢出时 drain(0..n) 每条消息 O(n) 搬移,高频消息下产生大量拷贝;环形缓冲均摊 O(1),并按配置上限 预分配避免反复扩容 - connection_id/topic/payload 改 Arc<str>:引擎侧主题驻留 (10k 上限,超出整体重建),同主题数千条记录只存一份; 消息列表每帧克隆最多 300 条用于渲染,payload 共享后零拷贝 - 单条 payload 历史保留上限 256KB(retained_payload,UTF-8 边界安全截断),超大报文不再长期占据环形缓冲;消息行显示 「已截断」徽章;发布记录改用实际发送字节(修 Base64/Hex 历史内容与真实报文不一致的隐患) - live 测试适配 Arc<str>;补截断边界单元测试(22 个 lib 测试 + 2 个 live 测试通过)
This commit is contained in:
+20
-13
@@ -2,6 +2,7 @@
|
||||
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui_kit::component::button::{Button, ButtonVariants as _, ButtonVariant};
|
||||
use gpui_kit::component::scroll::ScrollableElement as _;
|
||||
@@ -24,7 +25,7 @@ use crate::aliyun::AliyunPreset;
|
||||
use crate::model::{
|
||||
AppSettings, ConnectionConfig, ConnectionStatus, Direction, GlobalVariable, LogEntry,
|
||||
LogLevel, MqttRecord, PublishParams, PublishPreset, SubscribeOptions, Subscription,
|
||||
ThemeModePref,
|
||||
ThemeModePref, retained_payload,
|
||||
};
|
||||
use crate::mqtt::{EngineEvent, MqttEngine};
|
||||
use crate::store::Storage;
|
||||
@@ -83,7 +84,8 @@ pub struct MqttXApp {
|
||||
// ── 运行时状态 ──
|
||||
pub statuses: HashMap<String, ConnectionStatus>,
|
||||
pub errors: HashMap<String, String>,
|
||||
pub messages: HashMap<String, Vec<MqttRecord>>,
|
||||
/// 每连接消息环形缓冲:键为驻留后的连接 ID,容量受 max_messages 约束
|
||||
pub messages: HashMap<Arc<str>, VecDeque<MqttRecord>>,
|
||||
pub logs: VecDeque<LogEntry>,
|
||||
pub seq: u64,
|
||||
|
||||
@@ -308,7 +310,7 @@ impl MqttXApp {
|
||||
let id = record.connection_id.clone();
|
||||
record.seq = self.next_seq();
|
||||
self.push_message(id.clone(), record);
|
||||
if let Some(view) = self.views.get(&id) {
|
||||
if let Some(view) = self.views.get(id.as_ref()) {
|
||||
view.update(cx, |_, cx| cx.notify());
|
||||
}
|
||||
}
|
||||
@@ -321,11 +323,12 @@ impl MqttXApp {
|
||||
content_type,
|
||||
user_properties,
|
||||
} => {
|
||||
let (payload, payload_truncated) = retained_payload(&payload);
|
||||
let record = MqttRecord {
|
||||
seq: self.next_seq(),
|
||||
connection_id: connection_id.clone(),
|
||||
connection_id: Arc::from(connection_id.as_str()),
|
||||
direction: Direction::Published,
|
||||
topic,
|
||||
topic: Arc::from(topic.as_str()),
|
||||
payload,
|
||||
qos,
|
||||
retain,
|
||||
@@ -336,9 +339,10 @@ impl MqttXApp {
|
||||
correlation_data: None,
|
||||
message_expiry_interval: None,
|
||||
subscription_identifier: None,
|
||||
payload_truncated,
|
||||
};
|
||||
self.push_message(connection_id.clone(), record);
|
||||
if let Some(view) = self.views.get(&connection_id) {
|
||||
self.push_message(Arc::from(connection_id.as_str()), record);
|
||||
if let Some(view) = self.views.get(connection_id.as_str()) {
|
||||
view.update(cx, |_, cx| cx.notify());
|
||||
}
|
||||
}
|
||||
@@ -400,14 +404,17 @@ impl MqttXApp {
|
||||
self.seq
|
||||
}
|
||||
|
||||
fn push_message(&mut self, connection_id: String, record: MqttRecord) {
|
||||
fn push_message(&mut self, connection_id: Arc<str>, record: MqttRecord) {
|
||||
let cap = self.settings.max_messages.max(100);
|
||||
let list = self.messages.entry(connection_id).or_default();
|
||||
if list.len() >= cap {
|
||||
let drop_n = list.len() - cap + 1;
|
||||
list.drain(0..drop_n);
|
||||
let list = self
|
||||
.messages
|
||||
.entry(connection_id)
|
||||
.or_insert_with(|| VecDeque::with_capacity(cap));
|
||||
// 环形缓冲:满则丢最旧,均摊 O(1)(原来的 Vec::drain 是 O(n) 搬移)
|
||||
while list.len() >= cap {
|
||||
list.pop_front();
|
||||
}
|
||||
list.push(record);
|
||||
list.push_back(record);
|
||||
}
|
||||
|
||||
fn push_log(&mut self, entry: LogEntry) {
|
||||
|
||||
@@ -24,8 +24,9 @@ use gpui_kit::{
|
||||
use gpui_kit::component::IndexPath;
|
||||
|
||||
use crate::model::{
|
||||
render_template, render_will_templates, ConnectionConfig, ConnectionStatus, Direction, GlobalVariable,
|
||||
LogLevel, MqttRecord, PayloadFormat, PublishParams, SubscribeOptions, Subscription,
|
||||
render_template, render_will_templates, ConnectionConfig, ConnectionStatus, Direction,
|
||||
GlobalVariable, LogLevel, MqttRecord, PayloadFormat, PublishParams, SubscribeOptions,
|
||||
Subscription, MAX_PAYLOAD_RETAIN,
|
||||
};
|
||||
use crate::mqtt::MqttEngine;
|
||||
use crate::ui::app::MqttXApp;
|
||||
@@ -1285,7 +1286,7 @@ impl ConnectionView {
|
||||
.font_family(mono.clone())
|
||||
// 匹配到订阅色时主题文字着色,否则保持默认前景色
|
||||
.when_some(sub_color, |t, hue| t.text_color(hue_color(hue)))
|
||||
.child(record.topic.clone()),
|
||||
.child(record.topic.to_string()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
@@ -1315,6 +1316,17 @@ impl ConnectionView {
|
||||
.child("retain"),
|
||||
)
|
||||
})
|
||||
.when(record.payload_truncated, |t| {
|
||||
t.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.px_1()
|
||||
.rounded_sm()
|
||||
.bg(cx.theme().warning.alpha(0.15))
|
||||
.text_color(cx.theme().warning)
|
||||
.child(format!("已截断 {}KB", MAX_PAYLOAD_RETAIN / 1024)),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
@@ -1385,7 +1397,7 @@ impl ConnectionView {
|
||||
.tooltip("复制主题")
|
||||
.on_click(move |_, window, cx| {
|
||||
cx.stop_propagation();
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(copy_topic.clone()));
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(copy_topic.to_string()));
|
||||
window.push_notification(Notification::success("已复制主题"), cx);
|
||||
}),
|
||||
)
|
||||
@@ -1398,7 +1410,7 @@ impl ConnectionView {
|
||||
.tooltip("复制负载原文")
|
||||
.on_click(move |_, window, cx| {
|
||||
cx.stop_propagation();
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(copy_payload.clone()));
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(copy_payload.to_string()));
|
||||
window.push_notification(Notification::success("已复制负载"), cx);
|
||||
}),
|
||||
)
|
||||
@@ -1533,7 +1545,7 @@ impl ConnectionView {
|
||||
.collect();
|
||||
let mut matched = 0usize;
|
||||
let mut records: Vec<MqttRecord> = Vec::new();
|
||||
if let Some(m) = app.messages.get(&conn) {
|
||||
if let Some(m) = app.messages.get(conn.as_str()) {
|
||||
for r in m.iter().rev() {
|
||||
// 方向 / 搜索 / 订阅过滤叠加,过滤后再截断保证最新消息优先
|
||||
let dir_ok = match dir {
|
||||
@@ -1742,7 +1754,7 @@ impl ConnectionView {
|
||||
view.detail_format = DetailFormat::Auto;
|
||||
if let Some(app) = view.app.upgrade() {
|
||||
app.update(cx, |app, cx| {
|
||||
app.messages.remove(&clear_id);
|
||||
app.messages.remove(clear_id.as_str());
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user