关于 memory-leak-audit
用于审查和预防 VS Code 代码内存泄漏的技能,通过生命周期、事件监听器、可释放对象、对象池和测试检查,排查重复操作与对象清理问题。
主要功能
- 01审查 DOM 事件监听器注册
- 02为生命周期事件使用一次性处理器
- 03用 MutableDisposable 管理重复订阅
- 04将可释放存储绑定到模型生命周期
- 05为对象池中的单项管理清理
- 06检查测试中的可释放对象泄漏
适用场景
- 01审查注册事件或 DOM 处理器的代码
- 02调查监听器数量持续增长的问题
- 03检查重复调用方法中的清理逻辑
- 04验证测试套件中的可释放对象清理
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ microsoft/ vscode --skill memory-leak-audit安装后在 Claude Code 中输入/memory-leak-audit确认已加载。
详细介绍
内存泄漏审计
这是 VS Code 中排名第一的 Bug 类别。本技能总结了预防和修复泄漏的模式。
适用场景
- 审查注册事件监听器或 DOM 处理器的代码
- 修复已报告的内存泄漏(监听器数量随时间增长)
- 在会被重复调用的方法中创建对象
- 处理模型生命周期事件(onWillDispose、onDidClose)
- 在构造函数或设置方法中添加事件订阅
审计清单
按顺序逐项检查。只要漏掉一种模式,就可能导致数千个对象泄漏。
步骤 1:DOM 事件监听器
规则:绝不要直接使用原始的 .onload、.onclick 或 addEventListener()。始终使用 addDisposableListener()。
// BAD — leaks a listener every call
this.iconElement.onload = () => { ... };
// GOOD — tracked and disposable
this._register(addDisposableListener(this.iconElement, 'load', () => { ... }));
验证依据:PR #280566 —— 扩展图标组件在切换 37 次后泄漏了 185 个监听器。
步骤 2:一次性事件
规则:对于只应触发一次的事件(生命周期事件、关闭事件、首次更改事件),请使用 Event.once()。
// BAD — listener stays registered forever after first fire
model.onDidDispose(() => store.dispose());
// GOOD — auto-removes after first invocation
Event.once(model.onDidDispose)(() => store.dispose());
验证依据:PR #285657、#285661 —— 终端生命周期相关的变通方案已替换为 Event.once()。
步骤 3:重复调用的方法
规则:在会被多次调用的方法中创建的对象,绝不能注册到类的 this._register()。请使用 MutableDisposable,或将 IDisposable 返回给调用方。
// BAD — every call adds another listener to the class store
startSearch() {
this._register(this.model.onResults(() => { ... }));
}
// GOOD — MutableDisposable ensures max 1 listener
private readonly _searchListener = this._register(new MutableDisposable());
startSearch() {
this._searchListener.value = this.model.onResults(() => { ... });
}
如果事件在每次方法调用中只应触发一次,请将 Event.once() 与 MutableDisposable 结合使用——这样会在首次调用后自动移除监听器,同时防止重复调用造成问题:
private readonly _searchListener = this._register(new MutableDisposable());
startSearch() {
this._searchListener.value = Event.once(this.model.onResults)(() => { ... });
}
验证依据:PR #283466 —— 终端查找组件每次搜索都会泄漏 1 个监听器。
步骤 4:与模型绑定的 DisposableStore
规则:创建与模型生命周期绑定的 DisposableStore 时,请将 model.onWillDispose(() => store.dispose()) 注册到该 store 自身。
const store = new DisposableStore();
store.add(model.onWillDispose(() => store.dispose()));
store.add(model.onDidChange(() => { ... }));
验证依据:此模式用于 chatEditingSession.ts、fileBasedRecommendations.ts、testingContentProvider.ts。
步骤 5:资源池模式
规则:使用创建池化对象(列表、树)的工厂方法时,必须将可释放对象注册到单个项,而不是池类。
// BAD — registers to pool, never cleaned per item
createItem() {
const item = new Item();
this._register(item.onEvent(() => { ... }));
return item;
}
// GOOD — wrap with item-scoped disposal
createItem(): IDisposable & Item {
const store = new DisposableStore();
const item = new Item();
store.add(item.onEvent(() => { ... }));
return { ...item, dispose: () => store.dispose() };
}
验证依据:PR #290505 —— Chat 内容部件中的 CollapsibleListPool 和 TreePool 泄漏了可释放对象。
步骤 6:测试验证
规则:每个创建可释放对象的测试套件都必须调用 ensureNoDisposablesAreLeakedInTestSuite()。
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
suite('MyFeature', () => {
ensureNoDisposablesAreLeakedInTestSuite();
test('does something', () => {
// test disposables are tracked automatically
});
});
快速参考
| 场景 | 模式 | 反模式 |
|---|---|---|
| DOM 事件 | addDisposableListener() | .onclick =、addEventListener() |
| 一次性事件 | Event.once(event)(handler) | 生命周期事件使用 event(handler) |
| 重复调用的方法 | MutableDisposable 或返回 IDisposable | 在非构造函数中使用 this._register() |
| 模型生命周期 | store.add(model.onWillDispose(...)) | 忘记清理 |
| 池化对象 | 作用域限定于单个项的 DisposableStore | 作用域限定于池的 this._register() |
| 测试 | ensureNoDisposablesAreLeakedInTestSuite() | 不检查泄漏 |
验证
修复泄漏后,请通过以下方式验证:
- 检查重复操作前后的监听器数量
- 在测试中运行
ensureNoDisposablesAreLeakedInTestSuite() - 确认对象数量趋于稳定(不会随使用量线性增长)
- 对于 Chat 特有的泄漏:通过
npm run perf:chat-leak运行 Chat 内存泄漏检查器(参见chat-perf技能)。它会在单个会话中发送 N 条消息,在每条消息之间强制执行 GC,并对堆和 DOM 采样数据进行线性回归,以检测每条消息导致的增长。斜率超过 2 MB/msg 表示存在泄漏。使用--messages 20 --verbose可获得更准确的结果。