Quantcast
Viewing all articles
Browse latest Browse all 15

Improving WPF Rendering Performance

I have posted quite a few times previously covering WPF, its quirks and workarounds as well as ways to improve its overall perceived performance to end users. In this article I will summarise them all, add in lots more tips and tricks and tell you some big gotchas.

Versioning

WPF and Windows have come a long way since the initial ‘Avalon’ concept was internally thrown around at Microsoft. Making sure you are targeting the latest .Net version can help a great deal, not just in terms of optimising the WPF components, but being able to more efficiently code your app.

The rendering pipeline under the hood of WPF is all DirectX where possible, with fewer and fewer things falling back to a software rendering layer. Because of this the performance of WPF is tied to the rendering pipeline of the installed version of Windows.

If you run a WPF application in XP, Vista and then Windows 7 you will notice a steady performance boost through each version. Vista started the concept of a unified Desktop Windowing Manager (DWM) however it was only finalised in Windows 7. Vista still had some of the rendering pipeline being done via the system and not GPU memory and stored redundant graphical surfaces slowing down overall performance. XP was just composited using an older version of DirectX.

Do: Make sure you are writing your application against the current .Net and WPF version.

Do: Test your app across different versions of Windows to judge performance

.Net Source Code

This is one of the unsung amazing Microsoft actions that not many people know about which I give them credit for.

Microsoft releases the vast majority of the source code to the C# Base Class Library (BCL) and some of the components of .Net sitting on top. If you have a bug that just points of mscorlib, you can download the source and debug symbols and step inside the .Net source.

You can pick up the latest source from here:

.Net Source Code

You will want the NET source code for the current version of installed .Net (4.5) near the bottom of the download list.

Why is this important for WPF, well Microsoft kindly includes the source code for a vast majority of the WPF components, controls and namespaces. So recently when I had a problem with MediaPlayer I just looked at the source code:

\RefSrc\Source\.NET 4.5\4.5.50709.0\net\wpf\src\Core\CSharp\System\Windows\Media\MediaPlayer.cs\550320

Do: Just browsing through the code is a great way to learn how to layout source, build controls and to understand how they work.

Understand the WPF thread model

When you build most WinForms applications, everything is run in a single thread. Which becomes apparent if you perform a blocking operation; the UI becomes frozen.

WPF split the threading model in two, where there is an animation clocking thread and the UI thread.

The animation thread is clocked to poll at around 60FPS where possible, and triggers animation clock events at these intervals which call into the UI rendering thread, where the majority of user code lives.

Doing it this way means the clocks and animations can live in a separate low priority thread and not block where possible.

The downside to this is there is still the problem where user code in the UI thread can delay render calls by the animation thread; thus leading to the common performance symptom of stuttering or slow animations.

Don’t: Put long running code in UI thread.

Do: Any CPU bound operation should be pushed out to a BackgroundWorker, ThreadPool operation or a separate application thread.

WPF Animation Framerate

The WPF animation thread clock has been designed by default to run at 60FPS, this means that the render loop can run hot while nothing much is on screen by calls to invalidate if your components aren’t designed correctly. If you are porting from Flash especially (where framerates are usually set at 30 anyway) it may be a good idea to adjust this value. The improvement is best seen during heavy animation sequences where the animation thread only has to invoke onto the UI thread half as much.

Timeline.DesiredFrameRateProperty.OverrideMetadata(
    typeof(Timeline),
    new FrameworkPropertyMetadata { DefaultValue = 30 }
);

This can in extreme cases nearly half the CPU usage. Try adjusting the DefaultValue to even lower values for idle screens or when no animations are occurring.

Do: Lower the default animation frame rate if possible.

Changes to the animation framerate do not even have to be a global operation, as DesiredFrameRateProperty is a dependency property, they can be applied to animation in XAML and in the codebehind directly.

Changing the animation framerate on a signal animation will still mean the global animation thread clocks faster, but reduces the number of invalidate calls to the render target; thus the control needs to be rendered and composited fewer times.

<DoubleAnimation Storyboard.TargetProperty="Opacity" Duration="0:0:0.5" 
                 From="1.0" To="0.5" Timeline.DesiredFrameRate="30" />

Do: Check all animations to see if they can have their frame rate individually reduced.

Render Options

WPF has codec support for a large array of image formats. When loading in images into an ImageBrush and creating temporary surfaces during animations WPF can scale and cache resources, when this happens you will be able to make trade-offs on how this occurs and the quality of the scaling mode.

Do: Make sure you only use images of the final size and DPI of your application. Loading in larger sizes means a higher memory footprint and additional calls to scaling operations.

The default scaling mode is Linear, which is faster than HighQuality mode, but produces lower quality output. The full list of render modes can be found on MSDN.

