mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-03 05:44:44 +08:00
perf(memory): stop retaining module sources and cap nginx workers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
InfinityPacer
parent
9af903e677
commit
c924a455b2
@@ -4,14 +4,48 @@ import importlib
|
||||
import inspect
|
||||
import pkgutil
|
||||
import textwrap
|
||||
import threading
|
||||
import tokenize
|
||||
import weakref
|
||||
from pathlib import Path
|
||||
from types import FunctionType
|
||||
from typing import Any, Callable, List, get_type_hints
|
||||
from types import CodeType, FunctionType
|
||||
from typing import Any, Callable, List, Optional, get_type_hints
|
||||
|
||||
|
||||
FilterFuncType = Callable[[str, Any], bool]
|
||||
|
||||
|
||||
# check_method 结果缓存:弱引用代码对象,模块或插件卸载后自动清理
|
||||
_implemented_cache: "weakref.WeakKeyDictionary[CodeType, bool]" = weakref.WeakKeyDictionary()
|
||||
_implemented_cache_lock = threading.Lock()
|
||||
|
||||
|
||||
def _read_function_source(code: CodeType) -> str:
|
||||
"""
|
||||
读取函数定义源码。
|
||||
|
||||
与 inspect.getsource 的定位方式一致,但直接读文件而不经 linecache:
|
||||
getsource 会把整份源文件留在 linecache 中常驻,所有模块和插件累计约 10MB。
|
||||
"""
|
||||
with tokenize.open(code.co_filename) as fh:
|
||||
lines = fh.readlines()
|
||||
return "".join(inspect.getblock(lines[code.co_firstlineno - 1:]))
|
||||
|
||||
|
||||
def _is_placeholder_stmt(stmt: ast.stmt) -> bool:
|
||||
"""pass、docstring、... 与 raise NotImplementedError 视为未实现的占位语句。"""
|
||||
if isinstance(stmt, ast.Pass):
|
||||
return True
|
||||
if isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Constant):
|
||||
return isinstance(stmt.value.value, str) or stmt.value.value is Ellipsis
|
||||
if isinstance(stmt, ast.Raise):
|
||||
exc = stmt.exc
|
||||
if isinstance(exc, ast.Call):
|
||||
exc = exc.func
|
||||
return isinstance(exc, ast.Name) and exc.id == "NotImplementedError"
|
||||
return False
|
||||
|
||||
|
||||
def _default_filter(name: str, obj: Any) -> bool:
|
||||
"""接受具有名称和值的动态加载对象。"""
|
||||
return bool(name and obj)
|
||||
@@ -151,35 +185,32 @@ class ObjectUtils:
|
||||
def check_method(func: Callable[..., Any]) -> bool:
|
||||
"""
|
||||
检查函数是否已实现
|
||||
|
||||
模块链每次分发都会调用,结果按代码对象缓存;插件热重载生成新代码对象后自然重新判定,
|
||||
旧代码对象回收时缓存项随之释放。
|
||||
"""
|
||||
target = inspect.unwrap(func)
|
||||
code = getattr(getattr(target, "__func__", target), "__code__", None)
|
||||
if code is None:
|
||||
return ObjectUtils._check_method_uncached(func, None)
|
||||
cached = _implemented_cache.get(code)
|
||||
if cached is None:
|
||||
cached = ObjectUtils._check_method_uncached(func, code)
|
||||
with _implemented_cache_lock:
|
||||
_implemented_cache[code] = cached
|
||||
return cached
|
||||
|
||||
@staticmethod
|
||||
def _check_method_uncached(func: Callable[..., Any], code: Optional[CodeType]) -> bool:
|
||||
"""按源码语法判断函数体是否只有占位语句,源码不可得时退回字节码判断。"""
|
||||
try:
|
||||
src = inspect.getsource(func)
|
||||
tree = ast.parse(textwrap.dedent(src))
|
||||
if code is None:
|
||||
raise OSError("callable without code object")
|
||||
tree = ast.parse(textwrap.dedent(_read_function_source(code)))
|
||||
node = tree.body[0]
|
||||
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
return True
|
||||
body = node.body
|
||||
|
||||
for stmt in body:
|
||||
# 跳过 pass
|
||||
if isinstance(stmt, ast.Pass):
|
||||
continue
|
||||
# 跳过 docstring 或 ...
|
||||
if isinstance(stmt, ast.Expr):
|
||||
expr = stmt.value
|
||||
if isinstance(expr, ast.Constant):
|
||||
if isinstance(expr.value, str) or expr.value is Ellipsis:
|
||||
continue
|
||||
# 检查 raise NotImplementedError
|
||||
if isinstance(stmt, ast.Raise):
|
||||
exc = stmt.exc
|
||||
if isinstance(exc, ast.Call) and getattr(exc.func, "id", None) == "NotImplementedError":
|
||||
continue
|
||||
if isinstance(exc, ast.Name) and exc.id == "NotImplementedError":
|
||||
continue
|
||||
|
||||
return True
|
||||
return False
|
||||
return not all(_is_placeholder_stmt(stmt) for stmt in node.body)
|
||||
except Exception:
|
||||
# 源代码分析失败时,进行字节码分析
|
||||
code_obj = func.__code__ # type: ignore[attr-defined]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
user moviepilot;
|
||||
worker_processes auto;
|
||||
# 只代理单用户 WebUI 与 API,固定 2 个 worker 足够,auto 会按 CPU 核数起进程,多核 NAS 上白占内存
|
||||
worker_processes 2;
|
||||
pid /var/run/nginx.pid;
|
||||
worker_cpu_affinity auto;
|
||||
|
||||
|
||||
@@ -1,41 +1,101 @@
|
||||
from unittest import TestCase
|
||||
import functools
|
||||
import linecache
|
||||
|
||||
from app.foundation import reflection
|
||||
from app.foundation.reflection import ObjectUtils
|
||||
|
||||
|
||||
class ObjectUtilsTest(TestCase):
|
||||
def test_check_method():
|
||||
def implemented_function():
|
||||
return "Hello"
|
||||
|
||||
def test_check_method(self):
|
||||
def implemented_function():
|
||||
return "Hello"
|
||||
def pass_function():
|
||||
pass
|
||||
|
||||
def docstring_function():
|
||||
"""This is a docstring."""
|
||||
|
||||
def ellipsis_function():
|
||||
...
|
||||
|
||||
def not_implemented_function():
|
||||
raise NotImplementedError
|
||||
|
||||
def not_implemented_function_with_call():
|
||||
raise NotImplementedError()
|
||||
|
||||
async def multiple_lines_async_def(_param1: str,
|
||||
_param2: str):
|
||||
pass
|
||||
|
||||
def empty_function():
|
||||
return
|
||||
|
||||
assert ObjectUtils.check_method(implemented_function)
|
||||
assert not ObjectUtils.check_method(pass_function)
|
||||
assert not ObjectUtils.check_method(docstring_function)
|
||||
assert not ObjectUtils.check_method(ellipsis_function)
|
||||
assert not ObjectUtils.check_method(not_implemented_function)
|
||||
assert not ObjectUtils.check_method(not_implemented_function_with_call)
|
||||
assert not ObjectUtils.check_method(multiple_lines_async_def)
|
||||
assert ObjectUtils.check_method(empty_function)
|
||||
|
||||
|
||||
class _Module:
|
||||
"""模拟模块类:占位方法与已实现方法并存,并经装饰器包装。"""
|
||||
|
||||
def placeholder(self):
|
||||
"""未实现"""
|
||||
|
||||
@staticmethod
|
||||
def _wrap(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
@_wrap
|
||||
def decorated_placeholder(self):
|
||||
pass
|
||||
|
||||
def implemented(self):
|
||||
return 1
|
||||
|
||||
|
||||
def test_check_method_handles_bound_and_decorated_methods():
|
||||
module = _Module()
|
||||
|
||||
assert not ObjectUtils.check_method(module.placeholder)
|
||||
assert not ObjectUtils.check_method(module.decorated_placeholder)
|
||||
assert ObjectUtils.check_method(module.implemented)
|
||||
|
||||
|
||||
def test_check_method_does_not_leave_source_in_linecache():
|
||||
linecache.clearcache()
|
||||
|
||||
def pass_function():
|
||||
pass
|
||||
|
||||
assert not ObjectUtils.check_method(pass_function)
|
||||
assert __file__ not in linecache.cache
|
||||
|
||||
|
||||
def test_check_method_caches_result_per_code_object(monkeypatch):
|
||||
calls = []
|
||||
original = reflection._read_function_source
|
||||
monkeypatch.setattr(
|
||||
reflection, "_read_function_source", lambda code: calls.append(code) or original(code)
|
||||
)
|
||||
|
||||
def make():
|
||||
def pass_function():
|
||||
pass
|
||||
|
||||
def docstring_function():
|
||||
"""This is a docstring."""
|
||||
return pass_function
|
||||
|
||||
def ellipsis_function():
|
||||
...
|
||||
|
||||
def not_implemented_function():
|
||||
raise NotImplementedError
|
||||
|
||||
def not_implemented_function_with_call():
|
||||
raise NotImplementedError()
|
||||
|
||||
async def multiple_lines_async_def(_param1: str,
|
||||
_param2: str):
|
||||
pass
|
||||
|
||||
def empty_function():
|
||||
return
|
||||
|
||||
self.assertTrue(ObjectUtils.check_method(implemented_function))
|
||||
self.assertFalse(ObjectUtils.check_method(pass_function))
|
||||
self.assertFalse(ObjectUtils.check_method(docstring_function))
|
||||
self.assertFalse(ObjectUtils.check_method(ellipsis_function))
|
||||
self.assertFalse(ObjectUtils.check_method(not_implemented_function))
|
||||
self.assertFalse(ObjectUtils.check_method(not_implemented_function_with_call))
|
||||
self.assertFalse(ObjectUtils.check_method(multiple_lines_async_def))
|
||||
self.assertTrue(ObjectUtils.check_method(empty_function))
|
||||
# 两个闭包共享同一代码对象,只读取一次源码
|
||||
first, second = make(), make()
|
||||
assert not ObjectUtils.check_method(first)
|
||||
assert not ObjectUtils.check_method(second)
|
||||
assert calls == [first.__code__]
|
||||
|
||||
Reference in New Issue
Block a user