Operations 15 min read

How We Solved a Massive Memory Leak in a VSCode Extension Using llnode and heapdump

After releasing a new version of a VSCode extension, intermittent freezes were traced to a memory leak; the investigation used llnode and heapdump, tackled Electron version mismatches, extended DevTools parsing limits, and ultimately identified recursive socket callbacks as the root cause.

ELab Team
ELab Team
ELab Team
How We Solved a Massive Memory Leak in a VSCode Extension Using llnode and heapdump

Problem

After a new version of a VSCode extension was released, users reported occasional freezes, especially after the IDE entered sleep mode.

Data Collection

To diagnose the performance issue we first needed sufficient data. We tried several memory‑analysis tools, focusing on llnode and heapdump .

llnode

llnode works on a single process, but VSCode spawns many processes, so we first identified the process that runs the extension.

VSCode processes
VSCode processes

The VSCode documentation mentions the Extension Host process, which includes the launch argument --type=extensionHost:

/Applications/Visual Studio Code.app/Contents/MacOS/Electron --ms-enable-electron-run-as-node --nolazy --inspect-brk=55447 /Applications/Visual Studio Code.app/Contents/Resources/app/out/bootstrap-fork --type=extensionHost --skipWorkspaceStorageLock

Tests confirmed that the plugin runs in this extensionHost process.

We attempted to attach llnode directly to the process, but V8 reported no objects, likely because Electron applies special handling to the Node process.

llnode findjsobjects returned empty
llnode findjsobjects returned empty

Further attempts with a simple Electron "hello world" program yielded the same result, so we abandoned llnode.

heapdump

Introducing heapdump into the plugin caused a version mismatch error:

workbench.desktop.main.js:788 Error: The module '/Users/path/to/repo/node_modules/heapdump/build/Release/addon.node' was compiled against a different Node.js version using NODE_MODULE_VERSION 83. This version of Node.js requires NODE_MODULE_VERSION 89. Please try re‑compiling or re‑installing the module (for instance, using `npm rebuild` or `npm install`).

Heapdump calls V8 internals via a native addon, so the addon must be compiled against the exact Node version used by Electron (NODE_MODULE_VERSION 89). The Electron rebuild tool was used to compile the addon for the target Electron version, after which memory snapshots could be generated.

On the problematic machine we bundled the rebuilt addon with node-loader so that it is packaged inside the extension.

After installing the plugin, the process memory grew steadily and then spiked, as shown in the snapshot graph:

Memory usage graph
Memory usage graph

CPU usage also reached 400%, causing dump failures.

Parsing the Snapshot

Loading the snapshot in Chrome DevTools produced an error because the snapshot size exceeded V8's string length limit:

DevTools error
DevTools error

The relevant source code ( HeapSnapshotLoader.ts) shows that the snapshot is loaded as a single string, whose length is bounded by V8:

export class HeapSnapshotLoader {
  ...
  private buffer: string;
  write(chunk: string): void {
    this.buffer += chunk;
    ...
  }
  ...
}

We explored several avenues:

Using llnode to parse the snapshot – abandoned because its parser differs from V8.

Trimming the snapshot – infeasible without a format specification.

Modifying V8 – too risky and time‑consuming.

Instead we extended DevTools by implementing custom String and Uint32Array classes that internally store data in two‑dimensional arrays, thus bypassing V8's hard limits. The required methods and properties were added (clear, charCodeAt, slice, etc. for String; length, iterator, indexed access for Uint32Array). The String.toJson method was built on a lightly modified json-bigint library.

Running the modified DevTools still hit memory‑allocation failures, so we increased V8's heap size with the flag --max_old_space_size=16192:

./Chromium --custom-devtools-frontend=file:////Users/path/to/repo/devtools/devtools-frontend/out/Default/gen/front_end --js-flags="--max_old_space_size=16192"

Even then Chromium's own 2 GB single‑allocation limit caused failures. We reduced the maximum length of our custom Uint32Array from 2**32‑1 to 2**28‑1, and similarly adjusted the custom Map implementation.

After these tweaks the snapshot finally parsed successfully.

Analysis

Two major memory consumers were identified:

A ~280 MB string originating from the Live Share and Angular Console extensions.

A >700 MB retained size in a Socket object, introduced by a recent WebSocket feature.

Live Share string
Live Share string
Socket retained size
Socket retained size

The offending code was a recursive connect method that re‑added event listeners on each reconnection: this.innerSocket = io(wsServerHost) Because the socket instance is reused, each reconnection doubled the number of callbacks, leading to exponential memory growth.

Summary

The plugin runs in the VSCode extensionHost process.

llnode cannot directly parse Electron processes.

V8 imposes length limits on strings, TypedArrays and Maps.

Chromium limits a single memory allocation to 2 GB.

DevTools can be built separately and run inside a pre‑compiled Chromium.

Always remove event listeners (off) to avoid leaks.

References

Extension Host – https://code.visualstudio.com/api/advanced-topics/extension-host

heapdump source – https://github.com/bnoordhuis/node-heapdump/blob/master/src/heapdump.cc

Node.js releases – https://nodejs.org/en/download/releases/

electron‑rebuild – https://github.com/electron/electron-rebuild

node-loader – https://webpack.js.org/loaders/node-loader/#root

HeapSnapshotLoader source – https://github.com/ChromeDevTools/devtools-frontend/blob/04d4b64221e472bcbd5d1de16bef59c2cb9f8d02/front_end/entrypoints/heap_snapshot_worker/HeapSnapshotLoader.ts#L143

V8 string limit – https://chromium.googlesource.com/v8/v8.git/+/refs/heads/main/include/v8-primitive.h#125

V8 Uint32Array limit – https://chromium.googlesource.com/v8/v8.git/+/refs/heads/main/include/v8-typed-array.h#25

llnode source – https://github.com/nodejs/llnode/blob/279a540798a6c837a8ff49a7aeef15ba177fa749/src/llscan.cc#L1599

DevTools custom build – https://github.com/ChromeDevTools/devtools-frontend/blob/main/docs/workflows.md

json-bigint – https://github.com/sidorares/json-bigint

Browser memory limits article – https://textslashplain.com/2020/09/15/browser-memory-limits/

Memory test tool – https://webdbg.com/test/memory.aspx

Chromium 2 GB allocation limit – https://source.chromium.org/chromium/chromium/src/+/7a052054f35f5c976f2b72dc576092063f4950a3

Map size limit – https://source.chromium.org/chromium/chromium/src/+/main:v8/src/runtime/runtime-collections.cc;l=33

Socket reuse – https://socket.io/docs/v4/client-options/#forcenew

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Memory LeakVSCodedevtoolsPerformance debuggingheapdumpllnode
ELab Team
Written by

ELab Team

Sharing fresh technical insights

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.