Skip to content

Conversation

@SimonSiefke
Copy link
Contributor

Fixes #276958

  • Sets the this._ptyProcess to undefined on dispose so that ptyProcess can be garbage collected on dispose.
  • Sets the this._processStartupComplete to undefined on dispose so that the promise can be garbage collected

Before

When running a task 17 times, the number of various objects and functions grows each time:

{
  "namedFunctionCount3": [
    {
      "count": 40,
      "delta": 17,
      "name": "EventEmitter",
      "sourceLocation": "node:events:219:21"
    },
    {
      "count": 39,
      "delta": 34,
      "name": "EventEmitter2",
      "sourceLocation": "vscode/node_modules/node-pty/lib/eventEmitter2.js:7:26",
      "originalName": null
    },
    {
      "count": 38,
      "delta": 34,
      "name": "anonymous",
      "sourceLocation": "vscode/node_modules/node-pty/lib/eventEmitter2.js:14:39",
      "originalName": null
    },
    {
      "count": 32,
      "delta": 17,
      "name": "Buffer",
      "sourceLocation": "node:buffer:270:15"
    },
    {
      "count": 24,
      "delta": 17,
      "name": "ReadableState",
      "sourceLocation": "node:internal/streams/readable:261:22"
    },
    {
      "count": 22,
      "delta": 17,
      "name": "WritableState",
      "sourceLocation": "node:internal/streams/writable:303:22"
    },
    {
      "count": 20,
      "delta": 17,
      "name": "ChildProcessMonitor",
      "sourceLocation": "vscode/out/vs/platform/terminal/node/childProcessMonitor.js:24:13",
      "originalLocation": "src/vs/platform/terminal/node/childProcessMonitor.ts:52:19",
      "originalName": "ChildProcessMonitor"
    },
    {
      "count": 20,
      "delta": 17,
      "name": "StringDecoder",
      "sourceLocation": "node:string_decoder:80:22"
    },
    {
      "count": 20,
      "delta": 17,
      "name": "UnixTerminal",
      "sourceLocation": "vscode/node_modules/node-pty/lib/unixTerminal.js:45:25",
      "originalName": null
    },
    {
      "count": 20,
      "delta": 17,
      "name": "ReadStream",
      "sourceLocation": "node:tty:50:19"
    },
    {
      "count": 19,
      "delta": 17,
      "name": "anonymous",
      "sourceLocation": "vscode/node_modules/node-pty/lib/terminal.js:170:30",
      "originalName": null
    },
    {
      "count": 19,
      "delta": 17,
      "name": "anonymous",
      "sourceLocation": "vscode/node_modules/node-pty/lib/terminal.js:171:28",
      "originalName": null
    },
    {
      "count": 19,
      "delta": 17,
      "name": "anonymous",
      "sourceLocation": "vscode/node_modules/node-pty/lib/unixTerminal.js:140:43",
      "originalName": null
    },
    {
      "count": 19,
      "delta": 17,
      "name": "anonymous",
      "sourceLocation": "vscode/node_modules/node-pty/lib/unixTerminal.js:105:43",
      "originalName": null
    },
    {
      "count": 19,
      "delta": 17,
      "name": "anonymous",
      "sourceLocation": "vscode/node_modules/node-pty/lib/terminal.js:91:33",
      "originalName": null
    },
    {
      "count": 19,
      "delta": 17,
      "name": "anonymous",
      "sourceLocation": "vscode/node_modules/node-pty/lib/terminal.js:92:33",
      "originalName": null
    },
    {
      "count": 19,
      "delta": 17,
      "name": "anonymous",
      "sourceLocation": "vscode/out/vs/base/common/decorators.js:59:34",
      "originalLocation": "src/vs/base/common/decorators.ts:83:31",
      "originalName": "debounce"
    }
  ],
  "isLeak": true
}

After

When running a task 17 times, there are much less leaked functions and objects:

{
  "namedFunctionCount3": [
    {
      "count": 20,
      "delta": 17,
      "name": "ChildProcessMonitor",
      "sourceLocation": "file:///home/simon/.cache/repos/vscode/out/vs/platform/terminal/node/childProcessMonitor.js:24:13",
      "originalLocation": "src/vs/platform/terminal/node/childProcessMonitor.ts:52:19",
      "originalName": "ChildProcessMonitor"
    },
    {
      "count": 19,
      "delta": 17,
      "name": "anonymous",
      "sourceLocation": "file:///home/simon/.cache/repos/vscode/out/vs/base/common/decorators.js:59:34",
      "originalLocation": "src/vs/base/common/decorators.ts:83:31",
      "originalName": "debounce"
    }
  ],
  "isLeak": true
}

