Dotneteers.net
All for .net, .net for all!

DevTools Ecosystem Summit: Best Practices for Extending the IDE with a Focus on Performance, Part 2

<Part 1

image

After treating and demonstrating why is it beneficial to turn to WPF, let’s see how extensions can target better performance. I’d be able to talk about performance topics so much that I could take over all the session slots left for today or even for tomorrow… Instead of treating each of them, I collected a bunch of tips that gives you ideas what you can do with performance issues or how you can avoid them.

The key of providing a great performance can be described simply: Consume only the resources you really, really, really need. If possible, release unused resources and claim them when you need them again.

This approach starts with loading your package only when that is required and not before. By default the VS shell loads packages on-demand when the first command, UI, service or other package owned objects are about to be used. However, you can change this behavior by either loading explicitly your package with the IVsShell.LoadPackage method, or adding the ProvideAutoLoad attribute to your packages. This attribute provides that your package is automatically loaded when the Shell enters in a certain context, for example a solution is loaded, the build process started, and so on.

I suggest totally avoiding the LoadPackage approach.

You must use the ProvideAutoLoad attribute also wisely. If you use the so-called NoSolution context it causes the shell automatically load your package at VS startup time. This context is heavily overloaded and startup time is everyone’s concern. However, this is also broken in a few situations: for example when you run devenv.exe from a command line with a file or solution, your package does not get loaded. Be more selective with using this attribute.

If you are implementing tool windows, you can set the ProvideToolWindowVisibility attribute to display the tool window when the shell enters in a specific context. This time your package is loaded. When you create a tool window, the instance behind that is never destructed, even if you close the tool window. You can catch the event when tool windows are about to be hidden or shown and this can be used to release and then reallocate resources.

If your extension takes time to make some kind of processing be sure to write it so that the UI remain responsive. You can use some kind of background threading for provide this responsiveness or even you can leverage on using the CPU and other resources while Visual Studio is idle.

image

I think the concepts I have mentioned tell more if we try them in the practice. I have created a VSPackage to demonstrate how to use AutoProvideLoad attribute, what idle processing means and I also show how we can release and reallocate resources held by tool windows.

My package implements a tool window that parses all C# files in the active project and displays some useful statistics about them, for example, how many methods and statements they use. This package is far away from being functionally completed, but gives me good opportunity to demonstrate performance things. I also use this sample to show a few concepts and how it evolved to reach this state.

First I build this package and show how it will look like after applying all enhancements. When I open a solution, the C# Statistics tool window automatically appears and starts parsing C# files:

image

I used the ProvideToolWindowVisibility attribute and set it to the SolutionExist context to show the tool window automatically when a solution is opened:

  1. //[ProvideAutoLoad(GuidList.UICONTEXT_SolutionExists)]
  2. [ProvideToolWindowVisibility(typeof(CSharpStatsToolWindow),
  3.   GuidList.UICONTEXT_SolutionExists)]

This attribute takes two parameters. The first represents the type of the tool window we set the visibility options for, the second is the GUID of the UI context to bind the visibility of the window to. You can use this attribute for the same window type to set multiple UI contexts.

Should I used the ProvideAutoLoad attribute, the owner package would be loaded but the tool window would not be displayed.

Let’s debug the package and show how it works. I put a breakpoint into the Initialize method that is to be executed right after the package has been load by the shell, and start debugging it.

You see that Visual Studio has finished its startup, but the breakpoint have not yet reached. Now I open the C# solution in the Experimental Instance, and as you see, now the breakpoint has been reached signing that our package has been loaded.

image

Now, let’s change the context to FullScreen.

  1. [ProvideToolWindowVisibility(typeof(CSharpStatsToolWindow),
  2.   GuidList.UICONTEXT_FullScreenMode)]

Running the application this time will show the tool window when we use the View|Full screen. As the full screen mode is left, the tool window is hidden again. This is exactly what ProvideToolWindowVisibility does. When entering into the context, the tool window is (created and) displayed, when leaving the context, the window is hidden.

