Meine Bewerbung als Wirtschaftsweiser Sonnenklar

Silverlight: Fixing the BookShelf Sample

Published on Tuesday, January 4, 2011 3:10:00 PM UTC in Programming

Note: although this post is primarily about the BookShelf code sample, it also contains general information about shader effects in Silverlight and their performance implications, as well as some weird behaviors of the Silverlight runtime regarding those effects.

On 2010's PDC John Papa held a session named "Kung Fu Silverlight" to show some concepts of MVVM and RIA Services. The associated source code can be downloaded here. I've often used this code as a reference for people who were looking for a nice MVVM sample. Even if it's not fully polished to the last detail, it's an excellent start to learn about the involved patterns and practices. Yesterday, when I wanted to use the sample to demonstrate something, it required me to log in. And when I did that, my browser basically crashed. Well, actually it only hung, but it didn't come back to life, so I had to kill the process. I knew the sample had some performance issues due to a certain visual effect that results in problems, but it never happened before that my browser simply stopped working, so I decided to finally explore the problem in more detail and fix it. Read on to learn how.

A quick fix

Like I said I knew of some performance problems with a drop shadow effect used on one of the borders in this sample (more on that below) so that was a good starting point. I turned off that effect and indeed - the problem was gone instantly. The problem with this however was that it was completely illogical that this effect should be the cause based on what could be observed in the application. To understand this, let's talk about the problem with effects first.

Effects

