Avoiding Memory Leaks in Visual Studio Editor Extensions

Christian Gunderman

Visual Studio extenders make VS even better by augmenting it with specialized tools, new languages, and workflows. As a Visual Studio extender, you can ensure your extension’s customers have the most performant, reliable experience possible by avoiding common sources of memory leaks, described within this blog post.

Background

In the early 2010s, a small team of engineers was spun up to rethink Visual Studio’s C++/COM/Win32 Editor and Extensibility in terms of new, cutting edge technologies like C#, .NET, and WPF.

This team created several foundational pieces, such as Visual Studio MEF and the Visual Studio Text data model platform. Together, these technologies and the innovations that followed them re-invigorated the Visual Studio ecosystem and enabled some of its most distinctive features, like CodeLens.

After a decade of iteration, the VS IDE team has accumulated a list of best practices for using these components from extensions without adversely impacting Visual Studio performance. In-process extensions can do just about anything, so in the words of ‘Uncle Ben’, “With great power comes great responsibility”.

The Future of Visual Studio Extensions details how we’ll improve Visual Studio’s resiliency in future releases by running extensions in a separate process so they can’t impact the IDE’s performance. For now, though, here’s some DOs and DON’Ts for crafting leak free VS Editor extensions with the existing in-process extensibility model.

MEF Part Memory Leaks

What are MEF Parts

At its base level, the Visual Studio text editor is a collection of MEF parts. For a class to be a MEF part, all that is required is that it be decorated with an [Export] attribute in C#, like the following, be declared in a DLL registered in the VSIX manifest as a MefComponent, and be imported somewhere by the IDE or another MEF part.

[Export(typeof(ITextViewCreationListener))]
internal class MyPart : ITextViewCreationListener
{
    private readonly Lazy<IContentTypeRegistryService> contentTypeRegistryService;

    [ImportingConstructor]
    public MyPart(Lazy<IContentTypeRegistryService> contentTypeRegistryService)
    {
        this.contentTypeRegistryService = contentTypeRegistryService;
    }

    public void TextViewCreated(ITextView textView)
    {
    }
}

In the example above, we are exporting a MEF part, implementing ITextViewCreationListener, and importing a service that implements IContentTypeRegistryService from the IDE.

Properties of MEF

MEF has some important properties that you should make note of to avoid MEF related leaks.

  • The default policy for MEF parts in Visual Studio is shared, meaning that up to one instance of each part is created, no matter how often it’s imported.
  • Shared MEF parts can be configured to be instantiated only as needed, as shown via the Lazy<T> class, but once they are created, these parts and any object they reference will live until Visual Studio is restarted.

Common MEF Part Leaks

Consider a second MEF part example: an extender is exporting an ITextViewCreationListener, that will be signaled whenever a new text editor is opened, storing a reference to the text view for later use.

[Export(typeof(ITextViewCreationListener))]
internal sealed class MyPart : ITextViewCreationListener
{
    private readonly Lazy<IContentTypeRegistryService> contentTypeRegistryService;

    private ITextView mostRecentTextView;

    [ImportingConstructor]
    public MyPart(Lazy<IContentTypeRegistryService> contentTypeRegistryService)
    {
        this.contentTypeRegistryService = contentTypeRegistryService;
    }

    public void TextViewCreated(ITextView textView)
    {
        this.mostRecentTextView = textView;
    }
}

The state in the class has a fixed size (one text view, not a continuously growing collection), and it’s stored in a local variable, so at first glance, this code seems harmless.

The Problem

Image Picture1

  • ITextViewCreationListener is a MEF part, meaning it will not be garbage collected until Visual Studio is restarted, and any objects it directly references, including this text view, will leak.
  • The ITextView can be a very heavy object:
    • It has references to the ITextBuffer containing the underlying document, which can be very large, and is entirely stored in memory.
    • It has references to the undo stack for the text view, which can potentially store a long history of changes to the document while it was open.
    • It has references to the various UI elements, extensions, margins, taggers, etc. that were used to customize the presentation of the document.
    • Both ITextView and ITextBuffer have a ‘.Properties’ member, which is a dictionary of lots of arbitrary data that extensions have associated with that editor. The Roslyn C# language service, for example, stores a reference to the workspace in this property bag.

The implication is that a single leaked object can transitively leak much more than expected. With this example, closing your solution and opening another solution would consume the memory of having both solutions open simultaneously!

Best Practices for MEF parts

Since MEF parts live until Visual Studio is shutdown, it is important to be careful about how you use them to avoid memory leaks.

  • Avoid storing references to objects in MEF parts except other shared MEF parts (singleton services).
  • Avoid storing any state in MEF parts. Most Visual Studio IDE MEF parts follow a provider pattern: the MEF part is a stateless factory (e.g.: ITaggerProvider) for acquiring services and it in turn creates one or more stateful extensions (e.g.: ITagger).
  • Associate any shared state with objects that have a well-defined lifetime instead of with a MEF part. For example, .NET compiler (Roslyn) has a core ‘workspace’ concept, that is associated with an ITextView via the view’s ‘.Properties’ member. Many VS extensions follow this pattern, enabling their state to go out of scope and get collected when the owning text view is closed or the owning document is garbage collected.
  • In the rare case where you need to reference state in a MEF part, consider using a WeakReference, using relevant cleanup events to delete it when done with it, or enforcing a max size, if the stored data is a cache or collection of items.

C# Event Listener leaks