Do: If the animation sequence on an Imagebrush occurs fast, it may be advisable to lower the scaling image quality to increase performance.

Below is a sample of code that alters the RenderOptions of the brush _PictureBrush.

RenderOptions.SetCachingHint(_PictureBrush, CachingHint.Cache);
RenderOptions.SetBitmapScalingMode(_PictureBrush, BitmapScalingMode.LowQuality);

SetCachingHint is another way of telling WPF to cache the output of the visual. By default, WPF does not cache the rendered contents of DrawingBrush and VisualBrush objects because they may change. If the final state of these brushes is a static image, or the composition phase is CPU bound, make sure you enable this.

Do: Apply the SetCachingHint property for Brushes that have static content when produced in the code behind to reduce invalidation calls.

Bitmap Caching

An instant hot path is where complex objects have to be rendered repeatedly to the screen, what makes it worse from a design perspective is if these never change and are static. The CLR and compiler can notice this to varying degrees and can perform optimisations, but if you know when something won’t change for a long time; a button, or control, it might be best to give the CLR the heads up.

Bitmap caching works by rendering the control once, and then storing the finalised bitmap version. Future renders will use this cached version rather than needing to redraw the entire control again, this can lead to substantial improvements.

Caching has problems with the following:

  • Animations are happening within the control
  • Video
  • Interaction with native controls
  • Transformations (can cause image corrupts / degradation)

Caching options of a control can be accessed via the CacheMode dependency property of a UIElement, for example the following is how to set the CacheMode of a Canvas.

<Canvas.CacheMode>
    <BitmapCache EnableClearType="True" RenderAtScale="1.0" />
</Canvas.CacheMode>

The EnableClearType attribute specifies whether text in the cached object should still have ClearType font smoothing applied. If you are using ClearType on your app as a whole, have small text in the control and expect to be scaling then I would suggest you enable it.

RenderAtScale is important if you are scaling the end control. As the BitmapCache creates a pixel based representation of the control – no longer vector based, the attribute specifies how big the generated source bitmap should be. The default is 1.0, making it smaller means a smaller footprint, faster animations but blurring may become noticeable. Having the value higher than one means less detail will be lost during scaling transforms however the memory footprint will be larger and there will be a small (but growing as the attribute value rises) performance impact – although most likely lower than re-rendering the control from scratch.

Do: If using the RenderAtScale attribute, try different values to see the best trade off of quality vs speed.

Hide what you don’t need to render
When the render passes through the Visual Tree, it will update and render what it needs too, sometimes it will render controls regardless of if it is visible or not. To stop this from happening and thus reduce the path through the render phase you should hide or collapse the visibility of UIElements you do not require.
You may sometimes fade out UIElements using an Opacity brush to 0, effectively hiding it by making it transparent, however it is still part of the render tree.
Set the Visibility property to Hidden. Setting it to Collapse will do the same, but removes it completely from the visual tree rather than just not calling the render phase. Setting it to Collapse causes a small performance hit when re-showing it.

As Visibility is a Dependency Property, you can include this into your animations so their visibility is hidden when the animation finishes. You may even want to stop them being hit testable at the same time.

<Storyboard x:Key="Exit">
    <DoubleAnimation 
	Storyboard.TargetName="ControlToFadeOut"
	Storyboard.TargetProperty="Opacity"
	Duration="0:0:0.25"
	From="1" To="0" />
    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="ControlToFadeOut" Storyboard.TargetProperty="Visibility" Duration="0:0:0.25">
        <DiscreteObjectKeyFrame KeyTime="0:0:0.25">
            <DiscreteObjectKeyFrame.Value>
                <Visibility>Hidden</Visibility>
            </DiscreteObjectKeyFrame.Value>
        </DiscreteObjectKeyFrame>
    </ObjectAnimationUsingKeyFrames>
    <BooleanAnimationUsingKeyFrames Storyboard.TargetName="ControlToFadeOut" Storyboard.TargetProperty="IsHitTestVisible" Duration="0:0:0.25">
        <DiscreteBooleanKeyFrame KeyTime="0:0:0.25" Value="False" />
    </BooleanAnimationUsingKeyFrames>
</Storyboard>

Don’t: Hooking to the event if you need to in C# means additional overheads and some untidy code. Overall it may be actually less code, but by doing it in XAML means the majority is performed in the animation and not UI thread.

Reducing Image Footprint
If you are loading in large images, but do not need to view or process them at their original size. You should try and create a ‘thumbnail’ version, however don’t been fooled, you can create a thumbnail up to any reasonable size. Once done, the output can be saved and used just like the original. You can also set the CacheOption to cache the bitmap on load rather than on demand.

