memory-leak-audit by microsoft/vscode
npx skills add https://github.com/microsoft/vscode --skill memory-leak-auditVS Code 中的头号错误类别。此技能编码了预防和修复泄漏的模式。
按顺序完成每个检查。一个遗漏的模式就可能导致数千个对象泄漏。
规则:切勿直接使用原始的 .onload、.onclick 或 addEventListener()。始终使用 addDisposableListener()。
// 错误 — 每次调用都会泄漏一个监听器
this.iconElement.onload = () => { ... };
// 正确 — 可追踪且可释放
this._register(addDisposableListener(this.iconElement, 'load', () => { ... }));
已验证案例:PR #280566 — 扩展图标小部件在 37 次切换后泄漏了 185 个监听器。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
规则:对于应仅触发一次的事件(生命周期事件、关闭事件、首次变更事件),使用 Event.once()。
// 错误 — 监听器在首次触发后永久保持注册状态
model.onDidDispose(() => store.dispose());
// 正确 — 首次调用后自动移除
Event.once(model.onDidDispose)(() => store.dispose());
已验证案例:PRs #285657, #285661 — 终端生命周期 hack 被替换为 Event.once()。
规则:在多次调用的方法中创建的对象不得注册到类的 this._register()。使用 MutableDisposable 或向调用者返回 IDisposable。
// 错误 — 每次调用都会向类存储添加另一个监听器
startSearch() {
this._register(this.model.onResults(() => { ... }));
}
// 正确 — MutableDisposable 确保最多只有一个监听器
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 个监听器。
规则:当创建与模型生命周期绑定的 DisposableStore 时,将 model.onWillDispose(() => store.dispose()) 注册到存储本身。
const store = new DisposableStore();
store.add(model.onWillDispose(() => store.dispose()));
store.add(model.onDidChange(() => { ... }));
已验证案例:模式用于 chatEditingSession.ts、fileBasedRecommendations.ts、testingContentProvider.ts。
规则:当使用创建池化对象(列表、树)的工厂方法时,可释放对象必须注册到单个项目,而不是池类。
// 错误 — 注册到池,每个项目从未清理
createItem() {
const item = new Item();
this._register(item.onEvent(() => { ... }));
return item;
}
// 正确 — 使用项目作用域的释放进行包装
createItem(): IDisposable & Item {
const store = new DisposableStore();
const item = new Item();
store.add(item.onEvent(() => { ... }));
return { ...item, dispose: () => store.dispose() };
}
已验证案例:PR #290505 — 聊天内容部分 CollapsibleListPool 和 TreePool 泄漏了可释放对象。
规则:每个创建可释放对象的测试套件都必须调用 ensureNoDisposablesAreLeakedInTestSuite()。
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
suite('MyFeature', () => {
ensureNoDisposablesAreLeakedInTestSuite();
test('does something', () => {
// 测试中的可释放对象会被自动追踪
});
});
| 场景 | 模式 | 反模式 |
|---|---|---|
| DOM 事件 | addDisposableListener() | .onclick =, addEventListener() |
| 一次性事件 | Event.once(event)(handler) | 对生命周期事件使用 event(handler) |
| 重复方法 | MutableDisposable 或返回 IDisposable | 在非构造函数中使用 this._register() |
| 模型生命周期 | store.add(model.onWillDispose(...)) | 忘记清理 |
| 池化对象 | 项目作用域的 DisposableStore | 池作用域的 this._register() |
| 测试 | ensureNoDisposablesAreLeakedInTestSuite() | 无泄漏检查 |
修复泄漏后,通过以下方式验证:
ensureNoDisposablesAreLeakedInTestSuite()每周安装量
181
代码仓库
GitHub 星标数
183.0K
首次出现
2026年2月24日
安全审计
安装于
amp181
gemini-cli181
codex181
kimi-cli181
cursor181
opencode181
The #1 bug category in VS Code. This skill encodes the patterns that prevent and fix leaks.
Work through each check in order. A single missed pattern can cause thousands of leaked objects.
Rule : Never use raw .onload, .onclick, or addEventListener() directly. Always use addDisposableListener().
// BAD — leaks a listener every call
this.iconElement.onload = () => { ... };
// GOOD — tracked and disposable
this._register(addDisposableListener(this.iconElement, 'load', () => { ... }));
Validated by : PR #280566 — Extension icon widget leaked 185 listeners after 37 toggles.
Rule : Use Event.once() for events that should only fire once (lifecycle events, close events, first-change events).
// BAD — listener stays registered forever after first fire
model.onDidDispose(() => store.dispose());
// GOOD — auto-removes after first invocation
Event.once(model.onDidDispose)(() => store.dispose());
Validated by : PRs #285657, #285661 — Terminal lifecycle hacks replaced with Event.once().
Rule : Objects created in methods called multiple times must NOT be registered to the class this._register(). Use MutableDisposable or return IDisposable to the caller.
// 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(() => { ... });
}
When the event should only fire once per method call, combine Event.once() with MutableDisposable — this auto-removes the listener after the first invocation while still guarding against repeated calls:
private readonly _searchListener = this._register(new MutableDisposable());
startSearch() {
this._searchListener.value = Event.once(this.model.onResults)(() => { ... });
}
Validated by : PR #283466 — Terminal find widget leaked 1 listener per search.
Rule : When creating a DisposableStore tied to a model's lifetime, register model.onWillDispose(() => store.dispose()) to the store itself.
const store = new DisposableStore();
store.add(model.onWillDispose(() => store.dispose()));
store.add(model.onDidChange(() => { ... }));
Validated by : Pattern used in chatEditingSession.ts, fileBasedRecommendations.ts, testingContentProvider.ts.
Rule : When using factory methods that create pooled objects (lists, trees), disposables must be registered to the individual item, not the pool class.
// 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() };
}
Validated by : PR #290505 — Chat content parts CollapsibleListPool and TreePool leaked disposables.
Rule : Every test suite that creates disposable objects must call ensureNoDisposablesAreLeakedInTestSuite().
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
suite('MyFeature', () => {
ensureNoDisposablesAreLeakedInTestSuite();
test('does something', () => {
// test disposables are tracked automatically
});
});
| Scenario | Pattern | Anti-Pattern |
|---|---|---|
| DOM events | addDisposableListener() | .onclick =, addEventListener() |
| One-time events | Event.once(event)(handler) | event(handler) for lifecycle |
| Repeated methods | MutableDisposable or return IDisposable |
After fixing leaks, verify by:
ensureNoDisposablesAreLeakedInTestSuite() in testsWeekly Installs
181
Repository
GitHub Stars
183.0K
First Seen
Feb 24, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
amp181
gemini-cli181
codex181
kimi-cli181
cursor181
opencode181
GSAP 框架集成指南:Vue、Svelte 等框架中 GSAP 动画最佳实践
2,300 周安装
Docker安全指南:全面容器安全最佳实践、漏洞扫描与合规性要求
177 周安装
iOS开发专家技能:精通Swift 6、SwiftUI与原生应用开发,涵盖架构、性能与App Store合规
177 周安装
describe技能:AI驱动结构化测试用例生成,提升代码质量与评审效率
2 周安装
专业 README 生成器 | 支持 Rust/TypeScript/Python 项目,自动应用最佳实践
2 周安装
Django 6 升级指南:从 Django 5 迁移的完整步骤与重大变更解析
1 周安装
GitLab DAG与并行处理指南:needs与parallel优化CI/CD流水线速度
2 周安装
this._register() in non-constructor |
| Model lifecycle | store.add(model.onWillDispose(...)) | Forgetting cleanup |
| Pooled objects | Item-scoped DisposableStore | Pool-scoped this._register() |
| Tests | ensureNoDisposablesAreLeakedInTestSuite() | No leak checking |