memory-leak-audit
microsoft/vscode
Audit and fix memory leaks in event listeners, DOM handlers, and disposable patterns.
What is memory-leak-audit?
Encodes VS Code's patterns for preventing the #1 bug category: memory leaks from untracked event listeners and disposables. Use when reviewing event subscriptions, DOM handlers, lifecycle callbacks, or fixing reported leak growth.
- Identify DOM event listeners that should use addDisposableListener() instead of raw addEventListener()
- Detect one-time events that should use Event.once() to auto-remove after first invocation
- Spot repeated method calls that leak listeners via improper this._register() usage
- Validate model lifecycle cleanup with DisposableStore and onWillDispose patterns
- Check pooled object factories for item-scoped vs pool-scoped disposal mistakes
- Verify test suites with ensureNoDisposablesAreLeakedInTestSuite()
How to install memory-leak-audit
npx skills add https://github.com/microsoft/vscode --skill memory-leak-auditHow to use memory-leak-audit
- 1.Review DOM event listeners and replace raw addEventListener() or .onclick with addDisposableListener()
- 2.Check one-time events (lifecycle, close, first-change) and wrap with Event.once()
- 3.Audit methods called repeatedly and use MutableDisposable or return IDisposable instead of this._register()
- 4.Verify DisposableStore instances tied to models register model.onWillDispose() cleanup
- 5.Check pooled object factories wrap items with item-scoped DisposableStore, not pool-scoped
- 6.Add ensureNoDisposablesAreLeakedInTestSuite() to test suites and verify listener counts stabilize
Use cases
- Reviewing code that registers event listeners or DOM handlers for leaks
- Fixing reported memory leaks where listener counts grow over time
- Creating objects in methods called repeatedly without proper disposal
- Working with model lifecycle events like onWillDispose and onDidClose
- Auditing chat features and pooled UI components for per-operation growth
- Backend and frontend developers fixing memory leak reports
- Code reviewers checking event subscription patterns
- Feature developers working with lifecycle callbacks and disposables
- Test authors validating that test suites don't leak disposables
memory-leak-audit FAQ
Use Event.once() for truly one-time events (lifecycle, close). Use MutableDisposable when a method is called repeatedly but should only have one active listener at a time. Combine both when a repeated method should fire only once per call.
this._register() adds to the class-level store, so every call accumulates more listeners. For repeated methods, use MutableDisposable to replace the listener, or return IDisposable to let the caller manage cleanup.
Check that listener counts and object counts stabilize after repeated operations (don't grow linearly). Run ensureNoDisposablesAreLeakedInTestSuite() in tests. For chat features, use npm run perf:chat-leak to detect per-message heap growth above 2 MB.
addDisposableListener() returns an IDisposable that can be tracked and cleaned up. addEventListener() has no cleanup mechanism and will leak if not manually removed. Always use addDisposableListener() in VS Code.
When a factory method creates items that are reused or destroyed individually, wrap each item with its own DisposableStore. Never register item listeners to the pool's this._register(), or they'll accumulate across all items.
Full instructions (SKILL.md)
Source of truth, from microsoft/vscode.
name: memory-leak-audit description: '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.'
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().
// 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.
Step 2: One-Time Events
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().
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.
// 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.
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.
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.
// 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.
Step 6: Test Validation
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
});
});
Quick Reference
| 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 | 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 |
Verification
After fixing leaks, verify by:
- Checking listener counts before/after repeated operations
- Running
ensureNoDisposablesAreLeakedInTestSuite()in tests - Confirming object counts stabilize (don't grow linearly with usage)
- For chat-specific leaks: Run the chat memory leak checker via
npm run perf:chat-leak(see thechat-perfskill). 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 --verbosefor more accurate results.
Related skills
More from microsoft/vscode and the wider catalog.

sessions
Agents window architecture reference for VS Code — layers, layout, session management, and development guidelines.

tool-rename-deprecation
Preserve backward compatibility when renaming tool references by maintaining legacy name arrays.

update-screenshots
Download and commit screenshot baselines from VS Code CI runs after visual changes are detected.

accessibility
Ensure VS Code features are accessible to all users with keyboard navigation, screen reader support, and ARIA annotations.

expect
Verify React/web code changes in a real browser before claiming completion.

react-doctor
Scan React code for security, performance, accessibility, and architecture issues with a 0–100 health score.