From ba2eb869e2588d83d2bfb7e8f151d7ef33501b07 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sun, 23 Aug 2026 23:42:04 +0800 Subject: [PATCH] fix(ci): isolate startup performance resources --- app/testing/bootstrap.py | 22 +- scripts/startup/performance.py | 7 +- .../startup-performance-baseline.json | 290 +++++++++--------- tests/test_testing_bootstrap.py | 25 ++ 4 files changed, 191 insertions(+), 153 deletions(-) diff --git a/app/testing/bootstrap.py b/app/testing/bootstrap.py index 4062d8198..df2c501b4 100644 --- a/app/testing/bootstrap.py +++ b/app/testing/bootstrap.py @@ -136,12 +136,22 @@ def ensure_sites_stub() -> None: try: import app.application.site.sites # noqa: F401 本地已拉取时用真实模块 except (ModuleNotFoundError, ImportError): - from importlib.util import spec_from_loader - from types import ModuleType - stub = ModuleType("app.application.site.sites") - stub.SitesHelper = _SitesHelperStub - stub.__spec__ = spec_from_loader("app.application.site.sites", None) - sys.modules["app.application.site.sites"] = stub + install_sites_stub() + + +def install_sites_stub() -> None: + """强制安装确定性的站点资源垫片,供隔离探针排除本机动态资源差异。 + + 与 :func:`ensure_sites_stub` 的“真实资源优先”语义不同,性能与架构探针需要在开发机和 + source-only CI 中使用完全相同的 import 前提,因此必须在导入被测宿主模块前覆盖资源模块。 + """ + from importlib.util import spec_from_loader + from types import ModuleType + + stub = ModuleType("app.application.site.sites") + setattr(stub, "SitesHelper", _SitesHelperStub) + stub.__spec__ = spec_from_loader("app.application.site.sites", None) + sys.modules["app.application.site.sites"] = stub def ensure_optional_stub(name: str, **attrs) -> None: diff --git a/scripts/startup/performance.py b/scripts/startup/performance.py index 8bbe8bcfb..c6fe58170 100644 --- a/scripts/startup/performance.py +++ b/scripts/startup/performance.py @@ -40,6 +40,9 @@ import json import sys import time +from app.testing.bootstrap import install_sites_stub + +install_sites_stub() before = set(sys.modules) started_at = time.perf_counter() importlib.import_module({target!r}) @@ -94,9 +97,9 @@ import time from fastapi import FastAPI -from app.testing.bootstrap import ensure_sites_stub +from app.testing.bootstrap import install_sites_stub -ensure_sites_stub() +install_sites_stub() from app.startup import lifecycle diff --git a/tests/fixtures/architecture/startup-performance-baseline.json b/tests/fixtures/architecture/startup-performance-baseline.json index fab94f39d..6eccdcecf 100644 --- a/tests/fixtures/architecture/startup-performance-baseline.json +++ b/tests/fixtures/architecture/startup-performance-baseline.json @@ -1,41 +1,41 @@ { "schema_version": 2, - "generated_at": "2026-08-23T15:32:22.484415+00:00", + "generated_at": "2026-08-23T15:39:20.210777+00:00", "platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O", "python": "3.14.3", "repeat": 3, "targets": { "app.startup.lifecycle": { - "loaded_app_module_count": 366, - "max_ms": 985.623, - "median_ms": 947.803, - "min_ms": 932.937, + "loaded_app_module_count": 355, + "max_ms": 914.989, + "median_ms": 909.991, + "min_ms": 907.871, "samples_ms": [ - 947.803, - 985.623, - 932.937 + 914.989, + 907.871, + 909.991 ] }, "app.factory": { - "loaded_app_module_count": 378, - "max_ms": 957.03, - "median_ms": 950.108, - "min_ms": 947.713, + "loaded_app_module_count": 367, + "max_ms": 916.407, + "median_ms": 914.085, + "min_ms": 913.804, "samples_ms": [ - 950.108, - 957.03, - 947.713 + 916.407, + 913.804, + 914.085 ] }, "app.main": { - "loaded_app_module_count": 380, - "max_ms": 1098.929, - "median_ms": 1095.482, - "min_ms": 1076.479, + "loaded_app_module_count": 369, + "max_ms": 1096.074, + "median_ms": 1051.665, + "min_ms": 1050.168, "samples_ms": [ - 1076.479, - 1095.482, - 1098.929 + 1051.665, + 1050.168, + 1096.074 ] } }, @@ -47,87 +47,87 @@ { "mode": "normal", "enabled_component_count": 23, - "startup_ms": 0.622, - "full_lifespan_ms": 0.781, + "startup_ms": 0.672, + "full_lifespan_ms": 0.831, "stage_ms": { - "后台任务登记器": 0.076, + "后台任务登记器": 0.081, "数据库准备": 0.037, - "HTTP 基础能力": 0.029, - "领域依赖装配": 0.031, - "数据库引擎预热": 0.026, - "数据库连接预算": 0.025, - "路由": 0.022, - "模块服务": 0.02, - "插件备份恢复": 0.023, - "插件": 0.023, - "定时器": 0.024, - "监控器": 0.025, - "待处理整理回放": 0.02, - "命令服务": 0.022, - "工作流": 0.02, - "插件同步与启动收尾": 0.033 - }, - "threads_before": 2, - "threads_started": 2, - "threads_after": 2, - "tasks_before": 1, - "tasks_started": 2, - "tasks_after": 1, - "database_connections_started": 0 - }, - { - "mode": "normal", - "enabled_component_count": 23, - "startup_ms": 0.629, - "full_lifespan_ms": 0.782, - "stage_ms": { - "后台任务登记器": 0.075, - "数据库准备": 0.039, - "HTTP 基础能力": 0.029, - "领域依赖装配": 0.031, - "数据库引擎预热": 0.024, - "数据库连接预算": 0.022, - "路由": 0.023, - "模块服务": 0.021, - "插件备份恢复": 0.023, - "插件": 0.023, - "定时器": 0.025, - "监控器": 0.02, - "待处理整理回放": 0.024, - "命令服务": 0.021, - "工作流": 0.022, - "插件同步与启动收尾": 0.033 - }, - "threads_before": 2, - "threads_started": 2, - "threads_after": 2, - "tasks_before": 1, - "tasks_started": 2, - "tasks_after": 1, - "database_connections_started": 0 - }, - { - "mode": "normal", - "enabled_component_count": 23, - "startup_ms": 0.631, - "full_lifespan_ms": 0.785, - "stage_ms": { - "后台任务登记器": 0.08, - "数据库准备": 0.04, - "HTTP 基础能力": 0.031, - "领域依赖装配": 0.028, + "HTTP 基础能力": 0.035, + "领域依赖装配": 0.029, "数据库引擎预热": 0.025, - "数据库连接预算": 0.023, + "数据库连接预算": 0.024, "路由": 0.022, - "模块服务": 0.022, + "模块服务": 0.024, + "插件备份恢复": 0.022, + "插件": 0.02, + "定时器": 0.032, + "监控器": 0.022, + "待处理整理回放": 0.019, + "命令服务": 0.023, + "工作流": 0.023, + "插件同步与启动收尾": 0.033 + }, + "threads_before": 2, + "threads_started": 2, + "threads_after": 2, + "tasks_before": 1, + "tasks_started": 2, + "tasks_after": 1, + "database_connections_started": 0 + }, + { + "mode": "normal", + "enabled_component_count": 23, + "startup_ms": 0.598, + "full_lifespan_ms": 0.742, + "stage_ms": { + "后台任务登记器": 0.069, + "数据库准备": 0.042, + "HTTP 基础能力": 0.033, + "领域依赖装配": 0.031, + "数据库引擎预热": 0.025, + "数据库连接预算": 0.022, + "路由": 0.021, + "模块服务": 0.021, "插件备份恢复": 0.021, - "插件": 0.025, - "定时器": 0.022, - "监控器": 0.024, - "待处理整理回放": 0.02, - "命令服务": 0.024, - "工作流": 0.02, - "插件同步与启动收尾": 0.034 + "插件": 0.019, + "定时器": 0.024, + "监控器": 0.023, + "待处理整理回放": 0.019, + "命令服务": 0.021, + "工作流": 0.023, + "插件同步与启动收尾": 0.03 + }, + "threads_before": 2, + "threads_started": 2, + "threads_after": 2, + "tasks_before": 1, + "tasks_started": 2, + "tasks_after": 1, + "database_connections_started": 0 + }, + { + "mode": "normal", + "enabled_component_count": 23, + "startup_ms": 0.765, + "full_lifespan_ms": 0.951, + "stage_ms": { + "后台任务登记器": 0.088, + "数据库准备": 0.067, + "HTTP 基础能力": 0.094, + "领域依赖装配": 0.039, + "数据库引擎预热": 0.031, + "数据库连接预算": 0.022, + "路由": 0.024, + "模块服务": 0.027, + "插件备份恢复": 0.024, + "插件": 0.021, + "定时器": 0.026, + "监控器": 0.023, + "待处理整理回放": 0.023, + "命令服务": 0.023, + "工作流": 0.023, + "插件同步与启动收尾": 0.062 }, "threads_before": 2, "threads_started": 2, @@ -138,8 +138,8 @@ "database_connections_started": 0 } ], - "median_startup_ms": 0.629, - "median_full_lifespan_ms": 0.782, + "median_startup_ms": 0.672, + "median_full_lifespan_ms": 0.831, "enabled_component_count": 23, "enabled_components": [ "后台任务登记器", @@ -172,18 +172,18 @@ { "mode": "safe", "enabled_component_count": 11, - "startup_ms": 0.597, - "full_lifespan_ms": 0.772, + "startup_ms": 0.465, + "full_lifespan_ms": 0.605, "stage_ms": { - "后台任务登记器": 0.075, - "数据库准备": 0.044, - "HTTP 基础能力": 0.033, - "领域依赖装配": 0.059, - "数据库引擎预热": 0.061, - "数据库连接预算": 0.032, - "路由": 0.027, - "模块服务": 0.046, - "插件同步与启动收尾": 0.044 + "后台任务登记器": 0.072, + "数据库准备": 0.047, + "HTTP 基础能力": 0.035, + "领域依赖装配": 0.03, + "数据库引擎预热": 0.024, + "数据库连接预算": 0.022, + "路由": 0.023, + "模块服务": 0.022, + "插件同步与启动收尾": 0.031 }, "threads_before": 2, "threads_started": 2, @@ -196,42 +196,42 @@ { "mode": "safe", "enabled_component_count": 11, - "startup_ms": 0.461, - "full_lifespan_ms": 0.642, + "startup_ms": 0.478, + "full_lifespan_ms": 0.647, "stage_ms": { - "后台任务登记器": 0.076, - "数据库准备": 0.037, + "后台任务登记器": 0.083, + "数据库准备": 0.038, + "HTTP 基础能力": 0.034, + "领域依赖装配": 0.025, + "数据库引擎预热": 0.022, + "数据库连接预算": 0.025, + "路由": 0.025, + "模块服务": 0.025, + "插件同步与启动收尾": 0.052 + }, + "threads_before": 2, + "threads_started": 2, + "threads_after": 2, + "tasks_before": 1, + "tasks_started": 2, + "tasks_after": 1, + "database_connections_started": 0 + }, + { + "mode": "safe", + "enabled_component_count": 11, + "startup_ms": 0.477, + "full_lifespan_ms": 0.651, + "stage_ms": { + "后台任务登记器": 0.074, + "数据库准备": 0.044, "HTTP 基础能力": 0.029, "领域依赖装配": 0.029, - "数据库引擎预热": 0.026, + "数据库引擎预热": 0.027, "数据库连接预算": 0.023, - "路由": 0.024, - "模块服务": 0.023, - "插件同步与启动收尾": 0.06 - }, - "threads_before": 2, - "threads_started": 2, - "threads_after": 2, - "tasks_before": 1, - "tasks_started": 2, - "tasks_after": 1, - "database_connections_started": 0 - }, - { - "mode": "safe", - "enabled_component_count": 11, - "startup_ms": 0.483, - "full_lifespan_ms": 0.659, - "stage_ms": { - "后台任务登记器": 0.077, - "数据库准备": 0.039, - "HTTP 基础能力": 0.034, - "领域依赖装配": 0.035, - "数据库引擎预热": 0.025, - "数据库连接预算": 0.026, - "路由": 0.025, - "模块服务": 0.024, - "插件同步与启动收尾": 0.059 + "路由": 0.022, + "模块服务": 0.021, + "插件同步与启动收尾": 0.057 }, "threads_before": 2, "threads_started": 2, @@ -242,8 +242,8 @@ "database_connections_started": 0 } ], - "median_startup_ms": 0.483, - "median_full_lifespan_ms": 0.659, + "median_startup_ms": 0.477, + "median_full_lifespan_ms": 0.647, "enabled_component_count": 11, "enabled_components": [ "后台任务登记器", diff --git a/tests/test_testing_bootstrap.py b/tests/test_testing_bootstrap.py index 9a5b6e39d..f74b9db90 100644 --- a/tests/test_testing_bootstrap.py +++ b/tests/test_testing_bootstrap.py @@ -2,12 +2,37 @@ from __future__ import annotations import builtins +import sys import types from pathlib import Path from app.testing import bootstrap +def test_install_sites_stub_replaces_loaded_dynamic_resource(monkeypatch): + """隔离探针必须覆盖本机资源模块,保证 source-only CI 与开发机前提一致。""" + real_module = types.ModuleType("app.application.site.sites") + real_module.SitesHelper = object + monkeypatch.setitem(sys.modules, "app.application.site.sites", real_module) + + bootstrap.install_sites_stub() + + installed = sys.modules["app.application.site.sites"] + assert installed is not real_module + assert installed.SitesHelper is bootstrap._SitesHelperStub + + +def test_ensure_sites_stub_preserves_loaded_dynamic_resource(monkeypatch): + """常规测试引导仍应优先复用已经加载的真实站点资源。""" + real_module = types.ModuleType("app.application.site.sites") + real_module.SitesHelper = object + monkeypatch.setitem(sys.modules, "app.application.site.sites", real_module) + + bootstrap.ensure_sites_stub() + + assert sys.modules["app.application.site.sites"] is real_module + + def test_isolate_config_cleanup_uses_loaded_db_module_without_late_import(monkeypatch): """清理回调只读取已加载模块,避免解释器关停期触发二次导入。""" captured = {}