@vs-code-engineering
Copy link

vs-code-engineering bot commented Nov 24, 2025

📬 CODENOTIFY

The following users are being notified based on files changed in this PR:

@Tyriar

Matched files:

  • src/vs/platform/terminal/node/terminalProcess.ts

Copy link
Member

@Tyriar Tyriar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SimonSiefke do you know why this is necessary? Disposing of a TerminalProcess is mean to release all handles to it and therefore free it and it's own references up for GC?

@Tyriar Tyriar added this to the December / January 2026 milestone Dec 8, 2025
@SimonSiefke
Copy link
Contributor Author

Hi @Tyriar ,
I don't think it's necessary in general, but rather it can helpful sometimes to set properties to undefined on dispose to reduce the amount of leaked data.

I believe the whole TerminalProcess instance is leaking to due some kind of issue with debounce.

By setting this_ptyProcess in TerminalProcess to undefined, it ensures this._ptyProcess can be garbage collected even though TerminalProcess instances might still be leaking.

But you are definitly right: When there would be no other leaks, this.ptyProcess would be garbage collected automatically also.


About the promise, I'm not entirely sure. I think it's best to always set all promise properties to undefined on dispose.

@anthonykim1
Copy link
Contributor

@SimonSiefke Do you know how I can repro this memory leak?
If its disposable leak I thought they showed up in console of dev tool.

I've tried creating and deleting terminal but that alone did not seem to show the leak on dev tool console. Are you watching the processes in some specific tool that shows the leak

@SimonSiefke
Copy link
Contributor Author

Hey @anthonykim1,

I have this repo that I'm using: https://github.com/SimonSiefke/vscode-memory-leak-finder/

Prerequisites: Ensure you have node 24 or later installed.

Test script:

git clone [email protected]:SimonSiefke/vscode-memory-leak-finder.git &&
cd vscode-memory-leak-finder &&
npm ci &&
node packages/cli/bin/test.js --cwd packages/e2e   --only terminal.create --runs 97 --check-leaks --measure named-function-count3 --run-skipped-tests-anyway --measure-after --inspect-ptyhost && 
cat .vscode-memory-leak-finder-results/pty-host/named-function-count3/terminal.create.json

With local vscode version (adjust --vscode-path to your local vscode path):

git clone [email protected]:SimonSiefke/vscode-memory-leak-finder.git &&
cd vscode-memory-leak-finder &&
npm ci &&
node packages/cli/bin/test.js --cwd packages/e2e   --only terminal.create --runs 97 --check-leaks --measure named-function-count3 --run-skipped-tests-anyway --measure-after --inspect-ptyhost --vscode-path "/home/simon/.cache/repos/vscode/scripts/code.sh"  &&
cat .vscode-memory-leak-finder-results/pty-host/named-function-count3/terminal.create.json

The script launches vscode, takes a heapsnapshot of pty-host, runs an e2e tests which creates and closes a terminal 97 times and takes a heapsnapshot of pty-host again. It then compares the heapsnapshot location arrays to see how many of each function have been added between the heapsnapshots. That's how it generates this data:

