Files
MoviePilot/scripts/perf/README.md
T
InfinityPacerandjxxghp be7dfd77a3 fix(runtime): 收敛模块关闭线程所有权 (#6443)
* fix(runtime): bound module shutdown owners

* fix(runtime): declare blocking shutdown owners

---------

Co-authored-by: jxxghp <jxxghp@gmail.com>
2026-08-25 06:58:31 +08:00

278 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MoviePilot Docker A/B Harness
该工具用同一个冻结 Docker substrate 对两个 Git commit 做源码级 A/B。派生镜像会先清空
`/app`,复制目标 commit 的完整 `git archive`,再从 substrate 注入镜像构建阶段生成的插件目录、
`sites.*.so``user.sites.v3.bin`。如果依赖或 Docker substrate 输入发生变化,工具会拒绝继续,
避免把外部构建输入漂移误算成性能收益。
工具不会读取工作区或容器内的 `app.env`,不挂载真实配置、媒体目录或 Docker socket。固定配置只使用
内置实验室占位凭据,样本不发布主机端口,并运行在无外网的 internal network。
## 一次性预热浏览器
默认从固定命名 volume `mp-perf-v3-browser-seed` 复制 CloakBrowser 缓存。该 volume 只需联网预热
一次:
```bash
docker volume create mp-perf-v3-browser-seed
docker run --rm \
--mount source=mp-perf-v3-browser-seed,target=/moviepilot/.cloakbrowser \
--entrypoint python3 \
jxxghp/moviepilot-v3@sha256:925de1fdf1bb0312144bc818bc8ebaa999a9a159c6d14f1b48b0ff05edb7f720 \
-m cloakbrowser install
```
也可以在 `seed``run` 时显式传入 `--allow-browser-download`,但该选项会让 seed 阶段联网;
正式 Before/After 样本仍然只克隆预热结果并使用 internal network。
## 分阶段执行
所有全局参数必须放在子命令前。结果默认写到系统临时目录下的
`moviepilot-perf-results/<campaign>/`
```bash
PYTHON=../.venv/bin/python
CAMPAIGN=v3-perf-001
${PYTHON} scripts/perf/moviepilot_docker_ab.py \
--campaign "${CAMPAIGN}" \
build --before-ref upstream/v3 --after-ref HEAD
${PYTHON} scripts/perf/moviepilot_docker_ab.py \
--campaign "${CAMPAIGN}" \
seed
${PYTHON} scripts/perf/moviepilot_docker_ab.py \
--campaign "${CAMPAIGN}" \
sample --variant before --index 1 --points 1,5,10,30
```
开发 harness 时可以用小数分钟做短冒烟,例如 `--points 0,0.02`。正式数据必须保持
`1,5,10,30`
未指定 `--scenario` 时仍使用 `idle-default`,样本目录和 Docker 资源名称与既有命令保持一致。
## 浏览器激活场景
浏览器场景使用 campaign browser seed 的独立克隆卷,不直接挂载或写入固定来源卷,也不会在样本
阶段下载浏览器。容器保持 internal network;探针只发送信号,`app.sdk.browser` 的导入、浏览器上下文
创建和本地 `data:` 页面校验都发生在主 MoviePilot Python 进程中。
非默认浏览器场景用于候选实现的 After 激活门禁;Before 不具备新 SDK,且旧实现启动时已经常驻
Xvfb,因此不能用同一个 `0 → 0` / `0 → 1` 不变量衡量。三轮 Before/After 空载收益仍由默认
`run``idle-default` 场景完成,浏览器场景用三个隔离的 After sample 记录冷激活成本。
```bash
../.venv/bin/python scripts/perf/moviepilot_docker_ab.py \
--campaign v3-perf-002-headless \
sample --variant after --index 1 --scenario browser-headless --points 1,5,10,30
../.venv/bin/python scripts/perf/moviepilot_docker_ab.py \
--campaign v3-perf-002-headed \
sample --variant after --index 1 --scenario browser-headed --points 1,5,10,30
```
- `browser-headless`:一次真实 headless context 激活,要求 Xvfb `0 → 0`
- `browser-headed`:主进程内两个线程通过屏障并发调用
`launch_browser_context(headless=False)`;要求两个真实 SDK 冷启动调用成功、额外上下文关闭后只保留一个、
Capability observation 只有一个 `headed_browser_launch` generation/start,且 Xvfb `0 → 1`
- 激活完成后再开始 `1/5/10/30m` 计时,JSON 保留激活前后 Engine 网络、working set、进程
PSS/USS/RSS/线程、Xvfb 数量/PSS、`sys.modules` 和进程内 marker
- 非默认场景结果保存在 `samples/<scenario>/<variant>-<index>/`,可与同 campaign 的 idle 样本并存,
Markdown 中位数会按场景分组,不会混算。
## Agent 惰性物化场景
PERF-003 在既有 `AI_AGENT_ENABLE=false` 固定配置下增加两个 After-only 场景。探针只向主 MoviePilot
Python 进程发送信号;OpenAPI 生成和工具目录构造均发生在该解释器内,不通过 `docker exec` 启动
第二个 Python,也不调用真实 Agent、LLM provider 或外部 MCP。
先以 `f2e548e1` 冻结 Before,候选提交完成后把 `AFTER_COMMIT` 替换为其精确 commit
```bash
../.venv/bin/python scripts/perf/moviepilot_docker_ab.py \
--campaign v3-perf-003 \
build --before-ref f2e548e1 --after-ref AFTER_COMMIT
../.venv/bin/python scripts/perf/moviepilot_docker_ab.py \
--campaign v3-perf-003 \
seed --browser-source-volume mp-perf-v3-browser-seed --replace
```
正式 idle-default 三组 A/B 仍使用原 `run` 合同;下面两个动作场景在同一 build/seed 后单独采 After
不会覆盖 idle 结果:
```bash
../.venv/bin/python scripts/perf/moviepilot_docker_ab.py \
--campaign v3-perf-003 \
run --before-ref f2e548e1 --after-ref AFTER_COMMIT \
--browser-source-volume mp-perf-v3-browser-seed \
--points 1,5,10,30 --replace --keep-resources
../.venv/bin/python scripts/perf/moviepilot_docker_ab.py \
--campaign v3-perf-003 \
sample --variant after --index 1 --scenario agent-disabled-router --points 1,5,10,30
../.venv/bin/python scripts/perf/moviepilot_docker_ab.py \
--campaign v3-perf-003 \
sample --variant after --index 2 --scenario agent-tool-catalog --points 1,5,10,30
```
- `agent-disabled-router`:直接从主进程 FastAPI app 生成完整 OpenAPI,确认 Agent、LLM、MCP、OpenAI、
Anthropic 路由在禁用态仍存在,同时 callback、LLM helper、工具域、orchestrator、LangGraph 和 provider SDK
前后保持 0,工具工厂不物化;
- `agent-tool-catalog`:通过主进程已有的 `moviepilot_tool_manager.list_tools()` 首次构建现有工具目录和 JSON Schema
要求动作前工具域未物化,动作后仅工具 base/catalog/factory/impl 物化;目录还必须无身份碰撞、Schema
digest 完整,重复读取复用同一 snapshot/revision。结果记录工具数、Schema 摘要、plugin revision 与
factory revision
- 固定哨兵覆盖 `app.agent.orchestrator``app.agent.callback``app.agent.llm.helper`、工具
`base/catalog/factory/impl``langgraph``langchain``langchain_core``openai``anthropic`
`google.genai``boto3``botocore`。其中 `langchain/langchain_core` 可能由完整 Schema 聚合形成既有
基线,只记录数量与变化,不作为禁用态归零门禁;
- JSON 保留动作前后 Engine、PSS/USS、线程、完整 `sys.modules`、materialization observation、revision、
网络累计值和浏览器卷指纹;动作前后容器网络收发必须为 0,Markdown 另汇总 Agent 场景与各定时点的
模块哨兵峰值;
- 启用态 Agent 生命周期不会在该无凭据场景中伪造。现有 `get_running_agent_manager()` 是严格只读、
non-materializing 的运行态 getter`begin_agent_shutdown()` 也只是关闭轴;二者都不是安全启用入口。
启用态必须由正式 startup/service lifecycle 驱动,只有宿主形成明确不创建 provider/client、不会外联的
公共初始化合同后,才适合加入同一测量门禁。
## 完整三组 A/B
```bash
../.venv/bin/python scripts/perf/moviepilot_docker_ab.py \
--campaign v3-perf-001 \
run \
--before-ref upstream/v3 \
--after-ref HEAD \
--points 1,5,10,30
```
执行顺序固定为:
```text
Before-1 → After-1 → After-2 → Before-2 → Before-3 → After-3
```
每个样本都从 SQLite 和浏览器 seed 克隆新的命名 volume。样本结束后立即移除容器和样本卷;
完整 `run` 结束后还会移除 campaign seed 与 internal network。派生镜像和本地结果保留,便于复核。
## 输出
```text
<output>/<campaign>/
├── build.json
├── seed.json
├── results.json
├── report.md
└── samples/
├── before-1/
│ ├── result.json
│ ├── container.log
│ └── modules/
└── ...
```
- `results.json`Engine stats、进程 PSS/USS/RSS/线程、网络累计值和模块前缀计数;
- `report.md`:三次原值与中位数 Before/After 汇总;
- `modules/`:由目标 MoviePilot Python 进程自身写出的完整 `sys.modules` 名称清单;
- `container.log`:已移除实验室凭据值和本地实例 UUID。
容器 working set 统一按 Docker Engine API 的
`memory_stats.usage - memory_stats.stats.inactive_file` 计算。进程 PSS 仅用于归因,不能替代容器指标。
## 清理
清理严格限定到 campaign 标签;不会执行 Docker prune
```bash
../.venv/bin/python scripts/perf/moviepilot_docker_ab.py \
--campaign v3-perf-001 cleanup --images
```
本地 JSON、Markdown 和日志不会被 `cleanup` 删除。
## Python 3.14 free-threaded 镜像 A/B
`free_threaded_ab.py` 用于正式发布前在同一 Docker daemon、相同 CPU/内存限制下比较
`moviepilot-v3``moviepilot-v3t`。它不构建镜像,只接受两份
`repository@sha256:<digest>` 不可变引用,并要求镜像标签证明两者来自相同源码 revision 和版本。
未使用 `--pull` 时,digest 也可以是本机 Docker image ID,供依赖尚未发布前验收本地候选。
preflight 会验证 Python 3.14、GIL 状态、`thread_inherit_context`、MoviePilot-Rust 0.3 的
`jieba_cut`/中文转换入口,以及标准与 free-threaded 镜像互斥的原生依赖 profile。正式样本使用
固定 seed 和 fixture hash,按 `v3-1 → v3t-1 → v3t-2 → v3-2 → v3-3 → v3t-3`
交替执行真实 readiness 启动,并把应用识别热点明确分成 `V3 + Python``V3 + Rust`
`V3t + Rust` 三组。两种镜像的纯 Python 并发与直接 Rust 并发是解释器/ABI 探针,不代表产品 Rust
开关的第三组结果;PostgreSQL 驱动选择使用不连接数据库的命令单独验证。
```bash
../.venv/bin/python scripts/perf/free_threaded_ab.py \
--campaign v3-ft-001 \
--standard-image 'jxxghp/moviepilot-v3@sha256:<64-hex-digest>' \
--free-threaded-image 'jxxghp/moviepilot-v3t@sha256:<64-hex-digest>' \
--pull
```
结果默认写入系统临时目录的 `moviepilot-free-threaded-ab/<campaign>/`
- `results.json` 使用 `schema_version` 保存镜像身份、preflight、阈值、原始样本与中位数;
- `report.md` 提供维护者可读摘要;
- `samples/` 保存六个交替样本,便于排查离群值。
退出码 `0` 表示合同与性能阈值通过,`1` 表示样本有效但出现性能回退,`2` 表示 digest、ABI、
依赖、语义、驱动、启动或样本完整性不成立。该工具只用于隔离的本地长 A/B,不接真实凭据、用户数据库、
媒体目录或外网,也不加入常规 CI。
## 模块关闭事件循环 A/B
`module_shutdown_ab.py` 使用隔离资源 owner,分别测量 `stop_modules()` 内部同步关闭和生命周期总入口的
同步关闭。默认制造 50ms 同步等待,并观察 10ms 心跳是否在关闭完成前执行:
```bash
../.venv-test/bin/python scripts/perf/module_shutdown_ab.py \
--block-ms 50 \
--heartbeat-ms 10 \
--samples 7
```
分别在 Before/After revision 运行相同参数并保存 JSON。总关闭耗时应保持接近固定等待时间;心跳中位延迟
用于判断事件循环是否被同步 owner 占用,所有样本在关闭完成前执行心跳属于正确性门禁。探针不启动真实
模块、线程池、数据库、配置或网络。
## TaskRegistry 跨线程提交 A/B
`task_registry_ab.py` 验证目标事件循环尚未分发 callback 时执行 shutdownpending completion 与原始
coroutine 是否取得明确终态,同时采集跨线程提交最小协程的提交和完成耗时。分别在 Before/After revision
运行相同参数并保留两份 JSON,即可比较正确性与固定负载开销:
```bash
../.venv-test/bin/python scripts/perf/task_registry_ab.py \
--iterations 2000 \
--samples 7
```
该探针不访问数据库、配置或网络。吞吐结果用于识别可重复回退,不作为跨机器性能阈值;pending completion
取消且 coroutine 关闭属于正确性门禁。
### PostgreSQL 同步驱动三方案
`postgresql_driver_ab.py` 在同一 PostgreSQL 容器中比较标准 V3/psycopg2、标准
V3/psycopg3 binary 和 V3t/psycopg3 C。三个输入镜像必须来自相同源码 revision 和产品版本;
标准 V3/psycopg3 镜像是只增加该驱动的本地验证衍生镜像,不是发布制品。
```bash
../.venv/bin/python scripts/perf/postgresql_driver_ab.py \
--campaign v3-ft-pg-001 \
--postgres-container moviepilot-pg-ab \
--dsn 'postgresql://moviepilot:<benchmark-password>@127.0.0.1:5432/moviepilot' \
--standard-image 'moviepilot-v3@sha256:<64-hex-digest>' \
--standard-psycopg3-image 'moviepilot-v3-pg3@sha256:<64-hex-digest>' \
--free-threaded-image 'moviepilot-v3t@sha256:<64-hex-digest>'
```
脚本按三方案的六个全排列执行固定 SQL,默认把每个采样容器限制为 2 CPU/1 GiB,保存单连接查询、
16 线程查询、批量事务、长事务行锁并行、驱动/libpq/SOABI 和 GIL 状态。每个 campaign 使用独立
测试表并在成功或失败后清理;DSN 只传入隔离容器,不写入结果。性能数据用于解释驱动选择,不作为
跨机器发布阈值;驱动实现、GIL、查询结果、长事务并行和样本完整性属于硬门禁。