Let’s set it back the attribute’s value to SolutionExists. Now I show you a few internals of the package and then demonstrate a few steps I have reached during application development. The implementation follows the Model-View-Controller pattern.

The C# parsing related functions are provided by the ICSharpStats service (Model interface), here you can see, how simple the service interface is:

  1. [Guid("99C37858-E1A7-468B-AC62-FBE842E25387")]
  2. public interface ICSharpStats
  3. {
  4.   // ------------------------------------------------------------------------
  5.   /// <summary>
  6.   /// Collects all .cs files from the active project
  7.   /// </summary>
  8.   // ------------------------------------------------------------------------
  9.   IEnumerable<string> GetCSharpFiles();
  10.  
  11.   // ------------------------------------------------------------------------
  12.   /// <summary>
  13.   /// Parses the specified file and collects C# info
  14.   /// </summary>
  15.   // ------------------------------------------------------------------------
  16.   CSharpFileInfo ProcessFile(string fileName);
  17. }

This service is implemented by the CSharpService class that plays the role of Model. It uses my own C# parser to collect information from C# files.

The UI of the application is provided by a WPF user control that really implements the View role in the MVC pattern, and does not handle any controller functions. The XAML definition of the UI is simple:

  1. <UserControl x:Class="DiveDeeper.CSharpStats.CSharpStatsControl"
  2.              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  5.              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  6.              mc:Ignorable="d"
  7.              d:DesignHeight="300" d:DesignWidth="300"
  8.              Name="MyToolWindow"
  9.              Background="{DynamicResource VsBrush.ToolWindowBackground}">
  10.   <UserControl.Resources>
  11.     <Style x:Key="ColumnHeaderStyle" TargetType="DataGridColumnHeader">
  12.       <Setter Property="FontWeight" Value="SemiBold" />
  13.     </Style>
  14.     <Style x:Key="NumericCellStyle">
  15.       <Setter Property="TextBlock.TextAlignment" Value="Right" />
  16.     </Style>
  17.   </UserControl.Resources>
  18.   <DockPanel>
  19.     <StackPanel Margin="4" DockPanel.Dock="Bottom" Orientation="Horizontal">
  20.       <TextBlock Text="Remaining items: " />
  21.       <TextBlock x:Name="RemainingLabel">0</TextBlock>
  22.     </StackPanel>
  23.     <DataGrid Name="FileInfoGrid"
  24.               AutoGenerateColumns="False" GridLinesVisibility="All"
  25.               HeadersVisibility="All" VerticalAlignment="Top"
  26.               VerticalContentAlignment="Top" VerticalScrollBarVisibility="Auto"
  27.               ColumnHeaderStyle="{StaticResource ColumnHeaderStyle}"
  28.               BorderBrush="CadetBlue"
  29.               HorizontalGridLinesBrush="Gainsboro" VerticalGridLinesBrush="Gainsboro"
  30.               AlternatingRowBackground="Azure" AreRowDetailsFrozen="False" FontSize="14">
  31.       <DataGrid.Columns>
  32.         <DataGridTextColumn Header="File" Binding="{Binding FileName}"
  33.                             Width="Auto" IsReadOnly="True" FontWeight="SemiBold"
  34.                             Foreground="Maroon" />
  35.         <DataGridTextColumn Header="Namespaces" Binding="{Binding Namespaces}"
  36.                             Width="Auto" IsReadOnly="True"
  37.                             ElementStyle="{StaticResource NumericCellStyle}"/>
  38.         <DataGridTextColumn Header="Types" Binding="{Binding Types}"
  39.                             Width="Auto" IsReadOnly="True"
  40.                             ElementStyle="{StaticResource NumericCellStyle}" />
  41.         <DataGridTextColumn Header="Nested Types" Binding="{Binding NestedTypes}"
  42.                             Width="Auto" IsReadOnly="True"
  43.                             ElementStyle="{StaticResource NumericCellStyle}"/>
  44.         <DataGridTextColumn Header="Members" Binding="{Binding Members}"
  45.                             Width="Auto" IsReadOnly="True"
  46.                             ElementStyle="{StaticResource NumericCellStyle}"/>
  47.         <DataGridTextColumn Header="Methods" Binding="{Binding Methods}"
  48.                             Width="Auto" IsReadOnly="True"
  49.                             ElementStyle="{StaticResource NumericCellStyle}"/>
  50.         <DataGridTextColumn Header="Statements" Binding="{Binding Statements}"
  51.                             Width="Auto" IsReadOnly="True"
  52.                             ElementStyle="{StaticResource NumericCellStyle}"/>
  53.         <DataGridTextColumn Header="Path" Binding="{Binding FilePath}"
  54.                             Width="*" IsReadOnly="True" />
  55.       </DataGrid.Columns>
  56.     </DataGrid>
  57.   </DockPanel>
  58. </UserControl>

