fix(msgbot): 修复内存未释放——延迟easyocr/torch导入+修正SetProcessWorkingSetSize参数+添加_heapmin(Task #1625)

This commit is contained in:
youpaishidifu
2026-08-29 23:35:31 +08:00
parent d3bb961c90
commit d2a4b4c0ea
+49 -23
View File
@@ -18,19 +18,12 @@ logger = logging.getLogger("msgbot.monitor")
# ⚠ 顺序重要: torch/easyocr 必须在 COM 模块 (uiautomation/winrt) 之前导入,
# 否则 c10.dll 会因 COM 初始化冲突抛出 WinError 1114。
_HAS_EASYOCR = False
# ⭐ Task #1625: 延迟导入 easyocr/numpy 到 _init_easyocr() 惰性加载,
# 避免主进程启动时加载 torch DLL (~300MB) + numpy (~100MB)。
# 模块级乐观设 True,首次 _init_easyocr() 尝试导入;失败则改 False。
_HAS_EASYOCR = True # 乐观标记,延迟确认
_easyocr = None
_np = None
try:
import easyocr as _easyocr
import numpy as _np
_HAS_EASYOCR = True
except (ImportError, OSError) as _e:
import traceback as _tb
_easyocr = None
_np = None
logger.debug(f"[ocr] EasyOCR 导入失败: {type(_e).__name__}: {_e}")
logger.debug(f"[ocr] Traceback: {_tb.format_exc()}")
import uiautomation as auto
import win32gui
@@ -1061,13 +1054,25 @@ class MessageMonitor:
gc.collect()
# 2) Windows 特定:把工作集物理内存归还 OS
# ⭐ Task #1625: 修正 SetProcessWorkingSetSize 参数和调用顺序
try:
kernel32 = ctypes.windll.kernel32
psapi = ctypes.windll.psapi
h_process = kernel32.GetCurrentProcess()
# EmptyWorkingSet:强制把进程工作集所有页面换出到页面文件/丢弃,
# 是真正把物理 RAM 交还 OS 的调用(SetProcessWorkingSetSize 仅设上下限)。
# 先 SetProcessWorkingSetSize(1, -1):设置最小=1字节,最大=无限制
# 允许系统回收所有可回收页面,为 EmptyWorkingSet 创造条件
try:
kernel32.SetProcessWorkingSetSize(
h_process,
1, # 最小工作集:1字节,允许系统回收所有可回收页面
-1 # 最大工作集:无限制
)
logger.debug("[memory] 已调用 SetProcessWorkingSetSize(1, -1) 设置工作集下限")
except Exception as e:
logger.debug(f"[memory] SetProcessWorkingSetSize 失败: {e}")
# 再 EmptyWorkingSet:强制把进程工作集所有页面换出到页面文件/丢弃
try:
if psapi.EmptyWorkingSet(h_process):
logger.debug("[memory] 已调用 EmptyWorkingSet 归还物理内存")
@@ -1075,16 +1080,8 @@ class MessageMonitor:
logger.debug("[memory] EmptyWorkingSet 返回 0(无需归还)")
except Exception as e:
logger.debug(f"[memory] EmptyWorkingSet 失败: {e}")
# SetProcessWorkingSetSize(-1,-1):允许系统后续按需在最小/最大间调度
kernel32.SetProcessWorkingSetSize(
h_process,
-1, # 最小工作集:允许系统回收所有可回收页面
-1 # 最大工作集:允许系统回收所有可回收页面
)
logger.debug("[memory] 已调用 SetProcessWorkingSetSize 释放工作集内存")
except Exception as e:
logger.debug(f"[memory] SetProcessWorkingSetSize 失败: {e}")
logger.debug(f"[memory] 内存释放调用失败: {e}")
# 3) 归还 GPU 显存(若 torch/CUDA 可用)
try:
@@ -1100,6 +1097,14 @@ class MessageMonitor:
# 再次垃圾回收,确保上述释放后的垃圾被最终回收
gc.collect()
# ⭐ Task #1625: 强制 C 运行时释放空闲堆内存回 OS
try:
import msvcrt
msvcrt._heapmin()
logger.debug("[memory] 已调用 msvcrt._heapmin() 释放 C 运行时堆内存")
except Exception:
pass
logger.info("[memory] 内存释放完成,已交还系统")
def _kill_easyocr_proc(self, reason: str = ""):
@@ -2175,10 +2180,24 @@ class MessageMonitor:
def _init_easyocr(self):
"""惰性初始化 EasyOCR(首次约 3-10 秒)。"""
if not _HAS_EASYOCR or self._easyocr_reader is not None:
global _easyocr, _np, _HAS_EASYOCR
if self._easyocr_reader is not None:
return
try:
logger.info("[ocr] EasyOCR 初始化中(首次约 3-10 秒)...")
# ⭐ Task #1625: 延迟导入 easyocr 和 numpy,避免主进程加载 torch DLL
try:
import easyocr as _easyocr
import numpy as _np
_HAS_EASYOCR = True
except (ImportError, OSError) as _e:
import traceback as _tb
_easyocr = None
_np = None
_HAS_EASYOCR = False
logger.debug(f"[ocr] EasyOCR 导入失败: {type(_e).__name__}: {_e}")
logger.debug(f"[ocr] Traceback: {_tb.format_exc()}")
return
# 限制 torch 线程数,避免 MKL/OpenMP 在多线程环境段错误
import torch
torch.set_num_threads(1)
@@ -2939,6 +2958,13 @@ class MessageMonitor:
if not _HAS_EASYOCR or self._easyocr_broken:
return
# EasyOCR 用原图(1x):2x 图送 GPU 显存压力大 + 加速泄漏
# ⭐ Task #1625: numpy 懒加载(主进程不需要 torch,仅需 numpy 做 PIL→array 转换)
global _np
if _np is None:
try:
import numpy as _np
except ImportError:
return
img_np = _np.array(im_original)
# 使用动态超时:初始45秒,每次超时增加15秒,成功后重置
_inner_timeout = self._easyocr_current_timeout