Memory Leak Audit

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

by microsoft001f205a1f401a09dd530b1ac1a54424a2225d0dNo license193K starsListed Oct 9, 2026Updated Oct 9, 2026Repository updated today

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.

FeaturedInstructions onlySoftware DevelopmentSecurity
AI-generated overview

Audits code for memory leaks and disposable-object issues, covering event listeners, lifecycle callbacks and disposal patterns.

What it does
Provides a step-by-step audit checklist for finding and fixing memory leaks in code, focused on disposable-object patterns such as addDisposableListener, Event.once, MutableDisposable, DisposableStore and onWillDispose. Each step gives a rule, bad and good code examples, and references to validated pull requests. It also includes a quick-reference table of scenarios, patterns and anti-patterns, plus verification steps such as checking listener counts and running leak-checking test utilities.
When to use it
Use when reviewing code that registers event listeners or DOM handlers, fixing reported memory leaks where listener counts grow over time, creating objects inside repeatedly called methods, or working with model lifecycle events and event subscriptions added in constructors or setup methods.
Requirements
No scripts or tools are required; it is an instruction-only skill. The verification guidance references a test utility and an npm performance command from the project being audited.

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.

Source and attribution

Source:microsoft/vscodein.github/skills/memory-leak-auditat commit001f205

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal