WPF in Visual Studio 2010 – Part 2 : Performance tuning
This post, the second in a series of articles on Visual Studio 2010’s use of WPF, covers tips and techniques for optimizing performance of WPF applications, and also several areas where we needed to tune Visual Studio 2010 in order to squeeze the best out of WPF. The first post in the series covered the motivation for selecting WPF and some of the new WPF 4 features of which Visual Studio takes advantage.
Performance optimization begins with measurement and tuning of WPF in Visual Studio is no exception. To measure performance in Visual Studio, we used the following tools:
The Visual Studio Profiler. This feature is built into Visual Studio itself, so it’s great that it’s always handy . The profiler team has its own blog: http://blogs.msdn.com/profiler/
For a deeper analysis of events and timing at the operating system level, we use xperf from the Windows Performance Analysis Tools: http://msdn.microsoft.com/en-us/performance/cc825801.aspx (Download the latest Windows SDK and find the “Install Windows Performance Toolkit” link). Actually, if I had to choose one tool to do performance analysis, this would be it. Whether your performance problem is CPU, disk, memory or network (or a combination), there’s no hiding from this tool.
Also included in the Windows Performance Toolkit is the WPF Performance suite: http://msdn.microsoft.com/en-us/library/aa969767.aspx This utility includes a “Visual Profiler” giving you a detailed insight into the behavior of your WPF application. More information here: http://windowsclient.net/wpf/perf/wpf-perf-tool.aspx.
For general WPF performance, please start with these MSDN topics: http://msdn.microsoft.com/en-us/library/aa970683.aspx
The following tips are based on real WPF performance problems that we encountered during Visual Studio 2010 development.
1. Use Effects wisely
The Perforator tool in the WPF Performance suite contains a “dirty region” overlay which helped us track down a performance problem with using drop-shadows in Visual Studio.
Adding a drop-shadow to an element causes the entire element to be ‘dirtied’ even when only a small part of the element is updated. For example, adding a drop-shadow to a toolbar causes the entire toolbar to be re-rendered whenever one of its buttons changes. At one point during development, we tried placing a drop-shadow on the document well container in Visual Studio without realizing the performance implications. From then on, any time you typed in the text editor, or moved the cursor around, the entire document well would be redrawn. Even blinking the caret caused the entire region to be ‘dirtied’. Of course, once we discovered this, we removed the drop-shadow effect and performance was back to normal. The conclusion is that bitmap effects such as DropShadowEffect, should be used very sparingly and only on simple elements. Often you can create the drop shadow effect a different way – for example, by surrounding the perimeter of the element with shapes filled with gradient brushes, varying the brush opacity to create a soft shadow effect.
2. Reduce IRTs
The Perforator tool in the WPF Performance suite also showed a spike in the “Hardware IRT” graph every time the toolbars were refreshed. IRTs (Intermediate Render Targets) are used during composition of the final image in WPF. While not particularly expensive in themselves, each one is extra memory allocated on the GPU and contributes to the overall rendering time. After a little experimentation, it turned out that our algorithm for creating grayed out icons for disabled toolbar buttons was the culprit. We were using a FormatConvertedBitmap to convert to grayscale. We replaced that algorithm with an equivalent hand-written routine for converting color images to grayscale that was optimized for the small 16×16 icons we use on the toolbar, thereby completely eliminating these IRTs.
3. Limit Visual Tree complexity
One area we didn’t explore very thoroughly was reduction of the depth of the Visual Tree. Our style templates, particularly for toolbar controls are quite complex. The visualization of a control depends on properties in the data model such as visible/invisible, enabled/disabled, iconic/textual and these are all bound to behaviors (property setters) in the templates. Even a simple-looking button with an icon on it contains all the necessary borders, grids and child elements to support the drawing of toolbar controls in the most generic way possible. It makes for a very flexible system, but results in an overly complex Visual Tree. We could replace this complexity with a single, custom “ToolbarButton” element, moving most of the styles and behaviors out of declarative markup (XAML) and into code. It’s hard to predict exactly how much that would improve toolbar construction time and memory usage.
4. Optimize style templates
One experiment we did try, however, was to replace all the toolbar control styles with a simple colored rectangle – the idea being to see if we could get a best case measurement for replacing declarative XAML styles with imperative C# code. This experiment alone revealed something interesting about style application in WPF. The full style template is parsed and evaluated even if the element is invisible. In our style templates, visibility is determined via a property setter triggered by the IsVisible property in the control’s data model. Seems straightforward enough but, as far as WPF is concerned, the Visibility property is no different from any other property. By analyzing xperf traces of toolbar initialization, we discovered that applying these style templates to controls which were ultimately invisible was costing us a significant penalty – all of which was wasted work. The fix was to add a little bit of logic to our toolbar controls – moving just the Visibility property setter and its trigger into code. When the style is selected, we look ahead at the “IsVisible” property and, if it is false, we force the Visibility to Collapsed and set up a listener on the data model’s IsVisible property. When the IsVisible property changes, we remove the listener and apply the original style.
5. Take care making bulk changes to Application.ResourceDictionary
Visual Studio sets up a ResourceDictionary at the Application level for use by any component in the application. The resources contain colors, brushes and styles for the color theme used throughout the UI. When the OS theme changes, for example when entering or leaving High Contrast mode, these resources are updated with new colors. Our first approach was to replace the resources one by one, but this was incredibly slow because each change caused property change notifications to ripple throughout the entire visual tree – nearly every visual was listening to resource changes and reapplying its style. So, the solution was to construct a brand new, standalone ResourceDictionary with the new colors and, when complete, switch out the existing resources for this new set in one operation.
6. MeasureOverride’s availableSize uses PositiveInfinity to indicate “don’t care”
If you’re implementing a control with custom layout by overriding MeasureOverride and ArrangeOverride, be aware that the parent control may pass a value of Double.PositiveInfinity for either width or height (or both) to indicate it doesn’t care. This may be your cue to use an optimized code path in your measurement logic. If your implementation needs to call Measure on its children, then continue the favor by passing PositiveInfinity down to the children too. Bonus tip: WPF TextBlocks are optimized so that if you call Measure more than once with the same size, then it will return the same size without doing complicated measurement.
7. Fix BindingExpression path errors
If, when debugging your WPF application, you see errors in the output window like:
System.Windows.Data Error: 40 : BindingExpression path error: ‘AcquireFocus’ property not found on ‘object’ ”DataSource’ (HashCode=61327894)’. BindingExpression:Path=AcquireFocus; DataItem=’DataSource’ (HashCode=61327894); target element is ‘VsButton’ (Name=”); target property is ‘AcquireFocus’ (type ‘Boolean’)
then, as well as a broken data-binding, you may have a performance problem. WPF tries several different ways to resolve path errors, including searching for attached properties and this is quite expensive. Eliminate all such warnings, and you should be good. Visual Studio 2010 has new options for debugging WPF databindings.
8. Optimize for Remote Desktop scenarios : Reduce visual complexity
Over a remote desktop connection, all WPF content is rendered as a bitmap. This is in contrast to GDI rendering, where primitives such as rectangles and text are sent over the wire for reconstruction on the client. The way to improve remote desktop performance is to minimize the number of bytes sent “over the wire”. The remote desktop protocol has some compression so, this means optimizing the WPF scene for compression. A simple scene compresses far better than a complex one. For example, solid colors compress better than gradients or textured brushes. In remote desktop scenarios, Visual Studio 2010 automatically reduces visual complexity by turning off animations and shadows, and by using solid color backgrounds. Also check out Jossef Goldberg’s Blog for some additional tips and background.
9. Optimize for Remote Desktop scenarios : Use scrolling hint
Also on the topic of Remote Desktop, one area where we needed additional support from WPF was for scrolling in the text editor. As I mentioned above, when in a remote session, all WPF content is transmitted as a bitmap. When the text editor scrolls by a line, that means that the entire contents of the editor region needs to be retransmitted as a bitmap. This, of course can be expensive – the larger the area of the text view, the larger the bitmap and the slower it will be. Fortunately, WPF 4.0 now knows how to issue a “ScrollWindow” command to the remote desktop session which drastically reduces the amount of information transferred across the wire. Only the scroll operation itself (very short) and the newly-exposed line need to be transmitted. To take advantage of this new operation, you need to use the property VisualScrollableAreaClip. There are some restrictions on where this can be used, so read the documentation carefully.
10. Optimize for virtual machines and low-end hardware
As well as remote desktop, Visual Studio also reduces visual complexity when it detects a virtual machine or a low-end graphics device. WPF handles the rendering tier detection and we augment it with some additional heuristics to detect virtual environments. If necessary, we automatically reduce visual complexity just as if we were in a remote session. We can also force software rendering for the entire process with the RenderOptions.ProcessRenderMode property which is new in WPF 4.0. In testing, we found some netbooks which, even though they reported Tier 2 capable GPUs, were slower in hardware rendering than in software. For those situations, we don’t automatically detect, but the user can always override these settings via a page in Tools/Options…
Other parts of Visual Studio, including extensions can also react to this setting by either checking the Visual Effects setting directly via the new shell property “VSSPROPID_VisualEffectsAllowed” or, if using WPF binding to the property “EnvironmentRenderCapabilities”.
It’s easy to overlook memory usage while trying to optimize for elapsed time.
For memory analysis, particularly to identify memory leaks, we start with VMMap.
This identifies what “kind” of memory is being leaked: Images, managed heap, native heap, memory mapped files or private bytes.
For native heap leaks we use UMDH. For managed leaks, we use a combination of CLRProfiler and the SOS extension for WinDbg. The techniques have been covered by a number of people, including Rico Mariani.
Jossef Goldberg on the WPF team has this excellent write-up on tracking down memory leaks in WPF which is just as relevant today as it was two years ago: http://blogs.msdn.com/jgoldb/archive/2008/02/04/finding-memory-leaks-in-wpf-based-applications.aspx
There’s plenty of information about general performance tuning techniques on the web, but I hope this article has covered some topics you won’t find elsewhere. I left out a couple of items that I thought were a little too specific to Visual Studio and wouldn’t be very relevant in other applications. Besides, 10 was a nice round number for the tips. If there’s enough interest from the comment stream, I may revisit performance in a later part of this WPF in Visual Studio series.
As always, please leave your comments and questions. I’ll try to answer them either directly in the comments, or in future posts.
Next time: Part 3. Focus and Activation.
Previous articles in the WPF in Visual Studio 2010 series:
Paul Harrington – Principal Developer, Visual Studio Platform Team
Biography: Paul has worked on every version of Visual Studio .Net to date. Prior to joining the Visual Studio team in 2000, Paul spent six years working on mapping and trip planning software for what is today known as Bing Maps. For Visual Studio 2010, Paul designed and helped write the code that enabled the Visual Studio Shell team to move from a native, Windows 32-based implementation to a modern, fully managed presentation layer based on the Windows Presentation Foundation (WPF). Paul holds a master’s degree from the University of Cambridge, England and lives with his wife and two cats in Seattle, Washington.