OpenClaw测试堆内存泄漏分析 是做什么的?
使用本技能进行测试内存相关问题调查。当有堆快照可用时,不要仅凭RSS做出判断。将快照名称差异视为分类证据,而非最终证明,直到通过retainers或dominators确认调用关系。
首先阅读 ../openclaw-test-performance/SKILL.md 以获取最新的测试性能命令和证据路由。
针对运行时修复(例如网关等长期运行服务中的闭包泄漏),请参见下面的验证运行时修复部分——该部分使用专用的测试框架,而非单元测试性能分析工作流。
工作流
-
首先复现失败的形态。
-
匹配真实的入口点和工作进程预算。要获得基础粒度的单元快速基线(包含每个配置的最大RSS和文件级计时),请使用:
pnpm test:perf:groups \ --config test/vitest/vitest.unit-fast.config.ts \ --allow-failures \ --output .artifacts/test-perf/unit-fast-memory.json -
对于可疑文件,使用一个工作进程重新运行该文件,并收集wall/RSS证据:
/usr/bin/time -l pnpm test <file> --maxWorkers=1 --reporter=verbose。 -
当前
pnpm test的执行由scripts/test-projects.mts规划。记录打印出的Vitest配置或分片,并在报告与配置或工作进程预算相关时保留该形态。
-
-
收集最强的可用堆证据。
- 运行
pnpm test:perf:profile:runner -- --output-dir .artifacts/test-perf/vitest-runner-profile -- <file>获取CPU性能分析以及单元运行器的采样堆分析。在DevTools中打开堆分析并检查最大的分配家族。 - 采样的
.heapprofile输出不是.heapsnapshot;不要将其传递给快照差异工具。 - 当专用测试框架或其他已知生产者生成重复的
.heapsnapshot文件时,使用.agents/skills/openclaw-test-heap-leaks/scripts/heapsnapshot-delta.mjs比较来自同一PID的快照。 - 如果没有快照或堆分析可用,RSS增长是有用的初步分类证据,但泄漏分类应保持不确定。
- 运行
-
分类增长类型后再选择修复方案。
- 如果增长主要由Vite/Vitest转换后的源码字符串、
Module、system / Context、字节码、描述符数组或属性映射构成,则视为长期运行工作进程中可能保留的模块图增长。 - 如果增长主要由应用对象、缓存、缓冲区、服务器句柄、定时器、模拟状态、sqlite状态或类似的运行时对象构成,则视为可能的清理或生命周期泄漏。
- 如果名称不明确,请暂停于明确标签之前,并在DevTools中检查主要差异的retainers/dominators。
- 如果增长主要由Vite/Vitest转换后的源码字符串、
-
修复正确的层面。
- 对于共享工作进程中可能的保留转换模块增长:
- 检查所属的Vitest配置以及
scripts/test-projects.test-support.mjs中的进程分片。仅当相同形态的证据表明共享工作进程保留是原因时,才在那里修复进程生命周期或项目所有权。 test/vitest/vitest.unit-fast-isolated.config.ts用于需要全新模块图的有状态测试审计。不要将其用作通用的内存热点列表。
- 检查所属的Vitest配置以及
- 对于真正的泄漏:
- 修补受影响的测试或运行时清理路径。
- 查找缺失的
afterEach/afterAll、模块重置间隙、保留的全局状态、未释放的数据库句柄,或在文件完成后仍然存活的监听器/定时器。
- 对于共享工作进程中可能的保留转换模块增长:
-
使用最直接的证据进行验证。
- 重新运行相同的分组或限定命令,确认最大RSS趋势或OOM已降低。
- 当专用生产者生成堆分析或快照时,重复相同的生产者并比较等效的产物。
- 对于路由或配置更改,验证预期的Vitest配置或分片启动且受影响的测试完成。
启发式规则
- 不要将所有增长都称为泄漏。在非隔离的共享Vitest项目中的增长可能是工作进程生命周期问题,而非应用对象泄漏。
scripts/test-projects.mts、scripts/test-group-report.mts和scripts/run-vitest-profile.mts分别是当前执行、分组RSS和性能分析的入口点。[test] starting ...行标识了要复现的Vitest配置或分片。.artifacts/vitest-shard-timings.json存储了配置/分片的执行时长用于调度。它不是文件级内存热点或行为清单。- 当同一保留对象族在同一个工作进程PID的多个时间间隔内增长时,相信快照而非直觉,然后通过retainer证据确认可疑的调用。
快照比较
- 直接比较:
node .agents/skills/openclaw-test-heap-leaks/scripts/heapsnapshot-delta.mjs before.heapsnapshot after.heapsnapshot
- 自动选择同一通道内每个PID的最早/最晚快照:
node .agents/skills/openclaw-test-heap-leaks/scripts/heapsnapshot-delta.mjs --lane-dir <snapshot-directory>
- 有用标志:
--top 40--min-kb 32--pid 16133
首先读取正向差异最大的条目。模块转换产物的大量正向增长指向共享进程生命周期或项目所有权问题;运行时对象的大量正向增长则表明真正的泄漏。如果仅凭名称无法确定,请在DevTools中打开同一对快照,检查前几行的retainers/dominators,然后再声明根因。
验证运行时修复(非测试内存)
上述工作流用于诊断Vitest工作进程内存增长。对于验证运行时/闭包修复是否确实释放了捕获的状态,请使用专用测试框架:
pnpm leak:embedded-run— 运行scripts/embedded-run-abort-leak.ts。在一个模拟runEmbeddedAttempt的函数作用域内循环N次中止运行,写入堆快照,并通过基于FinalizationRegistry的跟踪实例计数和RSS差异,给出关于保留增长通过/未通过的结论。
模式:
closure-extracted(默认)— 生产修复形态(模块作用域的辅助函数)。closure-inline— 修复前形态(运行器作用域内的闭包)。用作敏感性检查:如果它通过了,说明你破坏了测试框架而非修复了Bug。synthetic-leak— 通过模块级别的桶故意保留对象。用于确认测试框架能够检测泄漏,然后再信任真实修复的通过结果。
快照存储在 .tmp/embedded-run-abort-leak/ 中。使用与上述相同的脚本进行差异分析:
node .agents/skills/openclaw-test-heap-leaks/scripts/heapsnapshot-delta.mjs \
.tmp/embedded-run-abort-leak/baseline-*.heapsnapshot \
.tmp/embedded-run-abort-leak/batch-N-*.heapsnapshot --top 30
当修复其他运行时泄漏时,请在此旁边添加新的测试框架,而不是改造此框架。夹具函数应模拟泄漏所在函数的词法作用域,而不是一个通用的中止循环。
输出期望
使用本技能时,请报告:
- 确切的复现命令。
- 复现了哪个Vitest配置或目标,以及当快照可用时比较了哪个PID。
- 堆分析或快照差异中的主要保留对象族(如果可用)。
- 问题是真正的泄漏还是共享工作进程保留模块增长,以及是否有retainers/dominators确认。
- 具体的修复或影响降低补丁。
- 你验证了哪些内容,以及由于快照开销无法验证的内容。