public BitmapImage CreateThumbnail(Uri Source, int PreferredWidth)
{
    BitmapImage bi = new BitmapImage();
    bi.BeginInit();
    bi.DecodePixelWidth = PreferredWidth;
    bi.CacheOption = BitmapCacheOption.OnLoad;
    bi.UriSource = Source;
    bi.EndInit();
    return bi;
}

This code takes a Uri Source and creates a thumbnail of the PeferredWidth, thus if you use this method you can drastically reduce the memory requirements of handling images as Brushes.

Do: Set the thumbnail size to your target on screen visual size, thus reducing memory utilisation and lowering graphics bandwidth utilisation.

Reducing Visual Tree Footprint
The Visual Tree contains all items that are to be rendered. There is a common illusion that built in controls only have one visual subelement, however things like TextBox have nearly 30! If you are never using an element again it may even be good to remove it completely from the parent control rather than just settings its Visibility to Hidden or Collapsed. If you ever do use it again then you may get a performance hit of re-adding it to the visual tree.

One way round may be to store the UIElement outside the Visual Tree so it won’t be touched during the Render Pass.

Below is a snippet you can use to work out how many subvisuals there are from a specific Visual in the Visual Tree. Note this does not take into account the Visibility dependency property, so even if it is not visible, the element will still be counted.

/// <summary>
/// Calculates the number of visual surfaces that are inherited from 
/// the visual specified
/// </summary>
/// <param name="v"></param>
/// <returns>Number of visual children.</returns>
public static int GetVisualTreeComplexity(Visual v)
{
    return GetVisualTreeComplexity(v, 0);
}

/// <summary>
/// Internal recursive Visual Tree helper to calculate visual complexity
/// </summary>
/// <param name="visual">The child visual to process</param>
/// <param name="complexity">The current complexity</param>
/// <returns></returns>
private static int GetVisualTreeComplexity(Visual visual, int complexity)
{
    int curComplexity = complexity;

    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(visual); i++)
    {
        Visual childVisual = (Visual)VisualTreeHelper.GetChild(visual, i);
        curComplexity = GetVisualTreeComplexity(childVisual, ++curComplexity);
    }

    return curComplexity;
}

Do: The lower the number generated is usually better, but sometimes unavoidable.

In Visual Studio 2010 and 2012 you can use the built in Tree Visualiser tool to analyse the Visual Tree and inspect dependency properties of child UIElements.
To open the Tree Visualiser, in a DataTip, Watch window, Autos window, or Locals window, next to a WPF object name, click the arrow adjacent to the magnifying glass icon. A list of visualizers is displayed. Click WPF Tree Visualizer.

Verify Data Bindings
Data binding in WPF and and also Silverlight is done vary lazy instantiated late bound resolution for bindings in XAML files. This feature allows a DataContext to be set at run-time via the code behind and the objects within that DataContext to correctly resolve their property bindings then.
This mechanism of using late binding (rather than compile time binding) allows for really useful features such as run-time loading of loose XAML, DataTemplates and composable applications.
Internally a lot of reflection occurs to attempt to wire up the data binding contexts. These are done in several orders of precedent to minimise the reflection hit. Therefore if an application has an incorrectly wired databinding or a missing data context, the applications performance can suffer while the control is initialised.
By default the output log will only show critical binding errors that can be detected during compile time. Internally additional checks can be performed and binding information can be shown to the output window.

Do: Fix incorrect bindings to speed up data context wiring and reduce reflection calls during UIElement construction.

To enable or customize WPF trace information for Data Binding

  1. On the Tools menu, select Options.
  2. In the Options dialog box, in the box on the left, open the Debugging node.
  3. Under Debugging, click Output Window.
  4. Under General Output Settings, select All debug output.
  5. In the box on the right, look for WPF Trace Settings.
  6. Open the WPF Trace Settings node.
  7. Under WPF Trace Settings, click the category of settings that you want to enable (Pick Data Binding).A drop-down list control appears in the Settings column next to Data Binding or whatever category you clicked.
  8. Click the drop-down list and select the type of trace information that you want to see: All, Critical, Error, Warning, Information,Verbose, or ActivityTracing.Critical enables tracing of Critical events only.Error enables tracing of Critical and Error events.

    Warning enables tracing of Critical, Error, and Warning events.

    Information enables tracing of Critical, Error, Warning, and Information events.

    Verbose enables tracing of Critical, Error, Warning, Information, and Verbose events.

    ActivityTracing enables tracing of Stop, Start, Suspend, Transfer, and Resume events.

  9. Click OK.

You can use this menu to set trace options for other properties.


Viewing all articles
Browse latest Browse all 15

Trending Articles