The code behind of the UI illustrates the this user control is really used as a view, free from Controller functionality:

  1. public partial class CSharpStatsControl : UserControl
  2. {
  3.   // ------------------------------------------------------------------------
  4.   /// <summary>
  5.   /// Initializes the control
  6.   /// </summary>
  7.   // ------------------------------------------------------------------------
  8.   public CSharpStatsControl()
  9.   {
  10.     InitializeComponent();
  11.   }
  12.  
  13.   // ------------------------------------------------------------------------
  14.   /// <summary>
  15.   /// Sets the source of the data grid.
  16.   /// </summary>
  17.   // ------------------------------------------------------------------------
  18.   public void SetItemsSource(IEnumerable<CSharpFileInfo> source)
  19.   {
  20.     FileInfoGrid.ItemsSource = source;
  21.   }
  22.  
  23.   // ------------------------------------------------------------------------
  24.   /// <summary>
  25.   /// Sets the value of remaining items
  26.   /// </summary>
  27.   // ------------------------------------------------------------------------
  28.   public void SetRemainingCount(int remainingCount)
  29.   {
  30.     RemainingLabel.Text = remainingCount.ToString();
  31.   }
  32. }

The lion’s share of the work is done by the CSharpStatsToolWindow class in the role of Controller.

I have built in a few flags into this class to help me show earlier states of the application. So let me show you what the first version of my package was from the user responsiveness point of view.

  1. // --- Constants and fields used for demonstration
  2. const int Slowing = 1;
  3. const int SleepTime = 20;
  4. private int _IdleCounter;
  5. private bool UseIdleProcessing = true;
  6. private bool UseResourceSavings = true;
  7. private bool UseAutoRefresh = true;

[Set the UseIdleProcessing flag to false and run the package. Show that opening a project stops UI responding about for 5 seconds.]

You see, it is not a “polite” behavior. The root issue is that parsing more than 400 C# files takes time. If it happens on the main UI thread, then it blocks any other activities. How this could be improved? Well we can use idle processing, make the work done while Visual Studio is idle.

I have made the following architectural changes in the controller:

  • Instead of processing the C# files, I put them into a queue. When the Shell is idle, it takes one file from the queue, parses it and then adds it to the to the collection used as an item source for the grid.
  • The item source of the grid is implemented as an ObservableCollection<>. When a file is processed and added to this collection, the UI is refreshed without any further coding.

  1. // --- List containing the processed C# items
  2. private ObservableCollection<CSharpFileInfo> _FileInfoCollection;
  3.  
  4. // --- Queue holding items for idle processing
  5. private Queue<string> _FilesToProcess;

