From 273fe65a072281c56012da6f5e06baefdd89b579 Mon Sep 17 00:00:00 2001 From: ylytdeng Date: Sun, 17 May 2026 19:03:44 +0800 Subject: [PATCH] =?UTF-8?q?perf(monitor=5Fweb):=20=E4=B8=BB=E5=BE=AA?= =?UTF-8?q?=E7=8E=AF=20hot=20path=20=E4=B8=8D=E5=86=8D=E8=A7=A6=E5=8F=91?= =?UTF-8?q?=20message=20DB=20=E5=85=A8=E9=87=8F=E8=A7=A3=E5=AF=86=20(?= =?UTF-8?q?=E4=BF=AE=208-125s=20spike)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 根因 用户反馈实时消息延迟从亚秒级 → 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 通过 --- monitor_web.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/monitor_web.py b/monitor_web.py index 003cec2..3322c2f 100644 --- a/monitor_web.py +++ b/monitor_web.py @@ -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: