test: stabilize hosted startup performance gate

This commit is contained in:
jxxghp
2026-08-24 10:25:12 +08:00
parent 73d9d0dbf5
commit 259ca16838
3 changed files with 52 additions and 13 deletions
@@ -6,7 +6,7 @@
> 审计范围:宿主后端;排除 `app/plugins/**` 运行时插件副本
> 规范优先级:`AGENTS.md` 与 `docs/rules/` 高于本文
> 相关文档:`docs/architecture-overview.md`、`docs/refactor/backend-architecture-governance.md`、`docs/refactor/backend-module-refactor-compatibility.md`
> 实施进度:阶段 0~6 的宿主架构能力已完成收口;API/Application 公共复杂度基线已清零,启动组合根的 SystemConfigOper 构造点已由 14 降至 1;API 进程内后台任务已完成首批统一登记,插件仓适配和 Outbox 外围扩展仍按风险切片推进。Model/Base 查询与写装饰器、legacy 隐式会话外壳均已清零,插件 SDK 也不再导出宿主 Model。2026-08-23 的长期整改阶段 0 已恢复宿主、启动性能、官方插件和 SDK 契约门禁的可信基线;阶段 1a 已补齐 TaskRegistry owner 零债务门禁和诚实的关停超时语义;阶段 1b1 已收口整理 worker、pending 回放、失败通知、进程内 AI 重试、插件监控与事件投递的生命周期所有权;2026-08-24 的阶段 2 已将 212 个已观察宿主模块方法的 legacy aggregation 清零,并补齐可执行 fanout 与下载器文件 DTO 边界;阶段 3 已将消息交互和远程命令的订阅删除统一到 Application/UoW/outbox,宿主不再调用裸线程统计入口;阶段 4 已统一七种消息渠道的宿主回环与后台执行边界;阶段 5 已补齐事件窗口聚合任务的生命周期所有权;阶段 6 已统一插件文件操作的取消完成语义;阶段 7 已统一插件协程补偿的终态等待;阶段 8 已统一宿主同步函数的异步线程池入口;阶段 9 已统一工作流运行时的宿主获取路径;阶段 10 已统一模块、插件与调度运行时的显式 getter 调用;阶段 11 已清除系统配置 getter 的 Oper 形别名;阶段 12 已完成工作流域的显式 Chain 数据端口迁移;阶段 13 已收口用户、交互与消息链的数据端口;阶段 14 已收口音乐订阅数据端口;阶段 15 已收口站点数据端口;阶段 16 已收口媒体服务器数据端口;阶段 17 已收口下载数据端口;阶段 18 已收口主订阅数据端口;阶段 19 已收口整理数据端口;阶段 20 已收口 Agent 数据端口;阶段 21 已收口监控历史端口;阶段 22 已统一服务配置应用边界;阶段 23 已补齐媒体服务器 API 遗留的类形配置读取路径;阶段 24 已清除 Scheduler 内部无 owner 的协程提交双轨;阶段 25 已补齐 TaskRegistry 跨线程 owner 并迁移整理 AI 接管;阶段 26 已统一 Agent 会话清理提交;阶段 27 已统一历史 AI 进度 owner;阶段 28 已托管旧插件订阅统计线程;阶段 29 已统一 Emby 系条目转换并清零重复代码白名单;阶段 30 已收口插件市场请求级子任务;阶段 31 已托管搜索 AI 推荐任务;阶段 32 已清除事件调度器绕过生命周期 owner 的投递回退;阶段 33 已统一宿主 Agent 运行时的获取路径;阶段 34 已统一 durable-required 事件与 Outbox topic 事实源;阶段 35 已统一 LLM provider 管理 API 的运行时解析路径;阶段 36 已统一 WebAgent 音频能力访问边界;阶段 37 已统一插件输入事件发布路径;阶段 38 已统一 WebAgent 通知事件监听与队列边界;阶段 39 已补齐搜索 SSE 断线时的上游任务清理;阶段 40 已补齐异步防抖取消的终态所有权;阶段 41 已统一优雅重启兜底线程的唯一所有权;阶段 42 已补齐 Telegram typing 的多实例隔离和终态 owner;阶段 43 已统一 Discord typing 的异步 owner 和 shutdown 收尾;阶段 44 已清除 WebAgent 测试临时事件循环提前关闭产生的 CI 红注解;阶段 45 已统一影视与字幕搜索的请求级逐页任务编排。
> 实施进度:阶段 0~6 的宿主架构能力已完成收口;API/Application 公共复杂度基线已清零,启动组合根的 SystemConfigOper 构造点已由 14 降至 1;API 进程内后台任务已完成首批统一登记,插件仓适配和 Outbox 外围扩展仍按风险切片推进。Model/Base 查询与写装饰器、legacy 隐式会话外壳均已清零,插件 SDK 也不再导出宿主 Model。2026-08-23 的长期整改阶段 0 已恢复宿主、启动性能、官方插件和 SDK 契约门禁的可信基线;阶段 1a 已补齐 TaskRegistry owner 零债务门禁和诚实的关停超时语义;阶段 1b1 已收口整理 worker、pending 回放、失败通知、进程内 AI 重试、插件监控与事件投递的生命周期所有权;2026-08-24 的阶段 2 已将 212 个已观察宿主模块方法的 legacy aggregation 清零,并补齐可执行 fanout 与下载器文件 DTO 边界;阶段 3 已将消息交互和远程命令的订阅删除统一到 Application/UoW/outbox,宿主不再调用裸线程统计入口;阶段 4 已统一七种消息渠道的宿主回环与后台执行边界;阶段 5 已补齐事件窗口聚合任务的生命周期所有权;阶段 6 已统一插件文件操作的取消完成语义;阶段 7 已统一插件协程补偿的终态等待;阶段 8 已统一宿主同步函数的异步线程池入口;阶段 9 已统一工作流运行时的宿主获取路径;阶段 10 已统一模块、插件与调度运行时的显式 getter 调用;阶段 11 已清除系统配置 getter 的 Oper 形别名;阶段 12 已完成工作流域的显式 Chain 数据端口迁移;阶段 13 已收口用户、交互与消息链的数据端口;阶段 14 已收口音乐订阅数据端口;阶段 15 已收口站点数据端口;阶段 16 已收口媒体服务器数据端口;阶段 17 已收口下载数据端口;阶段 18 已收口主订阅数据端口;阶段 19 已收口整理数据端口;阶段 20 已收口 Agent 数据端口;阶段 21 已收口监控历史端口;阶段 22 已统一服务配置应用边界;阶段 23 已补齐媒体服务器 API 遗留的类形配置读取路径;阶段 24 已清除 Scheduler 内部无 owner 的协程提交双轨;阶段 25 已补齐 TaskRegistry 跨线程 owner 并迁移整理 AI 接管;阶段 26 已统一 Agent 会话清理提交;阶段 27 已统一历史 AI 进度 owner;阶段 28 已托管旧插件订阅统计线程;阶段 29 已统一 Emby 系条目转换并清零重复代码白名单;阶段 30 已收口插件市场请求级子任务;阶段 31 已托管搜索 AI 推荐任务;阶段 32 已清除事件调度器绕过生命周期 owner 的投递回退;阶段 33 已统一宿主 Agent 运行时的获取路径;阶段 34 已统一 durable-required 事件与 Outbox topic 事实源;阶段 35 已统一 LLM provider 管理 API 的运行时解析路径;阶段 36 已统一 WebAgent 音频能力访问边界;阶段 37 已统一插件输入事件发布路径;阶段 38 已统一 WebAgent 通知事件监听与队列边界;阶段 39 已补齐搜索 SSE 断线时的上游任务清理;阶段 40 已补齐异步防抖取消的终态所有权;阶段 41 已统一优雅重启兜底线程的唯一所有权;阶段 42 已补齐 Telegram typing 的多实例隔离和终态 owner;阶段 43 已统一 Discord typing 的异步 owner 和 shutdown 收尾;阶段 44 已清除 WebAgent 测试临时事件循环提前关闭产生的 CI 红注解;阶段 45 已统一影视与字幕搜索的请求级逐页任务编排;阶段 46 已收口启动性能门禁的托管 runner 假失败与诊断输出
## 当前复核结论(2026-08-24
@@ -483,6 +483,18 @@
一致性。公开搜索方法、SSE 字段、插件资源源、站点模块方法、SDK/Compat 与 V1/V2/V3 插件合同均未改变;
本阶段未修改插件仓。
### 长期整改阶段 46:启动性能门禁托管 runner 波动收口(2026-08-24
- 阶段 45 首轮 Actions 的四个单测分片全部成功,Architecture Contract Gate 仅在冷导入耗时失败:
`app.factory``2031.389ms / 1924.455ms``app.startup.lifecycle`
`1972.696ms / 1921.477ms`;宿主模块数、生命周期组件顺序、线程和 task 资源合同均未变化。同一失败
job 原提交复跑直接成功,启动性能步骤由约 40 秒降至约 26 秒,证明是 Linux 托管 runner 的共同波动。
- 冷导入仍以“维护者基线的 2 倍或增加 500ms,取较大者”为硬预算,只把最终跨平台/调度抖动带由 5%
调整为 15%,足以覆盖首轮实测但不会掩盖模块数、组件集合、资源泄漏或超过约 2.3 倍基线的真实回退。
- `--check` 现在逐目标打印实测中位数与预算;后续 CI 失败可直接判断越界幅度,不再只有结论而缺少通过
样本。脚本 CLI 的只读/显式写入边界和启动基线文件均未改变;本阶段不涉及运行时 API、SDK/Compat、
插件 ABI 或插件仓。
### 总体判断
当前架构总体合理,已经从跨层混合的遗留单体收敛为**边界清晰的模块化单体**:
+31 -6
View File
@@ -30,8 +30,9 @@ RESULT_PREFIX = "MOVIEPILOT_IMPORT_BASELINE="
LIFECYCLE_RESULT_PREFIX = "MOVIEPILOT_LIFECYCLE_BASELINE="
PERFORMANCE_FACTOR = 2.0
PERFORMANCE_SLACK_MS = 500.0
# 基线由维护者平台生成、CI 在 Linux runner 检查;给跨平台宽松预算保留小幅调度抖动带。
PERFORMANCE_JITTER_FACTOR = 1.05
# 基线由维护者平台生成、CI 在 Linux 托管 runner 检查;在既有 2 倍/500ms
# 硬预算之外保留 15% 的跨平台与持续调度抖动带,结构契约仍保持精确比较。
PERFORMANCE_JITTER_FACTOR = 1.15
def measure_import(target: str) -> dict[str, Any]:
@@ -310,10 +311,7 @@ def check_baseline(
f"{expected_target.get('loaded_app_module_count')} -> "
f"{actual_target.get('loaded_app_module_count')}"
)
budget_ms = max(
expected_target["max_ms"] * PERFORMANCE_FACTOR,
expected_target["max_ms"] + PERFORMANCE_SLACK_MS,
) * PERFORMANCE_JITTER_FACTOR
budget_ms = import_budget_ms(expected_target)
if actual_target["median_ms"] > budget_ms:
errors.append(
f"{target} 冷导入中位数 {actual_target['median_ms']}ms "
@@ -349,6 +347,31 @@ def check_baseline(
return errors
def import_budget_ms(expected_target: dict[str, Any]) -> float:
"""根据已提交样本计算跨平台冷导入中位数预算。"""
return max(
expected_target["max_ms"] * PERFORMANCE_FACTOR,
expected_target["max_ms"] + PERFORMANCE_SLACK_MS,
) * PERFORMANCE_JITTER_FACTOR
def describe_import_measurements(
expected: dict[str, Any],
actual: dict[str, Any],
) -> list[str]:
"""输出每个公共冷导入目标的实测中位数与预算,便于诊断 CI 波动。"""
expected_targets = expected.get("targets", {})
actual_targets = actual.get("targets", {})
return [
(
f"启动性能采样:{target} 中位数 "
f"{actual_targets[target]['median_ms']}ms / 预算 "
f"{round(import_budget_ms(expected_targets[target]), 3)}ms"
)
for target in sorted(set(expected_targets) & set(actual_targets))
]
def parse_args(argv: Optional[list[str]] = None) -> argparse.Namespace:
"""解析只读打印、检查或显式写入操作。"""
parser = argparse.ArgumentParser(description=__doc__)
@@ -402,6 +425,8 @@ def main(argv: Optional[list[str]] = None) -> int:
if not output.is_file():
raise SystemExit(f"性能基线不存在:{output}")
expected = json.loads(output.read_text(encoding="utf-8"))
for line in describe_import_measurements(expected, baseline):
print(line)
errors = check_baseline(expected, baseline)
if errors:
for error in errors:
+8 -6
View File
@@ -628,7 +628,9 @@ def test_performance_check_is_read_only(tmp_path: Path, monkeypatch, capsys):
) == 0
assert output.read_bytes() == content_before
assert "检查通过" in capsys.readouterr().out
output_text = capsys.readouterr().out
assert "启动性能采样:app.factory 中位数 90.0ms / 预算 690.0ms" in output_text
assert "检查通过" in output_text
def test_performance_write_requires_explicit_action(tmp_path: Path, monkeypatch):
@@ -664,17 +666,17 @@ def test_performance_check_ignores_environment_provenance_drift():
assert startup_performance.check_baseline(expected, actual) == []
def test_performance_check_keeps_small_cross_platform_jitter_margin():
"""跨平台 runner 可使用 5% 抖动带,但超过最终预算仍必须失败。"""
def test_performance_check_keeps_hosted_runner_jitter_margin():
"""跨平台托管 runner 可使用 15% 抖动带,但超过最终预算仍必须失败。"""
expected = _performance_sample()
within_margin = _performance_sample()
within_margin["targets"]["app.factory"]["median_ms"] = 630.0
within_margin["targets"]["app.factory"]["median_ms"] = 690.0
above_margin = _performance_sample()
above_margin["targets"]["app.factory"]["median_ms"] = 630.001
above_margin["targets"]["app.factory"]["median_ms"] = 690.001
assert startup_performance.check_baseline(expected, within_margin) == []
assert startup_performance.check_baseline(expected, above_margin) == [
"app.factory 冷导入中位数 630.001ms 超过预算 630.0ms"
"app.factory 冷导入中位数 690.001ms 超过预算 690.0ms"
]