Preparing our tool window for idle processing requires registering it with OleComponentManager in the OnCreated method:

  1. // --- Set up Idle processing
  2. var oleComponentManager = base.GetService(typeof(SOleComponentManager))
  3.   as IOleComponentManager;
  4. if (oleComponentManager != null)
  5. {
  6.   uint pwdId;
  7.   OLECRINFO[] crinfo = new OLECRINFO[1];
  8.   crinfo[0].cbSize = (uint)Marshal.SizeOf(typeof(OLECRINFO));
  9.   crinfo[0].grfcrf = (uint)_OLECRF.olecrfNeedIdleTime |
  10.                                 (uint)_OLECRF.olecrfNeedPeriodicIdleTime;
  11.   crinfo[0].grfcadvf = (uint)_OLECADVF.olecadvfModal |
  12.                                 (uint)_OLECADVF.olecadvfRedrawOff |
  13.                                 (uint)_OLECADVF.olecadvfWarningsOff;
  14.   crinfo[0].uIdleTimeInterval = 1000;
  15.   oleComponentManager.FRegisterComponent(this, crinfo, out pwdId);
  16. }

The tool window also should implement the IOleComponent interface with its 11 members.

  1. public class CSharpStatsToolWindow : ToolWindowPane,
  2.   IVsWindowFrameNotify3, IOleComponent

The most important is the FDoIdle method that runs where the shell is idle:

  1. int IOleComponent.FDoIdle(uint grfidlef)
  2. {
  3.   ProcessOnIdle();
  4.   return VSConstants.S_OK;
  5. }

The work is done by the ProcessOnIdle method that contains “artificial slowing” to support demonstration:

  1. public void ProcessOnIdle()
  2. {
  3.   if (_IdleCounter++ % Slowing != 0) return;
  4.   string nextFile;
  5.   lock (_FilesToProcess)
  6.   {
  7.     if (_FilesToProcess.Count == 0) return;
  8.     nextFile = _FilesToProcess.Dequeue();
  9.   }
  10.   _FileInfoCollection.Add(_CSharpService.ProcessFile(nextFile));
  11. }

Now I show you that this code provides UI responsiveness. For the sake of this demo, I slow idle processing down so that only every sixth FDoIddle call will do effective work.

