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:
2026-09-29 01:07:32 +08:00
parent 88a8652d7b
commit 96865c0588
5 changed files with 134 additions and 35 deletions
+57 -3
View File
@@ -2,6 +2,8 @@
//!
//! 本层不依赖 GPUI,可被持久化层与 MQTT 引擎共用。
use std::sync::Arc;
use serde::{Deserialize, Serialize};
// ─── 连接配置 ────────────────────────────────────────────────────────────────
@@ -410,13 +412,17 @@ impl PayloadFormat {
/// 一条收到/发出的消息记录。payload 以 UTF-8 文本保存(非文本按 lossy 显示)。
///
/// 内存优化:`connection_id`/`topic` 用 `Arc<str>`——同一连接内主题高度重复,
/// 驻留后数千条记录只保留一份字符串。
#[derive(Debug, Clone)]
pub struct MqttRecord {
pub seq: u64,
pub connection_id: String,
pub connection_id: Arc<str>,
pub direction: Direction,
pub topic: String,
pub payload: String,
pub topic: Arc<str>,
/// Arc 共享:消息列表渲染每帧克隆最多数百条记录,零拷贝
pub payload: Arc<str>,
pub qos: u8,
pub retain: bool,
/// unix 毫秒
@@ -427,6 +433,29 @@ pub struct MqttRecord {
pub correlation_data: Option<String>,
pub message_expiry_interval: Option<u32>,
pub subscription_identifier: Option<u32>,
/// payload 超过 [`MAX_PAYLOAD_RETAIN`] 被截断
pub payload_truncated: bool,
}
/// 消息历史中单条 payload 的保留上限。超限截断,避免个别大报文
/// 长期占据历史缓冲(MQTTX 亦有类似限制)。
pub const MAX_PAYLOAD_RETAIN: usize = 256 * 1024;
/// 把原始 payload 字节转成用于历史记录的共享文本,超限截断。
/// 返回 (Arc<str>, 是否截断)。截断时回退到 UTF-8 字符边界。
pub fn retained_payload(raw: &[u8]) -> (Arc<str>, bool) {
if raw.len() <= MAX_PAYLOAD_RETAIN {
(Arc::from(String::from_utf8_lossy(raw).as_ref()), false)
} else {
let mut end = MAX_PAYLOAD_RETAIN;
while end > 0 && (raw[end] & 0xC0) == 0x80 {
end -= 1;
}
(
Arc::from(String::from_utf8_lossy(&raw[..end]).as_ref()),
true,
)
}
}
// ─── 发布参数 / 发布预设 ──────────────────────────────────────────────────────
@@ -863,4 +892,29 @@ mod tests {
// Subscription::new 同样产出默认启用的订阅
assert!(Subscription::new("c1", "t", 0).enabled);
}
// payload 截断:上限内不截断;超出时截断且不切坏 UTF-8 字符
#[test]
fn retained_payload_caps_and_preserves_utf8() {
let small = b"hello";
let (p, trunc) = retained_payload(small);
assert_eq!(&*p, "hello");
assert!(!trunc);
// 中文每字符 3 字节,让上限落在字符中间(256K 不是 3 的倍数)
let big: Vec<u8> = "中".repeat(MAX_PAYLOAD_RETAIN).into_bytes();
let (p, trunc) = retained_payload(&big);
assert!(trunc, "超过上限必须截断");
assert!(p.len() <= MAX_PAYLOAD_RETAIN);
assert_eq!(p.chars().last(), Some('中'), "不能切坏 UTF-8 字符");
assert!(
big.starts_with(p.as_bytes()),
"截断内容必须是原数据前缀"
);
// 恰好上限不截断
let exact = vec![b'a'; MAX_PAYLOAD_RETAIN];
let (_, trunc) = retained_payload(&exact);
assert!(!trunc);
}
}