mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-03 22:04:57 +08:00
fix(docker): enable jemalloc background thread only in the main process
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
InfinityPacer
parent
3b2cd28983
commit
31ca107e41
@@ -204,6 +204,9 @@ 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()
|
||||
|
||||
start_tray()
|
||||
run_api_server()
|
||||
|
||||
@@ -89,6 +89,9 @@ def get_memory_usage() -> float:
|
||||
_JEMALLOC_ARENAS_ALL_PURGE = b"arena.4096.purge"
|
||||
# 把调用线程的线程缓存(tcache)交还所属 arena
|
||||
_JEMALLOC_THREAD_TCACHE_FLUSH = b"thread.tcache.flush"
|
||||
# 后台回收线程开关与数量上限,均可在运行时写入
|
||||
_JEMALLOC_BACKGROUND_THREAD = b"background_thread"
|
||||
_JEMALLOC_MAX_BACKGROUND_THREADS = b"max_background_threads"
|
||||
|
||||
|
||||
@functools.cache
|
||||
@@ -134,6 +137,32 @@ 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:
|
||||
"""
|
||||
在当前进程内开启 jemalloc 后台回收线程,按衰减时间把空闲脏页归还系统。
|
||||
|
||||
后台线程不能写进 ``MALLOC_CONF``:该环境变量与 ``LD_PRELOAD`` 一起被所有子进程继承,
|
||||
Chromium 的 GPU 进程在后台线程开启时无法启动,浏览器仿真会整体 abort。运行时开启只
|
||||
作用于主进程。用户在 ``MALLOC_CONF`` 中显式配置过后台线程时尊重其选择,不再改写。
|
||||
|
||||
Args:
|
||||
max_threads: 后台线程上限
|
||||
|
||||
Returns:
|
||||
是否实际开启
|
||||
"""
|
||||
if "background_thread" in os.environ.get("MALLOC_CONF", ""):
|
||||
return False
|
||||
mallctl = _jemalloc_mallctl()
|
||||
if mallctl is None:
|
||||
return False
|
||||
limit = ctypes.c_size_t(max_threads)
|
||||
if mallctl(_JEMALLOC_MAX_BACKGROUND_THREADS, None, None, ctypes.byref(limit), ctypes.sizeof(limit)) != 0:
|
||||
return False
|
||||
enabled = ctypes.c_bool(True)
|
||||
return mallctl(_JEMALLOC_BACKGROUND_THREAD, None, None, ctypes.byref(enabled), ctypes.sizeof(enabled)) == 0
|
||||
|
||||
|
||||
def release_allocator_memory() -> Optional[str]:
|
||||
"""
|
||||
把 C 分配器中已释放但尚未归还的空闲页交还操作系统。
|
||||
|
||||
@@ -285,11 +285,12 @@ RUN mkdir -p \
|
||||
# final 阶段: 安装运行时依赖和配置最终镜像
|
||||
FROM prepare_package AS final
|
||||
|
||||
# jemalloc 默认按 CPU 数×4 建 arena 且没有后台回收线程,空闲 arena 的脏页会长期计入 RSS。
|
||||
# 后台线程按 5 秒衰减归还脏页(muzzy 直接归还,避免 MADV_FREE 页继续计入 RSS);
|
||||
# arena 限制为 8 个,带 GIL 的 Python 大块分配竞争很小。用户可在 compose 中覆盖 MALLOC_CONF。
|
||||
# jemalloc 默认按 CPU 数×4 建 arena,空闲 arena 的脏页会长期计入 RSS。脏页按 5 秒衰减归还
|
||||
# (muzzy 直接归还,避免 MADV_FREE 页继续计入 RSS);arena 限制为 8 个,带 GIL 的 Python 大块分配竞争很小。
|
||||
# 后台回收线程由主进程启动时自行开启,不能写在这里:子进程继承 MALLOC_CONF 后 Chromium 无法启动。
|
||||
# 用户可在 compose 中覆盖 MALLOC_CONF。
|
||||
ENV LD_PRELOAD="/usr/local/lib/libjemalloc.so" \
|
||||
MALLOC_CONF="background_thread:true,max_background_threads:1,narenas:8,dirty_decay_ms:5000,muzzy_decay_ms:0" \
|
||||
MALLOC_CONF="narenas:8,dirty_decay_ms:5000,muzzy_decay_ms:0" \
|
||||
MOVIEPILOT_DOCKER_SUPERVISOR="true"
|
||||
|
||||
# 引入支持 amr 编码的静态 ffmpeg
|
||||
|
||||
@@ -761,7 +761,7 @@ flowchart LR
|
||||
| 指标 | 当前值 |
|
||||
|---|---:|
|
||||
| Python 模块 | 1051 |
|
||||
| 内部导入边 | 8,920 |
|
||||
| 内部导入边 | 8,921 |
|
||||
| 非平凡 SCC | 1(精确 containment 的 TMDB 移植包环) |
|
||||
| Application / Chain 具体 Adapter 直连 | 0 / 0 |
|
||||
| Direct egress | 53(债务已清零,53 条精确 containment) |
|
||||
@@ -771,7 +771,7 @@ flowchart LR
|
||||
| Model/Oper 自动事务与自建 Session | 0 |
|
||||
| 组合根外 `SystemConfigOper()` | 0 |
|
||||
|
||||
整理失败反馈由 `app.application.transfer.feedback` 集中投影;已整理下载的站点字幕复用 `app.chain.transfer` 自动整理队列;网络连通性检测按 `app.application.nettest` 归档,并覆盖 HTTP 与 WebSocket 探测。未启用智能助手的消息音乐交互由 `app.chain.music_interaction` 包中的解析展示与订阅协调模块处理;宿主依赖边现为 8,920,音乐交互包未新增 SCC。Agent 持久回执新增 Application 端口及 DB Model/Oper/Adapter 四个冷导入模块。当前 `app.startup.lifecycle` 为 572、`app.factory` 为 584、`app.main` 为 586。性能基线只同步模块数量,原有耗时预算、历史采样和生命周期资源约束保持有效。
|
||||
整理失败反馈由 `app.application.transfer.feedback` 集中投影;已整理下载的站点字幕复用 `app.chain.transfer` 自动整理队列;网络连通性检测按 `app.application.nettest` 归档,并覆盖 HTTP 与 WebSocket 探测。未启用智能助手的消息音乐交互由 `app.chain.music_interaction` 包中的解析展示与订阅协调模块处理;宿主依赖边现为 8,921,音乐交互包未新增 SCC。Agent 持久回执新增 Application 端口及 DB Model/Oper/Adapter 四个冷导入模块。当前 `app.startup.lifecycle` 为 572、`app.factory` 为 584、`app.main` 为 586。性能基线只同步模块数量,原有耗时预算、历史采样和生命周期资源约束保持有效。
|
||||
|
||||
架构专项验证分为两个 CI 投影:`Check event semantic policy` 先运行依赖、Adapter、出口和 Event
|
||||
语义门禁,`Check host architecture snapshot` 再执行快照测试及一次
|
||||
|
||||
@@ -94,7 +94,7 @@ ARCH-201 至 ARCH-204 均达到实现、验证、提交、推送和远端门禁
|
||||
|
||||
| 指标 | 当前值 | 解释 |
|
||||
|---|---:|---|
|
||||
| 宿主 Python 模块 / 内部依赖边 | 1051 / 8,920 | `dependency-baseline.json` 当前快照;系统设置合同按 `app.application.settings` 包组织,系统设置 API 端点独立按 `app.api.endpoints.settings` 归档;消息音乐交互由 `app.chain.music_interaction` 包中的音乐解析/展示与订阅协调模块处理;已整理下载的站点字幕经 `app.chain.transfer` 自动整理队列完成命名与归档;网络连通性检测按 `app.application.nettest` 归档 |
|
||||
| 宿主 Python 模块 / 内部依赖边 | 1051 / 8,921 | `dependency-baseline.json` 当前快照;系统设置合同按 `app.application.settings` 包组织,系统设置 API 端点独立按 `app.api.endpoints.settings` 归档;消息音乐交互由 `app.chain.music_interaction` 包中的音乐解析/展示与订阅协调模块处理;已整理下载的站点字幕经 `app.chain.transfer` 自动整理队列完成命名与归档;网络连通性检测按 `app.application.nettest` 归档 |
|
||||
| 非平凡 SCC | 1 | 仅保留精确 containment 的 29 模块 TMDB 移植包环 |
|
||||
| 跨层 DB 边界债务 | 0 | Application、Chain、API、Agent、Runtime、Workflow 到 DB 的受控债务均为零 |
|
||||
| Model/Oper 事务债务 | 0 | 自建 Session、自动事务装饰器、直接 commit/rollback 等基线均为零 |
|
||||
|
||||
@@ -1097,8 +1097,8 @@
|
||||
"runtime_only": true
|
||||
}
|
||||
},
|
||||
"edge_count": 8920,
|
||||
"edge_sha256": "68bc7f63a754251cc91fcc5ed5c8652e2492f60faa805f9533eedbccc8517bdd",
|
||||
"edge_count": 8921,
|
||||
"edge_sha256": "e34590c0ec863317ddeec72b19cd935a13b48e51bd56b31f5a3aae93f26a4953",
|
||||
"edges": [
|
||||
"app -> app.foundation",
|
||||
"app -> app.foundation.environment",
|
||||
@@ -6578,6 +6578,7 @@
|
||||
"app.main -> app.adapters.system.stdio",
|
||||
"app.main -> app.factory",
|
||||
"app.main -> app.runtime",
|
||||
"app.main -> app.runtime.gc",
|
||||
"app.main -> app.runtime.settings",
|
||||
"app.main -> app.runtime.stop",
|
||||
"app.main -> app.runtime.topology",
|
||||
|
||||
@@ -147,3 +147,31 @@ def test_thread_cache_flush_is_a_no_op_without_jemalloc(linux, monkeypatch):
|
||||
|
||||
assert runtime_gc.flush_thread_allocator_cache() is False
|
||||
assert trim.calls == []
|
||||
|
||||
|
||||
def test_background_thread_enabled_at_runtime(linux, monkeypatch):
|
||||
"""后台回收线程在主进程内开启,先设上限再打开开关。"""
|
||||
monkeypatch.delenv("MALLOC_CONF", raising=False)
|
||||
mallctl = _FakeSymbol()
|
||||
_use_lib(monkeypatch, _FakeLib(mallctl=mallctl))
|
||||
|
||||
assert runtime_gc.enable_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)
|
||||
|
||||
|
||||
def test_background_thread_respects_explicit_malloc_conf(linux, monkeypatch):
|
||||
"""用户在 MALLOC_CONF 里显式配置后台线程时不再改写。"""
|
||||
monkeypatch.setenv("MALLOC_CONF", "background_thread:false")
|
||||
mallctl = _FakeSymbol()
|
||||
_use_lib(monkeypatch, _FakeLib(mallctl=mallctl))
|
||||
|
||||
assert runtime_gc.enable_allocator_background_thread() is False
|
||||
assert mallctl.calls == []
|
||||
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user