ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

psutil 测试提速实战:将 pytest 启动时间从 0.42s 优化到 0.30s(约 28%)

psutil 测试提速实战:将 pytest 启动时间从 0.42s 优化到 0.30s(约 28%) 可观测性系统编程【免费下载链接】psutilCross-platform lib for process and system monitoring in Python项目地址https://gitcode.com/gh_mirrors/ps/psutil点击查看免费下载导读本文以 psutil 项目在 2025 年的一次真实测试基础设施优化为主线系统讲解如何通过禁用插件自动加载、选择性启用插件、monkey-patch 兼容pytest-xdist标记、以及收紧测试收集范围等手段将单测启动时间从约 0.42 秒压到约 0.30 秒约 28% 提升。读完本文你将掌握一套可复制的 pytest 启动性能排查方法论以及 psutil 仓库中落地这套优化所需的具体命令、配置与源码改动位置。一、背景psutil 从 unittest 迁移到 pytestpsutil 是一个跨平台的 Python 进程与系统监控库其测试体系长期建立在标准库unittest之上。在使用了 17 年之后项目开始逐步迁移到 pytest对应 GitHub issue 2446。迁移的核心动机有两个原生的assert语句测试中可以直接写assert x y失败时 pytest 会自动进行断言重写输出更清晰的差异信息。pytest-xdist 带来的免费并行能力无需改动测试代码即可通过多进程并行跑测试。需要强调的是psutil 的测试类仍然继承自unittest.TestCase并且刻意不使用conftest.py或 fixture相关理由记录在 PR 2456 中。也就是说这次迁移走的是最小侵入路线保留 unittest 的类体系仅借用 pytest 的运行器与断言体验。迁移之后一个绕不开的问题浮现出来pytest 的启动速度太慢。对于开发者在命令行反复执行单个测试的场景这构成了实实在在的开发体验瓶颈。二、问题复现pytest 启动到底有多慢2.1 基准测量为了量化问题作者选取了一个执行时间几乎可以忽略的简单测试作为基准——位于 tests/test_misc.py 的TestMisc::test_version它的全部工作只是校验psutil.__version__与version_info拼接结果一致def test_version(self): assert ( ..join([str(x) for x in psutil.version_info]) psutil.__version__ )分别用 pytest 和 unittest 运行同一个测试对比启动耗时$ time python3 -m pytest psutil/tests/test_misc.py::TestMisc::test_version 1 passed in 0.05s real 0m0,427s $ time python3 -m unittest psutil.tests.test_misc.TestMisc.test_version Ran 1 test in 0.000s real 0m0,204s结论很直观pytest 单测启动约 0.427 秒unittest 约 0.204 秒pytest 几乎是前者的两倍。测试本身只消耗 0.05 秒剩下的大约 0.37 秒全部花在启动与收集阶段——对于一个在开发中会被反复执行的命令来说这个开销相当可观。2.2 时间花在哪导入开销先用简单的导入实验定位瓶颈。分别测量import pytest、import unittest、import psutil三个模块的耗时$ time python3 -c import pytest real 0m0,151s $ time python3 -c import unittest real 0m0,065s $ time python3 -c import psutil real 0m0,056s数据非常清晰仅仅import pytest就要花掉 0.15 秒占整个启动时间的三分之一以上。这部分是 pytest 框架本身的模块体系_pytest包内大量子模块、插件基础设施、断言重写机制等带来的固有成本无法通过配置消除。而import psutil本身只有 0.056 秒说明被测库并不是瓶颈。由此得出第一个方法论结论pytest 的启动开销大头在框架导入本身这部分几乎无解真正可以优化的是插件自动加载带来的额外导入成本。三、优化一禁用插件自动加载PYTEST_DISABLE_PLUGIN_AUTOLOAD3.1 发现隐藏的插件pytest 的一个隐藏行为是它会自动加载当前 Python 环境中安装的所有 pytest 插件即使测试根本用不到它们。通过以下命令可以查看实际加载了哪些插件输出已截断$ pytest --trace-config --collect-only ... active plugins: ... setupplan : ~/.local/lib/python3.12/site-packages/_pytest/setupplan.py stepwise : ~/.local/lib/python3.12/site-packages/_pytest/stepwise.py warnings : ~/.local/lib/python3.12/site-packages/_pytest/warnings.py logging : ~/.local/lib/python3.12/site-packages/_pytest/logging.py reports : ~/.local/lib/python3.12/site-packages/_pytest/reports.py python_path : ~/.local/lib/python3.12/site-packages/_pytest/python_path.py unraisableexception : ~/.local/lib/python3.12/site-packages/_pytest/unraisableexception.py threadexception : ~/.local/lib/python3.12/site-packages/_pytest/threadexception.py faulthandler : ~/.local/lib/python3.12/site-packages/_pytest/faulthandler.py instafail : ~/.local/lib/python3.12/site-packages/pytest_instafail.py anyio : ~/.local/lib/python3.12/site-packages/anyio/pytest_plugin.py pytest_cov : ~/.local/lib/python3.12/site-packages/pytest_cov/plugin.py subtests : ~/.local/lib/python3.12/site-packages/pytest_subtests/plugin.py xdist : ~/.local/lib/python3.12/site-packages/xdist/plugin.py xdist.looponfail : ~/.local/lib/python3.12/site-packages/xdist/looponfail.py ...可以看到除了 pytest 自带的内置插件setupplan、warnings、logging、faulthandler等环境中还自动加载了一堆第三方插件anyio、pytest_cov、instafail、subtests、xdist、xdist.looponfail等。每一个插件都会带来额外的 import 与 hook 注册开销。3.2 用环境变量一键禁用pytest 提供了环境变量PYTEST_DISABLE_PLUGIN_AUTOLOAD设置为任意非空值惯例用1即可跳过所有插件的自动加载。再次运行--trace-config验证效果$ PYTEST_DISABLE_PLUGIN_AUTOLOAD1 pytest --trace-config --collect-only下列第三方插件从加载列表中消失了anyio pytest_cov pytest_instafail pytest_subtests xdist xdist.looponfail3.3 实测收益禁用自动加载后重新跑基准测试$ time PYTEST_DISABLE_PLUGIN_AUTOLOAD1 python3 -m pytest psutil/tests/test_misc.py::TestMisc::test_version 1 passed in 0.05s real 0m0,285s从 0.427 秒降到 0.285 秒约 40% 的提升。仅仅一个环境变量就砍掉了三分之一以上的启动时间性价比极高。四、优化二用-p选择性启用真正需要的插件禁用自动加载之后instafail、subtests等真正需要的插件也不会被加载了。解决方案是显式用-p参数按需启用。psutil 实际依赖pytest-instafail失败即时报出错误和pytest-subtestsunittest 风格子测试支持先只启用这两个$ time PYTEST_DISABLE_PLUGIN_AUTOLOAD1 python3 -m pytest -p instafail -p subtests ... real 0m0,320s时间回升到 0.320 秒——每个-p插件都会带来一定的 import 成本但整体仍明显优于最初的 0.427 秒。接着把pytest-xdist也加上$ time PYTEST_DISABLE_PLUGIN_AUTOLOAD1 python3 -m pytest -p instafail -p subtests -p xdist ... real 0m0,369s0.369 秒。pytest-xdist的导入代价不算大但在不需要并行执行测试时这笔开销依然是冤枉钱。于是自然的想法是仅在真正需要并行时再启用xdist。这一思路也真实体现在 psutil 的工程配置里。在 Makefile 中测试环境变量统一封装为PYTHON_ENV_VARS PYTHONWARNINGSalways PYTHONUNBUFFERED1 PSUTIL_DEBUG1 PSUTIL_TESTING1 PYTEST_DISABLE_PLUGIN_AUTOLOAD1而make test与make test-parallel两个目标则分别对应串行与并行两种形态见 Makefiletest: ## Run all tests (except memleak tests). # To run a specific test do make test ARGStests/test_process.py::TestProcess::test_cmdline $(RUN_TEST) $(ARGS) test-parallel: ## Run all tests (except memleak tests) in parallel. $(RUN_TEST) -n auto --dist loadgroup -m not isolated $(ARGS) $(RUN_TEST) -m isolated $(ARGS)此外pyproject.toml 的[tool.pytest.ini_options]中通过addopts显式声明了插件启用与禁用策略[tool.pytest.ini_options] addopts [ --captureno, --instafail, --no-header, --tbshort, --verbose, -p instafail, -p no:doctest, -p no:junitxml, -p no:nose, -p no:pastebin, -p xdist, ] strict_config true strict_markers true可以看到-p instafail、-p xdist显式开启所需插件同时用-p no:doctest、-p no:junitxml、-p no:nose、-p no:pastebin显式关闭用不到的内置插件与博客中按需启用的思路完全一致。--durations5则会在每次跑测试时汇报最慢的 5 个用例方便持续监控性能。五、优化三处理 pytest-xdist 禁用后的标记警告5.1 问题现象如果直接禁用pytest-xdistpsutil 的测试依然可以运行但会出现警告psutil/tests/test_testutils.py:367 ~/svn/psutil/psutil/tests/test_testutils.py:367: PytestUnknownMarkWarning: Unknown pytest.mark.xdist_group - is this a typo? You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/how-to/mark.html pytest.mark.xdist_group(nameserial)原因是 psutil 中有一批需要串行执行的测试它们被pytest.mark.xdist_group(nameserial)装饰确保在并行模式下被分配到同一个 worker避免并发争抢系统级资源。当pytest-xdist被禁用后xdist_group这个标记随之消失pytest 就会把装饰器当成未知标记报警告。5.2 解决方案monkey-patch 一个假的xdist_grouppsutil 的解法非常巧妙——在 tests/init.py 中用PYTEST_PARALLEL常量判断是否处于并行模式PYTEST_PARALLEL PYTEST_XDIST_WORKER in os.environ # make test-parallelpytest-xdist会在每个 worker 进程中设置PYTEST_XDIST_WORKER环境变量因此该常量在并行模式下为True。接着在串行模式下注入一个 no-op 版本的xdist_groupimport pytest, functools PYTEST_PARALLEL PYTEST_XDIST_WORKER in os.environ # True if running parallel tests if not PYTEST_PARALLEL: def fake_xdist_group(*_args, **_kwargs): Mimics pytest.mark.xdist_group decorator. No-op: it just calls the test method or return the decorated class. def wrapper(obj): functools.wraps(obj) def inner(*args, **kwargs): return obj(*args, **kwargs) return obj if isinstance(obj, type) else inner return wrapper pytest.mark.xdist_group fake_xdist_group # monkey patch这段代码的要点用functools.wraps保留原函数元信息名字、文档字符串、签名等避免影响 pytest 的收集与断言定位对类对象isinstance(obj, type)直接原样返回对函数则返回一个透明包装通过 monkey-patchpytest.mark.xdist_group让所有pytest.mark.xdist_group(nameserial)装饰的测试在串行模式下哑化为普通测试警告随之消失。在并行模式下PYTEST_PARALLEL为真这段代码不生效xdist_group保持真实行为配合如下命令手动启用 xdist 并行$ python3 -m pytest -p xdist -n auto --dist loadgroup-n auto让 xdist 根据 CPU 核数自动决定 worker 数量--dist loadgroup则告诉 xdist 把属于同一xdist_group的测试分配到同一 worker——这正是serial组测试所需的调度语义。5.3 仓库中的实际使用在 psutil 测试代码中serial与isolated两个标记定义在 tests/init.py# serial decorator: put all marked tests on the same xdist worker, # so they dont run concurrently in the same process. Needed by tests # that share the same system-wide resource (e.g. a socket) and must not # overlap. serial pytest.mark.xdist_group(nameserial) # isolated decorator: these tests are skipped under xdist and run in # a second, separate pytest run (-m isolated) that uses a single # process. They need a quiet, non-xdist environment, because # they measure noisy per-process or system counters. isolated pytest.mark.isolatedserial将标记的测试固定在同一个 xdist worker 上避免共享系统资源如端口、ss/netstat对比等的测试并发冲突典型用在 tests/test_connections.py、tests/test_linux.py 等文件中isolated标记的测试在并行模式下被跳过转而在第二阶段的单进程 pytest 运行中执行即make test-parallel的第二条命令-m isolated因为它们测量的是对噪音敏感的进程/系统计数器。PYTEST_PARALLEL还被用于内存泄漏测试的细节控制例如 tests/test_memleaks.pyMemoryLeakTestCase.verbosity 0 if PYTEST_PARALLEL else 1可见这个环境变量判断已经成为 psutil 测试套件中串行/并行模式分叉的标准开关。六、优化四收紧测试收集范围testpaths python_files6.1 问题与配置pytest 默认会递归扫描整个目录树来寻找测试文件扫描范围越大收集阶段耗时越长。psutil 通过 pyproject.toml 显式限定[tool.pytest.ini_options] testpaths [tests/] python_files [test_*.py]testpaths告诉 pytest 只在这一个目录下找测试不再全仓库扫描python_files只把匹配test_*.py的文件当作测试文件跳过其他无关.py文件。从源码可以看到psutil 的测试文件确实全部符合test_*.py命名约定如tests/test_process.py、tests/test_system.py、tests/test_linux.py等见 tests 目录。需要留意的是博客成文时测试目录位于psutil/tests/而当前仓库布局中测试已统一迁移到根目录的tests/因此testpaths对应写作[tests/]——原理完全一致。6.2 收益仅仅这两行配置就让测试收集时间从 0.20 秒降到 0.17 秒又省下了约 0.03 秒。虽然数值不大但这是一次性、零维护成本的常驻收益。另外Makefile 中还有两个特殊的测试目标体现了同样的限定范围思想test-docs: ## Run doc sanity tests (outside testpaths, run on demand). $(MAKE) -C docs test ARGS$(ARGS) test-bots: ## Run GitHub bot tests (outside testpaths, run on demand). $(PYTHON) -m pytest -o addopts .github/workflows/tests/ $(ARGS)文档测试与 CI 机器人测试位于testpaths之外需要按需单独运行——这也解释了为何它们被排除在常规测试路径之外。七、效果汇总与结论把所有优化叠加最终成果如下优化项手段启动耗时基线无0.427s禁用插件自动加载PYTEST_DISABLE_PLUGIN_AUTOLOAD10.285s按需启用插件-p instafail -p subtests0.320s再加 xdist-p xdist0.369s收紧收集范围testpathspython_files再省 ~0.03s总计约 0.12 秒的优化使 pytest 启动时间从 0.42 秒降至约 0.30 秒约 28%。作者特别指出对于完整测试套件成千上万个用例而言这 0.12 秒微不足道但对于开发中高频反复执行的单测命令28% 的提升感受非常明显。最终成果合入 PR 2538。值得一提的是psutil 后续还引入了两阶段并行策略见上文make test-parallel第一阶段用 xdist 并行跑非isolated测试第二阶段用单进程串行跑isolated测试。这是对博客所述 xdist 处理方案的进一步演进也说明串行/并行分治是大型测试套件兼顾速度与正确性的通用手法。八、可复用的优化清单把本文的方法提炼为一份可直接套用的 checklist量化现状用time python3 -m pytest 单个用例与time python3 -m unittest 同一用例对比确认问题确实出在 pytest 启动而非测试本身。定位导入成本分别time python3 -c import pytest、time python3 -c import 被测库判断开销大头在框架还是被测代码。审查插件加载运行pytest --trace-config --collect-only查看自动加载的插件清单。禁用自动加载设置PYTEST_DISABLE_PLUGIN_AUTOLOAD1再通过-p plugin只启用真正用到的插件用-p no:plugin关闭不需要的内置插件。处理缺失标记若禁用了提供自定义标记的插件如xdist_group用环境变量如PYTEST_XDIST_WORKER判断运行模式并对缺失的标记做 no-op monkey-patch。收紧收集范围在pyproject.toml或pytest.ini中配置testpaths与python_files让 pytest 只扫描测试目录与测试文件。固化配置将环境变量、插件参数统一封装进 Makefile 或 CI 配置确保团队成员与 CI 环境获得一致的加速效果。对 psutil 仓库而言以上每一项优化都能在 Makefile、pyproject.toml、tests/init.py 与 tests/conftest.py 中找到对应的工程化落地感兴趣可以对照源码继续深入。附本文数据与结论均来自 psutil 官方博客 docs/blog/2025/speedup-pytest-startup.rst并结合当前仓库源码Makefile、pyproject.toml、tests/init.py核实。文中实测耗时基于作者的 Python 3.12 开发环境具体数值会随机器与插件环境变化。赞分享可观测性系统编程【免费下载链接】psutilCross-platform lib for process and system monitoring in Python项目地址https://gitcode.com/gh_mirrors/ps/psutil点击查看免费下载相关推荐vim-nerdtree-syntax-highlight与vim-devicons完美搭配指南vim nerdtree syntax highlight与vim devicons完美搭配指南 vim nerdtree syntax highlight是一10 分钟把智能门锁接入 Home Assistant远程控制与访客密码完整实操10 分钟把智能门锁接入 Home Assistant远程控制与访客密码完整实操 出差第二天早上你掏出手机想确认前门是否反锁保洁阿姨周六上午来你要在她出文档教程智能家居物联网Notepad-- 完整指南3 分钟快速装好免费跨平台文本编辑器Notepad 完整指南3 分钟快速装好免费跨平台文本编辑器 从老系统导出的文件拿到新电脑上打开常常是满屏乱码同一份代码仓库在 Windows 上没问题桌面应用上一篇Frappe 文档分享机制深度剖析v5.1.1 新增的 Share with Everyone 功能与 DocShare 实现全解下一篇Meteor static-html 包使用指南为 React、Vue、Solid 等框架搭建静态 HTML 挂载点创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表