Skip to content

Fix memory leak in AutoRepliesPtyServiceContribution on process dispose#312150

Open
winjo wants to merge 1 commit intomicrosoft:mainfrom
winjo:fix/auto-replies-process-dispose-leak
Open

Fix memory leak in AutoRepliesPtyServiceContribution on process dispose#312150
winjo wants to merge 1 commit intomicrosoft:mainfrom
winjo:fix/auto-replies-process-dispose-leak

Conversation

@winjo
Copy link
Copy Markdown

@winjo winjo commented Apr 23, 2026

The problem

AutoRepliesPtyServiceContribution.handleProcessDispose only clears the inner per-process Map<string, TerminalAutoResponder> of _autoResponders. It never removes the key from the outer _autoResponders map, and it never removes the corresponding entry from _terminalProcesses.

ptyService calls handleProcessDispose(id) on every TerminalProcess's onProcessExit, so both maps grow monotonically for the lifetime of the pty host. Each leaked entry keeps a TerminalProcessUnixTerminal_socket (ReadStream) chain alive, which in turn keeps the pipe/pty file descriptors and their kernel read buffers allocated.

How it shows up

  • JS heap snapshot comparison between two pty-host snapshots shows near-zero JS heap delta, but a steady stream of extra ReadStream, TerminalProcess, UnixTerminal, and ArrayBuffer/Uint32Array instances.
  • ps RSS for the pty host grows by tens of MiB after a batch of spawned-then-exited terminals (we observed +70 MiB after a single task that spawned a few dozen pty-backed child processes).
  • In DevTools → Memory, the retainer chain of a leaked ReadStream reliably walks back to _terminalProcesses on an AutoRepliesPtyServiceContribution instance held by PtyService.

The fix

Add the two missing delete calls at the end of handleProcessDispose, unconditionally (they must run even when _autoResponders has no entry for the id, so they sit outside the existing if block).

this._autoResponders.delete(persistentProcessId);
this._terminalProcesses.delete(persistentProcessId);

After the fix, _terminalProcesses.size and _autoResponders.size track the number of live ptys, and the native fd/buffer memory is released as soon as the process exits.

How to verify

  1. Open a pty host heap snapshot before and after spawning & closing a batch of terminals.
  2. Comparison view: before the fix, ReadStream / TerminalProcess / UnixTerminal have # Deleted = 0; after the fix, # New ≈ # Deleted.
  3. ps -o rss= <pty-host-pid> stops climbing proportionally to the number of closed terminals.

`handleProcessDispose` only cleared the inner auto-responders map and
never removed entries from `_terminalProcesses` or `_autoResponders`.
Every exited pty stayed referenced, retaining the `TerminalProcess`,
its `UnixTerminal`, the `_socket` (`ReadStream`), and the underlying
pipe file descriptors / kernel buffers.

Symptoms: JS heap barely grows between snapshots, but RSS climbs by
tens of MiB per batch of spawned+exited terminals because the native
backing resources are held alive.

Fix: also `delete(persistentProcessId)` from both outer maps at the
end of `handleProcessDispose`, so each map's size tracks the number
of live ptys.
@vs-code-engineering
Copy link
Copy Markdown
Contributor

📬 CODENOTIFY

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

@anthonykim1

Matched files:

  • src/vs/platform/terminal/node/terminalContrib/autoReplies/autoRepliesContribController.ts

@meganrogge meganrogge enabled auto-merge (squash) April 23, 2026 19:02
@meganrogge meganrogge added this to the 1.118.0 milestone Apr 23, 2026
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.

3 participants