{
  "namedFunctionCount3": [
    {
      "count": 233,
      "delta": 218,
      "name": "Timeout",
      "sourceLocation": "node:internal/timers:174:13"
    },
    {
      "count": 201,
      "delta": 194,
      "name": "EventEmitter2",
      "sourceLocation": "resources/app/node_modules/node-pty/lib/eventEmitter2.js:7:26",
      "originalLocation": "src/eventEmitter2.ts:15:0",
      "originalName": ""
    },
    {
      "count": 200,
      "delta": 194,
      "name": "anonymous",
      "sourceLocation": "resources/app/node_modules/node-pty/lib/eventEmitter2.js:14:39",
      "originalLocation": "src/eventEmitter2.ts:21:21",
      "originalName": ""
    },
    {
      "count": 121,
      "delta": 97,
      "name": "EventEmitter",
      "sourceLocation": "node:events:219:21"
    },
    {
      "count": 113,
      "delta": 97,
      "name": "Buffer",
      "sourceLocation": "node:buffer:270:15"
    },
    {
      "count": 105,
      "delta": 97,
      "name": "ReadableState",
      "sourceLocation": "node:internal/streams/readable:261:22"
    },
    {
      "count": 103,
      "delta": 97,
      "name": "WritableState",
      "sourceLocation": "node:internal/streams/writable:303:22"
    },
    {
      "count": 101,
      "delta": 97,
      "name": "UnixTerminal",
      "sourceLocation": "resources/app/node_modules/node-pty/lib/unixTerminal.js:45:25",
      "originalLocation": "src/unixTerminal.ts:56:14",
      "originalName": ""
    },
    {
      "count": 101,
      "delta": 97,
      "name": "ReadStream",
      "sourceLocation": "node:tty:50:19"
    },
    {
      "count": 101,
      "delta": 97,
      "name": "Ge",
      "sourceLocation": "resources/app/out/vs/platform/terminal/node/ptyHostMain.js:32:497",
      "originalLocation": "src/vs/platform/terminal/node/childProcessMonitor.ts:52:19",
      "originalName": "ChildProcessMonitor"
    },
    {
      "count": 101,
      "delta": 97,
      "name": "StringDecoder",
      "sourceLocation": "node:string_decoder:80:22"
    },
    {
      "count": 100,
      "delta": 97,
      "name": "anonymous",
      "sourceLocation": "resources/app/out/vs/platform/terminal/node/ptyHostMain.js:25:79452",
      "originalLocation": "src/vs/base/common/decorators.ts:121:32",
      "originalName": "throttle"
    },
    {
      "count": 100,
      "delta": 97,
      "name": "anonymous",
      "sourceLocation": "resources/app/out/vs/platform/terminal/node/ptyHostMain.js:32:8059",
      "originalLocation": "src/vs/platform/terminal/node/terminalProcess.ts:337:20",
      "originalName": "TerminalProcess.setupPtyProcess"
    },
    {
      "count": 100,
      "delta": 97,
      "name": "anonymous",
      "sourceLocation": "resources/app/out/vs/platform/terminal/node/ptyHostMain.js:32:7812",
      "originalLocation": "src/vs/platform/terminal/node/terminalProcess.ts:319:20",
      "originalName": "TerminalProcess.setupPtyProcess"
    },
    {
      "count": 100,
      "delta": 97,
      "name": "anonymous",
      "sourceLocation": "resources/app/node_modules/node-pty/lib/terminal.js:92:33",
      "originalLocation": "src/terminal.ts:97:21",
      "originalName": ""
    },
    {
      "count": 100,
      "delta": 97,
      "name": "anonymous",
      "sourceLocation": "resources/app/node_modules/node-pty/lib/terminal.js:91:33",
      "originalLocation": "src/terminal.ts:96:20",
      "originalName": ""
    },
    {
      "count": 100,
      "delta": 97,
      "name": "anonymous",
      "sourceLocation": "resources/app/node_modules/node-pty/lib/unixTerminal.js:140:43",
      "originalLocation": "src/unixTerminal.ts:165:29",
      "originalName": ""
    },
    {
      "count": 100,
      "delta": 97,
      "name": "anonymous",
      "sourceLocation": "resources/app/node_modules/node-pty/lib/unixTerminal.js:105:43",
      "originalLocation": "src/unixTerminal.ts:123:30",
      "originalName": ""
    },
    {
      "count": 100,
      "delta": 97,
      "name": "anonymous",
      "sourceLocation": "resources/app/out/vs/platform/terminal/node/ptyHostMain.js:25:78961",
      "originalLocation": "src/vs/base/common/decorators.ts:83:31",
      "originalName": "debounce"
    }
  ],
  "isLeak": true
}

SVG

With npm run build-charts it one can see the json visualized as svg.

terminal create

The locations that have stayed the same are black, the locations that have been added / leaked are red.

@Tyriar Tyriar enabled auto-merge December 9, 2025 14:08
@Tyriar Tyriar merged commit dcb7026 into microsoft:main Dec 9, 2025
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Promise memory leak when creating terminal (pty-host)

4 participants