In Silverlight, you can add effects to visual elements using their Effects property. There are some built-in effects available already (drop shadow and blur), but you can also make your own effects (those effects are pixel shaders, but I don't go into further details here). Regarding performance, these effects are problematic in two ways:

  1. They are rendered using the CPU, not the GPU.
  2. The Silverlight runtime cannot know what a specific effect actually does, so it has to redraw an element (and all its children) if that element has an effect applied, whenever one of the child elements changes their visual appearance.

The second point is crucial here. If you apply an effect to a big container, then every little visual change within that container will cause a redraw of the complete container (even if the effect actually wouldn't require that!). The worst case is an effect on a top-level container element, and some (even a tiny) animation of an element in that container. This will lead to a redraw of the whole container on each animation frame.

This situation is exactly what we have in the BookShelf example. One of the outer borders has a drop shadow effect applied. Whenever something on the entire page changes, no matter how small it is, the entire border is redrawn. You can test that if you set the EnableRedrawRegions property to true. I've added a check box to the main page of the BookShelf sample to toggle it, like this:

private void RedrawRegionsCheckBox_Clicked(object sender, RoutedEventArgs e)
{
    var settings = Application.Current.Host.Settings;
    settings.EnableRedrawRegions = !settings.EnableRedrawRegions;

    RedrawRegionsCheckBox.IsChecked = settings.EnableRedrawRegions;
}

This tints those areas that are redrawn in cycling colors. You will see that even if you just hover of some list item element on the screen, the whole screen is redrawn. If you disable the drop shadow effect of the outer border, this changes dramatically: only those areas that actually have changed are redrawn.

When I found out that this effect is the problem of the bad performance, I was puzzled though. By the time the CPU load ramped up, absolutely nothing was going on on the screen. There was no reason for that effect to been redrawn at all. Then what was the problem?

A deeper analysis

My first approach actually was to work around the problem. Like I said the problem occurs when you use the login window to authenticate. What I tried was disable the effect before the login window was shown, and enable it once the window was closed or unloaded. To my surprise that didn't work either. I added another checkbox to the main page that allowed me to toggle the effect on the border for testing, and the result was even more confusing: you could open and close the login window and have no problems, but as soon as you did the actual login request (round-trip to the service), everything went downhill. Disabling the effect fixed the problem, but re-enabling it instantly hogged the CPU. Huh?

Long story short: when the call to the service is made, a busy indicator is shown. That busy indicator contains a progress bar which has its IsIndeterminate property set to true to show some animation as the service call proceeds. I found out that this particular animation is the reason for the drop shadow effect rampage on the main page. When you set IsIndeterminate to false everything works as expected. The two truly amazing parts of this are:

  1. The busy indicator and its progress bar are located in a child window. Those windows are not part of the normal visual tree; the progress bar in particular is not even a direct or indirect child of the border that has the effect.
  2. When the child window is closed, the progress bar is actually unloaded (you can confirm that when you hook its events). Yet still that animation somehow affects the rendering of that drop shadow effect.

I don't know how the management of those effects in the runtime works. However, if an animation of a control that's not visible (not even alive) anymore can affect the render behavior of an element which is not even a logical parent of that element, something is truly wrong here.

Possible solutions

One possible solution is to create a custom progress bar that switches the IsIndeterminate property to false when it is not visible. I consider this a slight hack as there are scenarios where it would not work, for example when the IsIndeterminate property is changed dynamically. For the BookShelf sample, it works without any problems (you have to replace the progress bar in the busy indicator style with it, and also update the style references to the normal progress bar to target the custom one instead):

public class ProgressBarEx : ProgressBar
{
    private bool? _isIndeterminateBackup;

    private bool IsVisible
    {
        get
        {
            FrameworkElement element = this;

            while (element != null)
            {
                if (element.Visibility == Visibility.Collapsed)
                {
                    return false;
                }

                element = VisualTreeHelper.GetParent(element) as FrameworkElement;
            }

            return true;
        }
    }

    public ProgressBarEx() : base()
    {
        this.LayoutUpdated += new EventHandler(ProgressBarEx_LayoutUpdated);            
    }

    private void ProgressBarEx_LayoutUpdated(object sender, EventArgs e)
    {
        if (!_isIndeterminateBackup.HasValue)
        {
            _isIndeterminateBackup = IsIndeterminate;
        }

        IsIndeterminate = IsVisible ? _isIndeterminateBackup.Value : false;            
    }
}

A surprising and much better solution however is the one that was also described in one of the sessions of the Silverlight Firestarter event (Session 6: Profiling and Performance Tips). I strongly recommend that session to anyone interested in the topic. It also analyzes the performance implications of shader effects I've discussed above, using the BookShelf project as one of the samples.

To avoid the redraw problems with the BookShelf sample, the solution is to remove the drop shadow effect from the style of the outer border. Then you have to add a second, identical border to your main page that has the drop shadow effect applied, but no content. That last detail is vital. The first border has all the page content but no effect (so no full redraw is triggered when some of the content changes), and the second border which has the effect has no content (so a redraw actually is never triggered).

<Border Style="{StaticResource ContentBorderStyle}">
    <!-- This empty border is identical to the real content border, 
            but has the drop shadow effect -->
    <Border.Effect>
        <DropShadowEffect BlurRadius="10"
                            Opacity="0.25"
                            ShadowDepth="0" />
    </Border.Effect>
</Border>

<!-- The drop shadow effect has been removed from the ContentBorderStyle -->
<Border x:Name="ContentBorder"
        Style="{StaticResource ContentBorderStyle}">
    <navigation:Frame x:Name="ContentFrame"
                        Style="{StaticResource ContentFrameStyle}"
                        Source="/BookView"
                        Navigated="ContentFrame_Navigated"
                        NavigationFailed="ContentFrame_NavigationFailed"
                        Margin="0,90,0,0">
        <navigation:Frame.UriMapper>
            <uriMapper:UriMapper>
                <uriMapper:UriMapping Uri="/{pageName}"
                                        MappedUri="/Views/{pageName}.xaml" />
            </uriMapper:UriMapper>
        </navigation:Frame.UriMapper>
    </navigation:Frame>
</Border>

Interestingly enough, this also fixes the observed problem with the login dialog and the progress bar. Not fully though. The CPU hogging happens here too, but when the child window is closed things seem to get cleaned up and everything returns to normal. You'll only notice the effect if for example you enter a wrong password, so the login dialog stays open after the progress bar was shown.

Once again I cannot fully explain why that is or why this work around also fixes the issue with the progress bar animation in some way. In fact, there doesn't seem to be any logic in this when you look at it from the outside. But I've followed the problem as far as possible without going overboard and digging into the runtime (I have no interest in that), found the culprit to be the combination of the effect and indeterminate progress bar and some possible solutions to the problem.

Hopefully this helps others in one way or another.

Tags: Bug Fix · Shader Effects · Silverlight