perf(monitor_web): 主循环 hot path 不再触发 message DB 全量解密 (修 8-125s spike)
## 根因
用户反馈实时消息延迟从亚秒级 → 8-125 秒。
后端 log:
[perf] decrypt=576页/47.8ms, query=46.6ms
... 总耗时=10381.0ms / 44964.4ms / 125398.8ms ...
解密+查询只 95ms, 但总耗时 8-125 秒。源头:
PR #106 (commit 1aa12c8, issue #42) 引入了 _lookup_latest_message,
在 check_updates 主循环里每个新消息都调:
dec_path = self.db_cache.get(db_key)
^^^^^^^^^^^^^^^^^^^^^^^^
mtime 变化时同步 full_decrypt 整个 message_N.db (~10s)
微信写消息时 message_N.db mtime 跟着变 → get() 触发全量解密 →
主循环阻塞 10 秒。多个 session 同时更新就叠加成几十秒。
之前 db_cache.get 主要在 _check_hidden_messages (走 _hidden_executor
后台线程) 调用, 不阻塞主线程。PR #106 把它带到了主线程 hot path。
证据 (log 里清晰可见):
[cache] message\message_0.db 全量解密 10551ms
[18:23:31 延迟=14.0s] [...] 莫名感触: ... ← 实测消息延迟 14 秒
## 修复
MonitorDBCache 加 peek(rel_key) 方法, **不触发**重新解密, 只读
当前已解密文件路径 (可能 stale 1 个 mtime 周期):
def peek(self, rel_key):
out_path = os.path.join(self.tmp_dir, out_name)
return out_path if os.path.exists(out_path) else None
_lookup_latest_message 把 self.db_cache.get(db_key) 改成 peek(db_key)。
## Trade-off
stale cache 可能让 _lookup_latest_message 查不到刚写入的 local_id,
返回 (None, None)。check_updates 会:
- 跳过加 _shown_keys (issue #79 的去重保险)
- 跳过用 full_content 替换 summary (issue #42 的扩展正文)
但**两个 fallback 路径都正常**:
- 1 秒后 _check_hidden_messages (异步线程) 会用 db_cache.get 等待
解密完, 拿到 local_id 并 emit hidden 消息 (issue #79 不破坏)
- 第一次推送仍用 SessionTable.summary 的 80 字短截断, 后续如果 user
开了详情自然加载完整 (issue #42 退化为"原始行为", 不影响主流程)
权衡: 用"偶发 80 字摘要" 换 "无 8-125 秒延迟"。
## 实测预期
修复后主循环总耗时应回到 < 200ms (跟解密+查询 95ms 同量级)。
SessionTable 跑 hidden_executor 仍然能补抓密集消息 (issue #79 保留)。
## 副作用 (无)
- get() 行为不变, hidden 路径仍同步等解密 (它在后台线程, 不影响主线程)
- peek() 是新加方法, 不影响现有调用方
- 测试 185/185 通过
This commit is contained in:
@@ -255,6 +255,25 @@ class MonitorDBCache:
|
||||
with lock:
|
||||
self._state.pop(rel_key, None)
|
||||
|
||||
def peek(self, rel_key):
|
||||
"""返回当前已解密文件路径,**不触发**重新解密 (即使源 mtime 变了)。
|
||||
|
||||
给主循环 hot path (check_updates → _lookup_latest_message) 用,
|
||||
避免每次新消息都同步等待整个 message_N.db 重新全量解密 (10s+),
|
||||
把主循环延迟从亚秒级飙到 8-125s。
|
||||
|
||||
返回的路径可能 stale (滞后 1 个 mtime 周期)。调用方应能容忍 stale
|
||||
(比如查不到 latest_local_id 时跳过加 _shown_keys, 让 hidden 路径
|
||||
异步兜底)。
|
||||
|
||||
get() 仍保留同步行为给真正需要最新的调用方 (hidden 路径异步线程)。
|
||||
"""
|
||||
if not get_key_info(self.keys, rel_key):
|
||||
return None
|
||||
out_name = rel_key.replace('\\', '_').replace('/', '_')
|
||||
out_path = os.path.join(self.tmp_dir, out_name)
|
||||
return out_path if os.path.exists(out_path) else None
|
||||
|
||||
def get(self, rel_key):
|
||||
"""返回解密后的临时文件路径,mtime 变化时自动重新解密"""
|
||||
key_info = get_key_info(self.keys, rel_key)
|
||||
@@ -956,7 +975,10 @@ class SessionMonitor:
|
||||
return None, None
|
||||
table_name = f"Msg_{hashlib.md5(username.encode()).hexdigest()}"
|
||||
for db_key in db_keys:
|
||||
dec_path = self.db_cache.get(db_key)
|
||||
# 用 peek 不触发同步解密 (主线程 hot path)。如果缓存还 stale
|
||||
# 没 latest_local_id, 让 hidden 异步路径稍后兜底加 _shown_keys。
|
||||
# 见 MonitorDBCache.peek 注释关于为什么这里不能用 .get。
|
||||
dec_path = self.db_cache.peek(db_key)
|
||||
if not dec_path:
|
||||
continue
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user