mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-31 12:24:39 +08:00
fix(runtime): strip inherited jemalloc background thread config for subprocesses
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
InfinityPacer
parent
31ca107e41
commit
7191b23555
@@ -205,8 +205,8 @@ def run_application() -> None:
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
# 只在主进程开启分配器后台回收,浏览器等子进程不继承
|
||||
from app.runtime.gc import enable_allocator_background_thread
|
||||
enable_allocator_background_thread()
|
||||
from app.runtime.gc import configure_allocator_background_thread
|
||||
configure_allocator_background_thread()
|
||||
|
||||
start_tray()
|
||||
run_api_server()
|
||||
|
||||
@@ -92,6 +92,8 @@ _JEMALLOC_THREAD_TCACHE_FLUSH = b"thread.tcache.flush"
|
||||
# 后台回收线程开关与数量上限,均可在运行时写入
|
||||
_JEMALLOC_BACKGROUND_THREAD = b"background_thread"
|
||||
_JEMALLOC_MAX_BACKGROUND_THREADS = b"max_background_threads"
|
||||
# MALLOC_CONF 中与后台线程相关、不能被子进程继承的配置项
|
||||
_BACKGROUND_THREAD_OPTIONS = frozenset({"background_thread", "max_background_threads"})
|
||||
|
||||
|
||||
@functools.cache
|
||||
@@ -137,21 +139,25 @@ def flush_thread_allocator_cache() -> bool:
|
||||
return mallctl(_JEMALLOC_THREAD_TCACHE_FLUSH, None, None, None, 0) == 0
|
||||
|
||||
|
||||
def enable_allocator_background_thread(max_threads: int = 1) -> bool:
|
||||
def configure_allocator_background_thread(max_threads: int = 1) -> bool:
|
||||
"""
|
||||
在当前进程内开启 jemalloc 后台回收线程,按衰减时间把空闲脏页归还系统。
|
||||
让 jemalloc 后台回收线程只在主进程运行,并从子进程将继承的 ``MALLOC_CONF`` 中移除相关项。
|
||||
|
||||
后台线程不能写进 ``MALLOC_CONF``:该环境变量与 ``LD_PRELOAD`` 一起被所有子进程继承,
|
||||
Chromium 的 GPU 进程在后台线程开启时无法启动,浏览器仿真会整体 abort。运行时开启只
|
||||
作用于主进程。用户在 ``MALLOC_CONF`` 中显式配置过后台线程时尊重其选择,不再改写。
|
||||
后台线程按衰减时间把空闲脏页归还系统,但 ``MALLOC_CONF`` 与 ``LD_PRELOAD`` 会被所有子进程
|
||||
继承,Chromium 的 GPU 进程在后台线程开启时无法启动,浏览器仿真整体 abort。
|
||||
- ``MALLOC_CONF`` 已配置后台线程(v3.0.10 镜像默认值或用户显式配置)时,主进程启动时已按其生效,不再改写。
|
||||
- 未配置时通过 ``mallctl`` 在主进程内开启。
|
||||
- 两种情况下都从 ``os.environ`` 中去掉后台线程项。内建更新只替换程序代码、不改镜像环境变量,
|
||||
因此必须由代码清理,才能修复从 v3.0.10 镜像内建更新上来的实例。
|
||||
|
||||
Args:
|
||||
max_threads: 后台线程上限
|
||||
|
||||
Returns:
|
||||
是否实际开启
|
||||
是否在运行时实际开启了后台线程
|
||||
"""
|
||||
if "background_thread" in os.environ.get("MALLOC_CONF", ""):
|
||||
configured = _strip_background_thread_conf()
|
||||
if configured:
|
||||
return False
|
||||
mallctl = _jemalloc_mallctl()
|
||||
if mallctl is None:
|
||||
@@ -163,6 +169,22 @@ def enable_allocator_background_thread(max_threads: int = 1) -> bool:
|
||||
return mallctl(_JEMALLOC_BACKGROUND_THREAD, None, None, ctypes.byref(enabled), ctypes.sizeof(enabled)) == 0
|
||||
|
||||
|
||||
def _strip_background_thread_conf() -> bool:
|
||||
"""从 ``os.environ["MALLOC_CONF"]`` 去掉后台线程项,返回原配置是否包含这些项。"""
|
||||
conf = os.environ.get("MALLOC_CONF")
|
||||
if not conf:
|
||||
return False
|
||||
options = [option for option in conf.split(",") if option]
|
||||
kept = [option for option in options if option.split(":", 1)[0] not in _BACKGROUND_THREAD_OPTIONS]
|
||||
if len(kept) == len(options):
|
||||
return False
|
||||
if kept:
|
||||
os.environ["MALLOC_CONF"] = ",".join(kept)
|
||||
else:
|
||||
del os.environ["MALLOC_CONF"]
|
||||
return True
|
||||
|
||||
|
||||
def release_allocator_memory() -> Optional[str]:
|
||||
"""
|
||||
把 C 分配器中已释放但尚未归还的空闲页交还操作系统。
|
||||
|
||||
@@ -149,29 +149,43 @@ def test_thread_cache_flush_is_a_no_op_without_jemalloc(linux, monkeypatch):
|
||||
assert trim.calls == []
|
||||
|
||||
|
||||
|
||||
def test_background_thread_enabled_at_runtime(linux, monkeypatch):
|
||||
"""后台回收线程在主进程内开启,先设上限再打开开关。"""
|
||||
monkeypatch.delenv("MALLOC_CONF", raising=False)
|
||||
"""未配置后台线程时在主进程内开启,先设上限再打开开关,子进程环境保持不变。"""
|
||||
monkeypatch.setenv("MALLOC_CONF", "narenas:8,dirty_decay_ms:5000")
|
||||
mallctl = _FakeSymbol()
|
||||
_use_lib(monkeypatch, _FakeLib(mallctl=mallctl))
|
||||
|
||||
assert runtime_gc.enable_allocator_background_thread() is True
|
||||
assert runtime_gc.configure_allocator_background_thread() is True
|
||||
assert [call[0] for call in mallctl.calls] == [b"max_background_threads", b"background_thread"]
|
||||
assert all(call[1] is None and call[2] is None for call in mallctl.calls)
|
||||
assert runtime_gc.os.environ["MALLOC_CONF"] == "narenas:8,dirty_decay_ms:5000"
|
||||
|
||||
|
||||
def test_background_thread_respects_explicit_malloc_conf(linux, monkeypatch):
|
||||
"""用户在 MALLOC_CONF 里显式配置后台线程时不再改写。"""
|
||||
monkeypatch.setenv("MALLOC_CONF", "background_thread:false")
|
||||
def test_inherited_background_thread_conf_is_stripped(linux, monkeypatch):
|
||||
"""v3.0.10 镜像把后台线程写进了 MALLOC_CONF;主进程已按其生效,只需从子进程将继承的环境中去掉。"""
|
||||
monkeypatch.setenv(
|
||||
"MALLOC_CONF",
|
||||
"background_thread:true,max_background_threads:1,narenas:8,dirty_decay_ms:5000,muzzy_decay_ms:0",
|
||||
)
|
||||
mallctl = _FakeSymbol()
|
||||
_use_lib(monkeypatch, _FakeLib(mallctl=mallctl))
|
||||
|
||||
assert runtime_gc.enable_allocator_background_thread() is False
|
||||
assert runtime_gc.configure_allocator_background_thread() is False
|
||||
assert mallctl.calls == []
|
||||
assert runtime_gc.os.environ["MALLOC_CONF"] == "narenas:8,dirty_decay_ms:5000,muzzy_decay_ms:0"
|
||||
|
||||
|
||||
def test_conf_with_only_background_thread_is_removed(linux, monkeypatch):
|
||||
monkeypatch.setenv("MALLOC_CONF", "background_thread:false")
|
||||
_use_lib(monkeypatch, _FakeLib(mallctl=_FakeSymbol()))
|
||||
|
||||
assert runtime_gc.configure_allocator_background_thread() is False
|
||||
assert "MALLOC_CONF" not in runtime_gc.os.environ
|
||||
|
||||
|
||||
def test_background_thread_is_a_no_op_without_jemalloc(linux, monkeypatch):
|
||||
monkeypatch.delenv("MALLOC_CONF", raising=False)
|
||||
_use_lib(monkeypatch, _FakeLib(malloc_trim=_FakeSymbol()))
|
||||
|
||||
assert runtime_gc.enable_allocator_background_thread() is False
|
||||
assert runtime_gc.configure_allocator_background_thread() is False
|
||||
|
||||
Reference in New Issue
Block a user