fix(msgbot): 修复OCR空列表IndexError并增强stop内存释放至最低占用
Task #1622: 行合并循环在 merged_lines 为空时无条件访问 merged_lines[-1] 抛 IndexError,被最外层 except 记为 [qq] OCR失败: list index out of range。 增加 `if merged_lines else False` 守卫,首行非@结尾/不含问号/非bot回复时 不再越界。 Task #1623: 原 _release_memory 仅 SetProcessWorkingSetSize(-1,-1) 不逼出常驻 页,且未释放 winrt Windows OCR 引擎、未清 GPU 缓存,导致 bot 运行后暂停 内存仍高达~600MB。增强为 EmptyWorkingSet 强制归还物理内存 + torch.cuda.empty_cache/synchronize + 释放 self._ocr_engine 引用,与 GUI 侧 _force_system_memory_cleanup 范式一致,使运行后暂停与刚启动待机内存一致。
This commit is contained in:
+69
-27
@@ -1020,47 +1020,86 @@ class MessageMonitor:
|
||||
return self._stop_event.wait(timeout=seconds)
|
||||
|
||||
def _release_memory(self):
|
||||
"""主动释放内存,交还系统重新分配调度。只保留GUI需要的基本内存。"""
|
||||
"""主动释放内存,交还系统重新分配调度。只保留GUI需要的基本内存。
|
||||
|
||||
⭐ Task #1623: 增强版——除原有 SetProcessWorkingSetSize 外,新增:
|
||||
1. psapi.EmptyWorkingSet 强制把工作集页面归还 OS(SetProcessWorkingSetSize(-1,-1)
|
||||
只会回收"已 eligible"的页面,无法把 OCR 引擎/图像缓存等常驻页逼出内存,
|
||||
故 600MB 残留)。
|
||||
2. torch.cuda.empty_cache() + synchronize() 归还 GPU 显存。
|
||||
3. 释放主进程内 Windows OCR 引擎 self._ocr_engine(winrt 原生对象,stop 时
|
||||
不释放会长期占用数百 MB)。
|
||||
目标:待机(刚启动 / bot 运行一会后暂停)内存都只保留最低所需。
|
||||
"""
|
||||
import gc
|
||||
import ctypes
|
||||
import sys
|
||||
|
||||
# 强制垃圾回收
|
||||
|
||||
# 1) 先释放 Python 侧大对象引用,再回收,避免 IMAGE/torch tensor 被环引用困住
|
||||
# 清空大对象引用
|
||||
for attr in ['_last_texts', '_seen_messages', '_replied_texts']:
|
||||
if hasattr(self, attr):
|
||||
getattr(self, attr).clear()
|
||||
if hasattr(self, '_semantic_fingerprint_cache'):
|
||||
self._semantic_fingerprint_cache.clear()
|
||||
|
||||
# ⭐ Task #1623: 释放 Windows OCR 引擎(winrt 原生对象)
|
||||
if getattr(self, '_ocr_engine', None) is not None:
|
||||
try:
|
||||
del self._ocr_engine
|
||||
except Exception:
|
||||
pass
|
||||
self._ocr_engine = None
|
||||
logger.debug("[memory] 已释放 Windows OCR 引擎引用")
|
||||
|
||||
# 清空 EasyOCR 相关缓存
|
||||
if hasattr(self, '_easyocr_proc') and self._easyocr_proc:
|
||||
# 子进程已杀,但引用可能残留
|
||||
self._easyocr_proc = None
|
||||
|
||||
# 强制垃圾回收(释放上述被清引用占据的对象)
|
||||
gc.collect()
|
||||
|
||||
# Windows 特定:使用 EmptyProcessWorkingSet 释放工作集内存
|
||||
|
||||
# 2) Windows 特定:把工作集物理内存归还 OS
|
||||
try:
|
||||
# 获取当前进程句柄
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
psapi = ctypes.windll.psapi
|
||||
|
||||
# EmptyProcessWorkingSet 函数:将进程工作集最小化
|
||||
# 参数:进程句柄,最小值(-1表示当前进程),最大值(-1表示当前进程)
|
||||
# 返回:非零表示成功
|
||||
h_process = kernel32.GetCurrentProcess()
|
||||
|
||||
# EmptyWorkingSet:强制把进程工作集所有页面换出到页面文件/丢弃,
|
||||
# 是真正把物理 RAM 交还 OS 的调用(SetProcessWorkingSetSize 仅设上下限)。
|
||||
try:
|
||||
if psapi.EmptyWorkingSet(h_process):
|
||||
logger.debug("[memory] 已调用 EmptyWorkingSet 归还物理内存")
|
||||
else:
|
||||
logger.debug("[memory] EmptyWorkingSet 返回 0(无需归还)")
|
||||
except Exception as e:
|
||||
logger.debug(f"[memory] EmptyWorkingSet 失败: {e}")
|
||||
|
||||
# SetProcessWorkingSetSize(-1,-1):允许系统后续按需在最小/最大间调度
|
||||
kernel32.SetProcessWorkingSetSize(
|
||||
kernel32.GetCurrentProcess(),
|
||||
h_process,
|
||||
-1, # 最小工作集:允许系统回收所有可回收页面
|
||||
-1 # 最大工作集:允许系统回收所有可回收页面
|
||||
)
|
||||
logger.debug("[memory] 已调用 SetProcessWorkingSetSize 释放工作集内存")
|
||||
except Exception as e:
|
||||
logger.debug(f"[memory] SetProcessWorkingSetSize 失败: {e}")
|
||||
|
||||
# 清空 EasyOCR 相关缓存
|
||||
if hasattr(self, '_easyocr_proc') and self._easyocr_proc:
|
||||
# 子进程已杀,但引用可能残留
|
||||
self._easyocr_proc = None
|
||||
|
||||
# 清空大对象引用
|
||||
for attr in ['_last_texts', '_seen_messages', '_replied_texts']:
|
||||
if hasattr(self, attr):
|
||||
getattr(self, attr).clear()
|
||||
if hasattr(self, '_semantic_fingerprint_cache'):
|
||||
self._semantic_fingerprint_cache.clear()
|
||||
|
||||
# 再次垃圾回收
|
||||
|
||||
# 3) 归还 GPU 显存(若 torch/CUDA 可用)
|
||||
try:
|
||||
import torch
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.synchronize()
|
||||
torch.cuda.empty_cache()
|
||||
logger.debug("[memory] 已调用 torch.cuda.empty_cache 归还显存")
|
||||
except Exception:
|
||||
# torch 未安装或当前为 CPU 推理,忽略
|
||||
pass
|
||||
|
||||
# 再次垃圾回收,确保上述释放后的垃圾被最终回收
|
||||
gc.collect()
|
||||
|
||||
|
||||
logger.info("[memory] 内存释放完成,已交还系统")
|
||||
|
||||
def _kill_easyocr_proc(self, reason: str = ""):
|
||||
@@ -3619,7 +3658,10 @@ class MessageMonitor:
|
||||
or re.match(r'^[\u4e00-\u9fff]{2,8}[@,,]', _next_line) \
|
||||
or re.match(r'^[/@((]', _next_line):
|
||||
_next_is_short_or_name = True
|
||||
_prev_ends_with_comma = merged_lines[-1].rstrip().endswith((',', ','))
|
||||
# ⭐ Task #1622: 必须先判断 merged_lines 非空,否则首行不以@结尾/不含问号/
|
||||
# 非bot回复时 merged_lines[-1] 抛 IndexError → 被最外层 except 捕获成
|
||||
# [qq] OCR失败: list index out of range
|
||||
_prev_ends_with_comma = merged_lines[-1].rstrip().endswith((',', ',')) if merged_lines else False
|
||||
if merged_lines and not _SENT_END.search(merged_lines[-1]) \
|
||||
and not _TS_START.match(rl) and not _TS_START.match(merged_lines[-1]) \
|
||||
and not _is_bot_reply_line and not _is_single_excl and not _prev_is_single_excl \
|
||||
|
||||
Reference in New Issue
Block a user