[Set the Slowing value to 6 and set the UseIdleProcessing flag to true. Open the C# project used to parse before. Show that while the files are parsed, the UI stays responsible, refreshes the list of files parsed. Demonstrate that files can be opened and edited and also the C# statistics tool window can be closed and reopened.]

During this demo you can follow how more and more C# files are parsed during idle processing:

image

image

Some remarks: when you close the tool window, idle processing goes on in the background, now it is intentional just because of demo reasons. However, in a real production environment you should implement idle processing so that hiding your tool window suspends the idle processing, showing the tool window enables it again.

Now, let me show how to release and reclaim resources when the tool window is hidden and shown again. I have extended the class providing information about a C# file so that it will allocate one megabyte of memory to put some fake information into it. I will show you how much memory it takes. Here is the code to allocate (and release) memory:

  1. public class CSharpFileInfo
  2. {
  3.   // --- Members used for demonstration purposes
  4.   const int MaxLength = 60;
  5.   const int BufferSize = 1000000;
  6.   byte[] _WasteMyMemory;
  7.  
  8.   // ------------------------------------------------------------------------
  9.   /// <summary>
  10.   /// Initializes an instance according to the specified file name.
  11.   /// </summary>
  12.   // ------------------------------------------------------------------------
  13.   public CSharpFileInfo(string fileName)
  14.   {
  15.     FullName = fileName;
  16.     FileName = Path.GetFileName(fileName);
  17.     FilePath = Path.GetDirectoryName(fileName);
  18.     AllocateResources();
  19.   }
  20.  
  21.   // --- Public properties for retrieving C# information
  22.   public string FileName { get; private set; }
  23.   public string FilePath { get; private set; }
  24.   public string FullName { get; private set; }
  25.   public int Namespaces { get; set; }
  26.   public int Types { get; set; }
  27.   public int NestedTypes { get; set; }
  28.   public int Members { get; set; }
  29.   public int Methods { get; set; }
  30.   public int Statements { get; set; }
  31.  
  32.   #region Resource allocation methods
  33.  
  34.   // ------------------------------------------------------------------------
  35.   /// <summary>
  36.   /// Allocates fake resources for the class
  37.   /// </summary>
  38.   // ------------------------------------------------------------------------
  39.   public void AllocateResources()
  40.   {
  41.     if (_WasteMyMemory != null) return;
  42.     try
  43.     {
  44.       _WasteMyMemory = new byte[BufferSize];
  45.       var reader = new BinaryReader(File.OpenRead(FileName));
  46.       var bytesRead = reader.ReadBytes(BufferSize);
  47.       bytesRead.CopyTo(_WasteMyMemory, 0);
  48.     }
  49.     catch (SystemException)
  50.     {
  51.       // --- This exception is intentionaly caught
  52.     }
  53.   }
  54.  
  55.   // ------------------------------------------------------------------------
  56.   /// <summary>
  57.   /// Releases fake resources
  58.   /// </summary>
  59.   // ------------------------------------------------------------------------
  60.   public void ReleaseResources()
  61.   {
  62.     _WasteMyMemory = null;
  63.   }
  64.  
  65.   #endregion
  66. }

[Set Slowing to 2, the UseResourceSavings flag to false, and run the package. Display the Process Info window. Open the C# project and refresh the C# statistics window. Show that parsing the files in the project consumed something like 450 Mbytes of memory]

Before parsing started:

image

After parsing finished:

image

When the user explicitly closes the tool window the memory is not reclaimed. Closing the window is an explicit sign that the user does not intend to deal with the C# Statistics anymore, so why not to release the memory held by this window? I’ve implemented this scenario.

First, I prepared the tool window to catch frame events including hiding and showing the frame. To catch these events we must subscribe to them by passing an IVsWindowFrameNotify3 instance -- in this case this interface is implemented by the tool window itself. The interface implements 5 methods, the one we are interested in is OnShow:

  1. // ------------------------------------------------------------------------
  2. /// <summary>
  3. /// Responds to the event when the tool window is hidden or shown.
  4. /// </summary>
  5. // ------------------------------------------------------------------------
  6. public int OnShow(int fShow)
  7. {
  8.   if (UseResourceSavings)
  9.   {
  10.     if (fShow == 0)
  11.     {
  12.       // --- Tool window is about to hide
  13.       foreach (var item in _FileInfoCollection)
  14.         item.ReleaseResources();
  15.       GC.Collect();
  16.     }
  17.     else
  18.     {
  19.       // --- Tool window is about to show
  20.       foreach (var item in _FileInfoCollection)
  21.         item.AllocateResources();
  22.     }
  23.   }
  24.   return VSConstants.S_OK;
  25. }

As you see the code above, when the window is about to hide, we release all allocated resources. In contrast when the window is to be shown again, we reclaim the resources again.

Now, let’s see the results in practice:

[Set the UseResourceSavings flag to true, the Slowing value to 1. Run the package, and open the Process Info Tool window. Load and analyze the C# project. Show the size of memory consumed after analysis, then close the tool window. Show the memory reservation again.]

This is what you can see before closing the tool window:

image

When you close the tool window, you can see, resources have been released:

image

When the tool window is displayed again the resources are reclaimed:

image

I hope you could see, with a few good techniques you can make your extensions’ UI more responsive and frugal with resources.

Part 3>


Posted Oct 20 2009, 02:25 PM by inovak

Comments

DiveDeeper's blog wrote DevTools Ecosystem Summit: Best Practices for Extending the IDE with a Focus on Performance, Part 3
on Thu, Oct 22 2009 17:18

<Part 2 In the last section of my presentation I would like to show you common mistakes or pitfalls

DiveDeeper's blog wrote DevTools Ecosystem Summit: Best Practices for Extending the IDE with a Focus on Performance, Part 3
on Thu, Oct 22 2009 17:50

<Part 2 In the last section of my presentation I would like to show you common mistakes or pitfalls

Add a Comment

(required)  
(optional)
(required)  
Remember Me?