Skip to content

Conversation

@SimonSiefke
Copy link
Contributor

This fixes a memory leak related to context keys. In contextkeys.ts there is this function:

this._register(Event.runAndSubscribe(onDidRegisterWindow, ({ window, disposables }) => disposables.add(addDisposableListener(window, EventType.FOCUS_IN, () => this.updateInputContextKeys(window.document, disposables), true)), { window: mainWindow, disposables: this._store }));

When a new window is registered, for each window it adds a focusin event listener for updating the context keys.


The memory leak problem seems to be that it passes the disposables of this window to the updateInputContextKeys function.

private updateInputContextKeys(ownerDocument: Document, disposables: DisposableStore): void {

 /* ... */

if (isInputFocused) {
	const tracker = disposables.add(trackFocus(ownerDocument.activeElement as HTMLElement));
	Event.once(tracker.onDidBlur)(() => {

	  /* ... */

		tracker.dispose();
	}, undefined, disposables);
}

Here the FocusTracker disposable is added to disposables each time an input element in the window is being focused, making disposables grow each time.

For example:

  1. A window is open
  2. The quickpick is being opened
  3. The quickpick input is being focused
  4. A focus tracker is created and added to the disposables array of this window
  5. Close the quickpick
  6. Repeat the steps 2-5. Each time a FocusTracker is being added to the disposables array

Fix

The fix uses a MutableDisposable to dispose and overwrite the previous focus trackers.

private updateInputContextKeys(ownerDocument: Document, mutableDisposable: MutableDisposable<any>): void {

 /* ... */

const store = new DisposableStore();
mutableDisposable.value = store;

if (isInputFocused) {
	const tracker = store.add(trackFocus(ownerDocument.activeElement as HTMLElement));
	Event.once(tracker.onDidBlur)(() => {

	  /* ... */

		tracker.dispose();
	}, undefined, store);
}

This way when an input element in the window is focused, the previous FocusTracker will be unreferenced by setting mutableDisposable.value with the new DisposableStore.

Before

When showing and hiding the quickpick 31 times, the number of FocusTracker instances increases each time from 63 to 125:

named-instance-count

After

When showing and hiding the quickpick 31 times, the number of FocusTracker stays constant at 63:

named-instance-count

@deepak1556 deepak1556 added this to the July 2025 milestone Jul 30, 2025
@hediet hediet merged commit 50bb1f1 into microsoft:main Jul 30, 2025
17 checks passed
@bpasero
Copy link
Member

bpasero commented Aug 5, 2025

@SimonSiefke thanks, I am pushing #259818 as refinement over your work. Notable:

  • avoid use of any
  • make the MutableDisposable a thing at the controller level and not within the function itself

Please speak up if that change does not make sense to you.

fyi @deepak1556

@vs-code-engineering vs-code-engineering bot locked and limited conversation to collaborators Sep 13, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

6 participants