Find memory leaks in vscode to improve robustness and performance.
git clone git@github.com:SimonSiefke/vscode-memory-leak-finder.git &&
cd vscode-memory-leak-finder &&
npm ci &&
npm run e2eSplit the selected test files across parallel jobs with the Jest-compatible, one-based --shard=<index>/<count> option:
node packages/cli/bin/test.js --cwd packages/e2e --measure promises-with-stack-trace --shard=1/2
node packages/cli/bin/test.js --cwd packages/e2e --measure promises-with-stack-trace --shard=2/2Each test file belongs to exactly one shard. Results from separate jobs can be downloaded into directories whose names start with
vscode-memory-leak-finder-results-linux- and combined with:
node packages/build/src/mergeArtifacts.tsFinds pending Promises added during a test and reports their shortest strong path from a GC root, together with per-path counts and retained bytes. Inspector query handles are released before the final heap snapshot.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure pending-promises-with-retainers --only baseRanks allocation sources by the bytes they still own after garbage collection. Source-map-aware results include surviving allocation count, dominated object count, and retained bytes.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure retained-bytes-by-source --only baseMeasures live V8 native contexts from before and after heap snapshots. A growing count can reveal leaked windows, workers, realms, or VM contexts.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure native-context-count --only baseTracks Node async resources created during the test and reports resources that remain active, grouped by resource type and creation stack. The tracker stores metadata only and removes its hook during capture.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure active-async-resources-with-stack-traces --only baseCompares V8 object shapes before and after a test. Results identify constructor, prototype, elements kind, descriptor names, shape-count delta, and live-instance delta.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure object-shape-difference --only baseSpies on URL.createObjectURL and URL.revokeObjectURL from renderer startup and reports cumulative call counts plus the number of created object URLs that are still active after each test.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure object-url-count --only baseCaptures allocation-aware renderer and extension-host heap snapshots, computes source-mapped retained ownership, and generates the interactive VS Code Memory City visualization.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure memory-city --only baseBuild or preview the standalone viewer:
npm --prefix packages/visualizations run build
npm --prefix packages/visualizations run devCaptures two detailed Chromium MemoryInfra snapshots around a browser scenario. The informational result compares per-process private footprint and detailed allocator paths, retains normalized allocator attributes and ownership edges, and reports trace completeness without storing the full raw trace. Because allocator paths are hierarchical, their sizes are not additive.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure chromium-memory-dump --only base
npm run build-chartsThe generated process and allocator charts are written to .vscode-charts/chromium-memory-dump-processes and .vscode-charts/chromium-memory-dump-allocators.
Measures the native backing-store bytes retained by live ArrayBuffer objects. The result includes before, after, and delta byte and backing-store counts.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure array-buffer-bytes --only baseMeasures the number of native Chromium PerformanceMark objects before and after a browser scenario. The generated chart shows the initial count in black and growth in red.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure performance-mark-counts --only base
npm run build-chartsMeasures the native self-size bytes retained by Chromium PerformanceMark objects before and after a browser scenario. The generated chart shows the initial bytes in black and growth in red.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure performance-mark-bytes --only base
npm run build-chartsMeasures the total number of arrays.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure array-count --only baseMeasures the total number of elements in all arrays.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure array-element-count --only baseMeasures the total number of classes.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure class-count --only baseMeasures V8 compiled-code bytes from before and after heap snapshots. Results include the exact total, attributed/shared/unattributed buckets, and rankings by function size and growth.
node packages/cli/bin/test.js --cwd packages/e2e --measure-after --measure compiled-code-size --only baseMeasures V8 concatenated-string nodes whose bounded prefix is an Error-style
stack trace. The result includes before/after matching counts and diagnostic
totals for all concatenated strings; stack contents are not written to the
result.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure concatenated-error-string-count --only baseMeasures V8 concatenated-string nodes and reconstructs their values when the heap snapshot contains enough rope data. The before/after data is an array of strings, with V8's node name used when a value cannot be reconstructed.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure concatenated-strings --only baseMeasures CPU instructions and cycles for the inspected process.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure cpu-performance-counters --only baseOn Linux, measures CPU activity and aggregate proportional memory for the Electron main process and its descendants during the scenario. CPU data comes from inherited perf stat counters. Memory data comes from the sum of /proc/<pid>/smaps_rollup PSS sampled every 250 ms, so sampledPeakPssMiB can miss shorter spikes. The result is informational and never reports a leak.
node packages/cli/bin/test.js --cwd packages/e2e --measure-after --measure linux-process-tree-resources --only baseMeasures the same process-tree resources from Electron launch through scenario completion. Use --startup-runs to restart Electron and aggregate every numeric metric across independent samples.
node packages/cli/bin/test.js --cwd packages/e2e --measure-after --measure linux-process-tree-resources-from-start --startup-runs 5 --only baseBoth measures run collection and parsing in a dedicated Linux process-tree worker. The caller starts it with the process-tree root PID, receives the parsed result when stopping it, and disposes the worker immediately after measurement. They require Linux, perf, access to the requested perf events, and readable smaps_rollup files. Missing facilities or permissions fail the measure instead of producing zero-valued counters. GNU time is intentionally not used because its maximum RSS is not the simultaneous sum of Electron's processes.
Measures the total number of detached dom nodes.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure detached-dom-node-count --only baseMeasures dom nodes, jsEventListeners and documents.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure dom-counters --only baseMeasures the total number of dom nodes.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure dom-node-count --only baseMeasures flat string values represented by more than one V8 heap node. Each duplicated value occurs once in the before/after string arrays.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure duplicated-strings --only baseMeasures the total number of edit context.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure edit-context-count --only baseMeasures the total number of event listeners.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure event-listener-count --only baseMeasures the event listeners.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure event-listeners --only baseMeasures the total number of live FinalizationRegistry instances.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure finalization-registry-count --only baseMeasures the total number of functions.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure function-count --only baseMeasures global variables / global lexical scope names.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure global-lexical-scope-names --only baseReports own string-named properties added to globalThis during the measured scenario. Global lexical bindings are covered separately by global-lexical-scope-names.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure global-property-difference --only baseMeasures heap usage.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure heap-usage --only baseMeasures the number of instances of each class.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure instance-counts --only baseMeasures the number of intersection observers.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure intersection-observer-count --only baseMeasures the total number of elements in all Maps.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure map-size --only baseMeasures the total number of MediaQueryLists.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure media-query-list-count --only baseMeasures the total number of MutationObservers.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure mutation-observer-count --only baseMeasures the count of each function.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure named-function-count --only baseMeasures the difference in counts of each function.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure named-function-difference --only baseMeasures the total number of Promises.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure promise-count --only baseMeasures the total number of Regex instances.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure regex-count --only baseMeasures the total number of ResizeObservers.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure resize-observer-count --only baseMeasures the total number of elements in all Sets.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure set-size --only baseMeasures the total number of Timeouts.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure set-timeout --only baseMeasures active timeouts using workbench instrumentation installed at application startup.
node packages/cli/bin/test.js --cwd packages/e2e --runs 1 --measure-after --measure tracked-timeouts --timeout-between 5000 --only baseReports allocation sites that retain instances after forced garbage collection. Results are informational potential leak candidates.
node packages/cli/bin/test.js --cwd packages/e2e --measure tracked-allocation-leaks --only editor-openCorrelates allocation churn with sampled JavaScript CPU self-time by source file. The CPU percentage describes the source file, not the cost of allocation itself.
node packages/cli/bin/test.js --cwd packages/e2e --measure tracked-allocation-performance --only editor-type-many-charactersMeasures V8 TurboFan optimization and deoptimization activity.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure v8-turbofan-stats --only baseMeasures the total number of WeakMaps.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure weak-map-count --only baseMeasures the total number of WeakSets.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure weak-set-count --only baseMeasures the total number of Windows.
node packages/cli/bin/test.js --cwd packages/e2e --check-leaks --measure-after --measure window-count --only base- packages/charts: Visualizations for test output
- packages/cli: Command Line Interface, similar to jest
- packages/devtools-protocol: Functionality related to Chrome Devtools Protocol
- packages/e2e: The e2e test scenarios
- packages/file-watcher-worker: Watch files for changes
- packages/injected-code: Code injected to the page for e2e tests
- packages/memory-leak-finder: Library for finding memory leaks
- packages/memory-leak-worker: Process for finding memory leaks (uses the library from above)
- packages/page-object: Page Object Model to simplify e2e tests
- packages/source-map-worker: Functions for querying original positions and function names using source maps
- packages/test-coordinator: Determines which tests to run, launches VSCode, file-watcher-worker, test-worker, memory-leak-worker, video-recording-worker
- packages/test-worker: Runs tests
- packages/visualizations: Interactive source-mapped memory visualizations
- packages/test-worker-commands: Functions used by test-worker
- packages/video-recording-worker: Record screencasts of the tests
Before and after a test is executed, all event listeners are queried using Chrome Devtools Protocol Runtime.queryObjects({ prototypeId: "EventTarget.prototype" }) and DomDebugger.getEventListeners.
We get an array of event listeners before and after, for example
and
// after
[
{
"type": "focusin",
"description": "()=>this.j()",
"objectId": "524841679309534768.4.2930",
"stack": [
"listener (file:///home/simon/.cache/repos/vscode-memory-leak-finder/.vscode-test/vscode-linux-x64-1.83.1/resources/app/out/vs/workbench/workbench.desktop.main.js:148:37007)",
],
"sourceMaps": [
"https://ticino.blob.core.windows.net/sourcemaps/f1b07bd25dfad64b0167beb15359ae573aecd2cc/core/vs/workbench/workbench.desktop.main.js.map",
],
},
{
"type": "keydown",
"description": "N=>{new P.$qO(N).equals(2)&&N.preventDefault()}",
"objectId": "3680313440875909344.4.4572",
"stack": [
"listener (file:///home/simon/.cache/repos/vscode-memory-leak-finder/.vscode-test/vscode-linux-x64-1.83.1/resources/app/out/vs/workbench/workbench.desktop.main.js:244:39878)",
],
"sourceMaps": [
"https://ticino.blob.core.windows.net/sourcemaps/f1b07bd25dfad64b0167beb15359ae573aecd2cc/core/vs/workbench/workbench.desktop.main.js.map",
],
"originalStack": ["/src/vs/base/browser/ui/menu/menu.ts:122:58"],
},
]The before and after arrays are compared to see which event listeners have been added. In the example above, there is one keydown listener more in the after array which is not in the before array.
The tests are structured in a way one would be expect that the number of event listeners before and after the test are equal. For example, when opening and closing the menu, one would expect the number of event listeners stays equal. This is the menu toggle test:
// title-bar-menu-toggle.js
export const run = async ({ TitleBar }) => {
await TitleBar.showMenuFile()
await TitleBar.hideMenuFile()
}Every time the test was executed, event listeners increased by one keydown listener in /src/vs/base/browser/ui/menu/menu.ts:122:58, which indicates a memory leak and in this case was precisely the location of the memory leak.
In other cases, the output for memory leaks might not be quite as clear, but maybe still helpful. This is the output for the notebook-open test (opening and closing a notebook):
[
{
"type": "contextmenu",
"description": "n=>{t.$_O.stop(n,!0)}",
"objectId": "2723967474668247540.4.13637",
"stack": [
"listener (file:///home/simon/.cache/repos/vscode-memory-leak-finder/.vscode-test/vscode-linux-x64-1.83.1/resources/app/out/vs/workbench/workbench.desktop.main.js:244:18357)"
],
"sourceMaps": [
"https://ticino.blob.core.windows.net/sourcemaps/f1b07bd25dfad64b0167beb15359ae573aecd2cc/core/vs/workbench/workbench.desktop.main.js.map"
],
"count": 1,
"originalStack": ["/src/vs/base/browser/ui/actionbar/actionbar.ts:370:117"]
},
{
"type": "-monaco-gesturetap",
"description": "r=>this.onClick(r,!0)",
"objectId": "2723967474668247540.4.13695",
"stack": [
"listener (file:///home/simon/.cache/repos/vscode-memory-leak-finder/.vscode-test/vscode-linux-x64-1.83.1/resources/app/out/vs/workbench/workbench.desktop.main.js:244:10198)"
],
"sourceMaps": [
"https://ticino.blob.core.windows.net/sourcemaps/f1b07bd25dfad64b0167beb15359ae573aecd2cc/core/vs/workbench/workbench.desktop.main.js.map"
],
"count": 1,
"originalStack": ["/src/vs/base/browser/ui/actionbar/actionViewItems.ts:121:68"]
},
{
"type": "mousedown",
"description": "r=>{c||I.$_O.stop(r,!0),this._action.enabled&&r.button===0&&o.classList.add(\"active\")}",
"objectId": "2723967474668247540.4.13697",
"stack": [
"listener (file:///home/simon/.cache/repos/vscode-memory-leak-finder/.vscode-test/vscode-linux-x64-1.83.1/resources/app/out/vs/workbench/workbench.desktop.main.js:244:10258)"
],
"sourceMaps": [
"https://ticino.blob.core.windows.net/sourcemaps/f1b07bd25dfad64b0167beb15359ae573aecd2cc/core/vs/workbench/workbench.desktop.main.js.map"
],
"count": 1,
"originalStack": ["/src/vs/base/browser/ui/actionbar/actionViewItems.ts:123:70"]
},
{
"type": "click",
"description": "r=>{I.$_O.stop(r,!0),this.m&&this.m.isMenu||this.onClick(r)}",
"objectId": "2723967474668247540.4.13699",
"stack": [
"listener (file:///home/simon/.cache/repos/vscode-memory-leak-finder/.vscode-test/vscode-linux-x64-1.83.1/resources/app/out/vs/workbench/workbench.desktop.main.js:244:10475)"
],
"sourceMaps": [
"https://ticino.blob.core.windows.net/sourcemaps/f1b07bd25dfad64b0167beb15359ae573aecd2cc/core/vs/workbench/workbench.desktop.main.js.map"
],
"count": 1,
"originalStack": ["/src/vs/base/browser/ui/actionbar/actionViewItems.ts:145:65"]
},
{
"type": "dblclick",
"description": "r=>{I.$_O.stop(r,!0)}",
"objectId": "2723967474668247540.4.13701",
"stack": [
"listener (file:///home/simon/.cache/repos/vscode-memory-leak-finder/.vscode-test/vscode-linux-x64-1.83.1/resources/app/out/vs/workbench/workbench.desktop.main.js:244:10572)"
],
"sourceMaps": [
"https://ticino.blob.core.windows.net/sourcemaps/f1b07bd25dfad64b0167beb15359ae573aecd2cc/core/vs/workbench/workbench.desktop.main.js.map"
],
"count": 1,
"originalStack": ["/src/vs/base/browser/ui/actionbar/actionViewItems.ts:154:68"]
},
{
"type": "mouseout",
"description": "n=>{I.$_O.stop(n),o.classList.remove(\"active\")}",
"objectId": "2723967474668247540.4.13705",
"stack": [
"listener (file:///home/simon/.cache/repos/vscode-memory-leak-finder/.vscode-test/vscode-linux-x64-1.83.1/resources/app/out/vs/workbench/workbench.desktop.main.js:244:10662)"
],
"sourceMaps": [
"https://ticino.blob.core.windows.net/sourcemaps/f1b07bd25dfad64b0167beb15359ae573aecd2cc/core/vs/workbench/workbench.desktop.main.js.map"
],
"count": 2,
"originalStack": ["/src/vs/base/browser/ui/actionbar/actionViewItems.ts:159:56"]
}
]It seems there is memory leak when opening and closing a notebook. But just looking at the output, it's difficult to say much more. It's not clear where exactly the memory leak is and one might need to look more closely at the actionbar.ts and actionViewItems.ts code.
| Component | Issue | Status |
|---|---|---|
| Menu | microsoft/vscode#195580 | Fixed |
| Dropdown | microsoft/vscode#197767 | Fixed |
| MenuBar | microsoft/vscode#198051 | Fixed |
| DefaultWorkerFactory | microsoft/vscode#198709 | Fixed |
| ExtensionList | microsoft/vscode#198709 | Fixed |
| SimpleFindWidget | microsoft/vscode#199043 | Fixed |
| ColorPickerWidget | microsoft/vscode#199814 | Fixed |
| DiffEditor | microsoft/vscode#200381 | Fixed |
| QuickPick | microsoft/vscode#201320 | Fixed |
| Terminal | xtermjs/xterm.js#4935 | Fixed |
| KeyBindingsEditor | microsoft/vscode#202455 | Fixed |
| NotebookEditorWidget | microsoft/vscode#204756 | Fixed |
| SettingsEditor2 | microsoft/vscode#216763 | Fixed |
| SettingEnumRenderer | microsoft/vscode#216855 | Fixed |
| GettingStarted | microsoft/vscode#216858 | Fixed |
| ExtensionTabs | microsoft/vscode#219726 | Fixed |
| Output | microsoft/vscode#221605 | Fixed |
| StickyScroll | microsoft/vscode#221622 | Fixed |
| SelectBox | microsoft/vscode#221507 | Fixed |
| DebugView | microsoft/vscode#225334 | Fixed |
| SettingsIndicators | microsoft/vscode#236417 | Fixed |
This project is based on the jest cli, playwright and fuite.