In addition to MEF, Visual Studio relies heavily on C# events to compose extensions into the editor. Under all the syntactic sugar, an event in C# is effectively just a collection of subscriber delegates that get invoked by the class implementing the event one by one when an event is ‘raised’. One implication of this is that every class implementing an event has a strong reference to any current subscribers, preventing those objects from being garbage collected.

[Export(typeof(ITextViewCreationListener))]
[Name("Colorization tagger")]
[ContentType("CSharp")]
[TagType(typeof(IClassificationTag))]
internal sealed class MyPart : IViewTaggerProvider
{
    private readonly Lazy<IEditorOptionsFactoryService> optionsFactoryService;

    [ImportingConstructor]
    public MyPart(Lazy<IEditorOptionsFactoryService> optionsFactoryService)
    {
        this.optionsFactoryService = optionsFactoryService;
    }

    public ITagger<T> CreateTagger<T>(ITextView textView, ITextBuffer buffer) where T : ITag
    {
        return new MyTagger(textView, this.optionsFactoryService.Value) as ITagger<T>;
    }
}

internal sealed class MyTagger : ITagger<IClassificationTag>
{
    private readonly ITextView textView;

    public MyTagger(
        ITextView textView,
        IEditorOptionsFactoryService optionsFactoryService)
    {
        this.textView = textView;
        optionsFactoryService.GlobalOptions.OptionsChanged += this.OnOptionChanged;
    }

    public event EventHandler<SnapshotSpanEventArgs> TagsChanged;

    public IEnumerable<ITagSpan<IClassificationTag>> GetTags(NormalizedSnapshotSpanCollection spans)
    {
        return Array.Empty<ITagSpan<IClassificationTag>>();
    }

    private void OnOptionChanged(object sender, EditorOptionChangedEventArgs e)
    {
    }
}

In the above example, we are once again doing something fairly innocuous: we’re creating a ‘classification tagger’, commonly used to provide code colorization, and subscribing to EditorOptions.OptionChanged, so we can update the colorization if some global option changes.

Subscribing each produced ITagger to OptionChanged, constructs a strong reference to this tagger. Since IEditorOptionsFactoryService.GlobalOptions is referenced by a MEF part (IEditorOptionsFactoryService), it is kept alive for the entirety of the VS session, and anything it references is too.

In this example, every time a document is opened and closed, we would leak another instance of MyTagger and anything it references. The tagger references the view, and is targeting C#, so we’d also leak the entire Roslyn workspace, and a unique copy of every C# file that user opened during their VS session!

Image Picture3

Best Practices for C# Event Listeners

Now that we’ve established the risks of memory leaks from C# event listeners, here are some best practices you can adopt in your extension.

  • Every event subscription should be unsubscribed at the appropriate time. If you see event handlers in your code that do not unsubscribe, look for opportunities, such as ITextView.Close, to unsubscribe them.
  • Many Editor extensibility points, including, ITaggers, automatically call Dispose() on any part that implements the optional IDisposable interface when that part is no longer needed. When implementing editor parts, consider implementing IDisposable.Dispose() to see if it’s called by the editor when the owning text view, text document, or IntelliSense is cleaned up. You can unsubscribe from event handlers in your implementation of IDisposable.Dispose().

Other Common Leaks

Other common sources of memory leaks are:

  • Static State: much like MEF part leaks, information stored in static fields and properties is gc-rooted by the garbage collector when the owning type is referenced for the first time and is not cleaned up until Visual Studio shutdown. Best practice is to:
    • Avoid static state unless there is no other alternative.
    • For static collections, especially caches, set a max size (e.g.: ’10 items’).
  • WPF Data Binding: behind XAML markup, WPF UI, used by Visual Studio extensions, employs C# event handlers, delegates, and even static dictionaries to implement Data Binding. Certain types of data binding can cause UI objects and any state it references to leak. For example, AddValueChanged(), as used below, adds a reference to the WPF control to a static dictionary, leaking it unless the listener is later unsubscribed.
private void OnLoaded(object sender, RoutedEventArgs e)
{
    var textDp = DepedencyPropertyDescriptor.FromProperty(TextBlock.TextProperty, typeof(TextBlock));
    textDp.AddValueChanged(this, this.OnTextChanged);

    this.RenderHighlighedSpans();
}

Diagnosing Leaks

To diagnose leaks in your Visual Studio extension you can use either of the following memory debugging tools.

  • Visual Studio .NET Memory Usage Snapshots tool: this built in Visual Studio functionality captures ‘memory snapshots’, before and after your extension scenario, that can then be diffed to identify memory leaks.
  • PerfView: this open-source Microsoft tool offers similar heap snapshotting and diffing capabilities under Memory > Take Heap Snapshot.

Image Picture4

The typical workflow is to open Visual Studio, run your scenario once and then close all associated tool windows/projects/documents and take a memory snapshot (the warmup iteration), then repeat the scenario again, take another snapshot, and diff the two. Commonly leaked types to look for are:

  • WpfTextView
  • TextBuffer
  • ProjectionBuffer

In this blog post we covered Visual Studio editor architecture, its history, and the various factors that contribute to unintentional memory leaks. Having read this post, you now have the tools to prevent reliability issues from memory leaks, and ensure your extension customers have the most reliable, performant experience possible.

1 comment

Discussion is closed. Login to edit/delete existing comments.

  • David N 0

    Great post! Really clear descriptions of the issues. I haven’t written any extensions yet but bookmarking this for future reference.

Feedback usabilla icon