Memory Leak Audit

microsoft/vscode/.github/skills/memory-leak-audit

作者 microsoft001f205a1f401a09dd530b1ac1a54424a2225d0d无许可证193K 个星标收录于 2026年10月9日更新于 2026年10月9日仓库今天更新

Audit code for memory leaks and disposable issues. Use when reviewing event listeners, DOM handlers, lifecycle callbacks, or fixing leak reports. Covers addDisposableListener, Event.once, MutableDisposable, DisposableStore, and onWillDispose patterns.

AI 生成的概览

审查代码中的内存泄漏与可释放对象问题,涵盖事件监听器、生命周期回调与释放模式。

功能
提供逐步审查清单,用于发现并修复代码中的内存泄漏,重点是可释放对象模式,如 addDisposableListener、Event.once、MutableDisposable、DisposableStore 和 onWillDispose。每个步骤给出规则、错误与正确代码示例,并引用已验证的拉取请求。还包含场景、模式与反模式的速查表,以及验证步骤,例如检查监听器数量和运行泄漏检查测试工具。
适用场景
适用于审查注册事件监听器或 DOM 处理器的代码、修复监听器数量随时间增长的已报告内存泄漏、在会被反复调用的方法中创建对象,以及处理模型生命周期事件和在构造函数或初始化方法中添加事件订阅的场景。
运行要求
无需脚本或工具,仅为指令型技能。验证指引中提及被审查项目中的测试工具和 npm 性能命令。

Memory Leak Audit

The #1 bug category in VS Code. This skill encodes the patterns that prevent and fix leaks.

When to Use

  • Reviewing code that registers event listeners or DOM handlers
  • Fixing reported memory leaks (listener counts growing over time)
  • Creating objects in methods that are called repeatedly
  • Working with model lifecycle events (onWillDispose, onDidClose)
  • Adding event subscriptions in constructors or setup methods

Audit Checklist

Work through each check in order. A single missed pattern can cause thousands of leaked objects.

Step 1: DOM Event Listeners

Rule: Never use raw .onload, .onclick, or addEventListener() directly. Always use addDisposableListener().

typescript
// BAD — leaks a listener every callthis.iconElement.onload = () => { ... };
// GOOD — tracked and disposablethis._register(addDisposableListener(this.iconElement, 'load', () => { ... }));

Validated by: PR #280566 — Extension icon widget leaked 185 listeners after 37 toggles.

Step 2: One-Time Events

Rule: Use Event.once() for events that should only fire once (lifecycle events, close events, first-change events).

typescript
// BAD — listener stays registered forever after first firemodel.onDidDispose(() => store.dispose());
// GOOD — auto-removes after first invocationEvent.once(model.onDidDispose)(() => store.dispose());

Validated by: PRs #285657, #285661 — Terminal lifecycle hacks replaced with Event.once().

Step 3: Repeated Method Calls

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.

typescript
// BAD — every call adds another listener to the class storestartSearch() {    this._register(this.model.onResults(() => { ... }));}
// GOOD — MutableDisposable ensures max 1 listenerprivate 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:

typescript
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.

Step 4: Model-Tied DisposableStores

Rule: When creating a DisposableStore tied to a model's lifetime, register model.onWillDispose(() => store.dispose()) to the store itself.

typescript
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.

Step 5: Resource Pool Patterns

Rule: When using factory methods that create pooled objects (lists, trees), disposables must be registered to the individual item, not the pool class.

typescript
// BAD — registers to pool, never cleaned per itemcreateItem() {    const item = new Item();    this._register(item.onEvent(() => { ... }));    return item;}
// GOOD — wrap with item-scoped disposalcreateItem(): 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.

Step 6: Test Validation

Rule: Every test suite that creates disposable objects must call ensureNoDisposablesAreLeakedInTestSuite().

typescript
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
suite('MyFeature', () => {    ensureNoDisposablesAreLeakedInTestSuite();
    test('does something', () => {        // test disposables are tracked automatically    });});

Quick Reference

ScenarioPatternAnti-Pattern
DOM eventsaddDisposableListener().onclick =, addEventListener()
One-time eventsEvent.once(event)(handler)event(handler) for lifecycle
Repeated methodsMutableDisposable or return IDisposablethis._register() in non-constructor
Model lifecyclestore.add(model.onWillDispose(...))Forgetting cleanup
Pooled objectsItem-scoped DisposableStorePool-scoped this._register()
TestsensureNoDisposablesAreLeakedInTestSuite()No leak checking

Verification

After fixing leaks, verify by:

  1. Checking listener counts before/after repeated operations
  2. Running ensureNoDisposablesAreLeakedInTestSuite() in tests
  3. Confirming object counts stabilize (don't grow linearly with usage)
  4. For chat-specific leaks: Run the chat memory leak checker via npm run perf:chat-leak (see the chat-perf skill). It sends N messages in a single session, forces GC between each, and uses linear regression on heap/DOM samples to detect per-message growth. A slope above 2 MB/msg indicates a leak. Use --messages 20 --verbose for more accurate results.

来源与署名

来源:microsoft/vscode位于.github/skills/memory-leak-audit提交001f205

许可证: 无许可证

内容归原作者所有。SourceWeft 从公开仓库中收录这些内容。

举报或申请下架