views:

185

answers:

3

In our app, we have some scrolling credits in a ChildWindow. When showing this window, our CPU utilization is very high. The text is using a BitmapCache and hardware acceleration is enabled. Even after removing the clipping rectangle and the drop shadow from the child window, the CPU usage climbs to 80-90%. When I enable redraw region visualization, I see that only the scrolling text is getting redrawn, so I'm unsure why the CPU is going crazy. I tried animating both Canvas.Top and the TranslateY property of a CompositeTransform to do the scrolling.

Any ideas as to what may be causing this animation to be so expensive? Are there any good articles out there that have recommendations for optimizing animations in general? Here's my XAML:

<c:ChildWindow xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
               xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
               xmlns:c="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
               x:Class="OurNamespace.UI.Views.AboutWindow"
               Title="About Our App" Width="575"
               Height="330" Style="{StaticResource ChromelessChildWindowStyle}"
               mc:Ignorable="d" 
               MouseRightButtonDown="ChildWindow_MouseRightButtonDown" 
               Background="Black">
  <Grid x:Name="LayoutRoot" CacheMode="BitmapCache">
    <Grid.Triggers>
      <EventTrigger RoutedEvent="Canvas.Loaded">
        <BeginStoryboard>
          <Storyboard Storyboard.TargetName="CreditsTransform" 
                      Storyboard.TargetProperty="TranslateY">
            <DoubleAnimation To="-750" RepeatBehavior="Forever" 
                             Duration="0:0:30"/>
          </Storyboard>
        </BeginStoryboard>
      </EventTrigger>
    </Grid.Triggers>
    <Image HorizontalAlignment="Left" VerticalAlignment="Top" 
           Source="/Assets/Graphics/SplashAbout/OurBackground.png"/>
    <Grid Height="150" Width="570" HorizontalAlignment="Right" 
          Margin="0,0,0,80" VerticalAlignment="Bottom">
      <Grid.RowDefinitions>
        <RowDefinition Height="30"/>
        <RowDefinition/>
      </Grid.RowDefinitions>
      <TextBlock x:Name="AppVersionTextBlock" Margin="10,0" 
                 VerticalAlignment="Center" FontFamily="Arial" 
                 FontSize="12" Foreground="White" 
                 Text="{Binding VersionInfo, FallbackValue=Version 2.0.0}" 
                 TextWrapping="Wrap"/>
      <TextBlock x:Name="FirmwareVersionTextBlock" Margin="10,0" 
                 VerticalAlignment="Center" FontFamily="Arial" FontSize="12"
                 Foreground="White" Text="{Binding FirmwareVersion.Value, FallbackValue=Firmware Version 1.0.0}" 
                 TextWrapping="Wrap" 
                 Visibility="{Binding FirmwareVersionVisibility.Value}" 
                 TextAlignment="Right"/>
      <Canvas Margin="0" Grid.Row="1" x:Name="Viewport">
        <Canvas.Clip>
          <RectangleGeometry Rect="0,0,575,120"/>
        </Canvas.Clip>
        <TextBlock FontFamily="Arial" FontSize="12" Width="555" 
                   Foreground="White" TextWrapping="Wrap" Canvas.Left="10"
                   Text="{Binding Credits}" x:Name="Credits" 
                   TextAlignment="Center" RenderTransformOrigin="0.5,0.5">
          <TextBlock.RenderTransform>
            <CompositeTransform TranslateY="0" x:Name="CreditsTransform"/>
          </TextBlock.RenderTransform>
          <TextBlock.CacheMode>
            <BitmapCache/>
          </TextBlock.CacheMode>
        </TextBlock>
      </Canvas>
    </Grid>
    <TextBlock Foreground="White" Text="{Binding CopyrightInfo, FallbackValue=© 2010 Our Company}" 
               TextWrapping="Wrap" Width="413" FontSize="10" 
               FontFamily="Arial" Height="44" HorizontalAlignment="Right" 
               Margin="0,0,30,21" VerticalAlignment="Bottom"/>
    <Button x:Name="CancelButton" Width="575" Height="330" Opacity="0" 
            Click="CancelButton_Click" HorizontalAlignment="Right" 
            Margin="0" VerticalAlignment="Bottom"/>
  </Grid>
</c:ChildWindow>

Update:

The CPU problem was not directly related to the ChildWindow itself but to the DropShadowEffect objects underneath which Silverlight was wastefully re-rendering. I've added an answer to describe how I worked around this.

A: 

You might be setting cache mode to BitmapCache a little too aggressively. Using BitmapCache can hurt performance in some cases.

You can find some basic guidance on Silverlight performance issues at http://msdn.microsoft.com/en-us/library/cc189071(VS.95).aspx (which includes some tips on using BitmapCache).

Jesse Collins
Thanks for your answer, but I had added `BitmapCache` because performance was even worse without it. Specifically, the entire window was getting re-rendered continuously.
Jacob
+2  A: 

When animating text in Silverlight you should be setting the TextHintingMode attached property to "Animated" on your TextBlock. To improve text readability Silverlight usually uses hinting to smooth each text glyph. This can have a large performance impact when animating text, since a change will cause recalculation of how the glyph is most legible which could be happening up to 60 frames a second with in an animation.

<TextBlock TextOptions.TextHintingMode="Animated"
           FontFamily="Arial" FontSize="12" Width="555" 
           Foreground="White" TextWrapping="Wrap" Canvas.Left="10"
           Text="{Binding Credits}" x:Name="Credits" 
           TextAlignment="Center" RenderTransformOrigin="0.5,0.5">
...
</TextBlock>

If that does not solve your problem I would recommend you start debugging performance with XPerf. There is a good tutorial on using this command-line tool to see where most of your CPU time is spent while a portion of your Silverlight application runs. You should be paying attention to how much CPU time is spent in agcore.dll, npctrl.dll, and coreclr.dll. If your performance problems are related to redrawing, most of the CPU time is likely spent in agcore.dll since that does most of the graphics related work for Silverlight. You can then drill into that and see the specific functions in agcore.dll that are getting called most often during your sample time. This can often help you realize which portions of your code are causing the performance hit and how you can optimize.

Dan Auclair
Excellent answer. I tried changing the text hinting mode to Animated, and while that helped a tiny bit, the CPU usage is still enormous. I'll try XPerf next.
Jacob
Blast! XPerf appears to be unavailable on Windows XP, which for some reason our company chooses to stick with. Is there something similar available for XP?
Jacob
Looks like you can run xperf.exe which is used to collect the CPU data on Windows XP, but you will need Server 2008, Vista, or 7 to run xperfview.exe to view and analyze the data. It is pretty much useless without being able to easily view the data. Not sure of another way without a more recent OS or some kind of virtual machine...http://blogs.msdn.com/b/pigscanfly/archive/2008/02/24/xperf-support-for-xp.aspx
Dan Auclair
A: 

It turned out that it wasn't really the contents of our ChildWindow that was causing the high CPU usage. Instead, the many DropShadowEffect objects behind the ChildWindow were sapping our CPU. Apparently, Silverlight is really dumb when it comes to the redrawing logic of its effects.

Eventually, we're going to phase out the use of effects, which is really sad. But since that's a lot of work, in the interim I created a handy attached property and utility methods for temporarily disabling effects and re-enabling them:

private static IDictionary<UIElement, Effect> _effects = 
    new Dictionary<UIElement, Effect>();

public static readonly DependencyProperty CanDisableEffectsProperty = DependencyProperty.RegisterAttached(
    "CanDisableEffects", typeof(bool), typeof(FrameworkUtils),
    new PropertyMetadata(onCanDisableEffectsChanged));

public static bool GetCanDisableEffects(DependencyObject obj)
{
    return (bool)obj.GetValue(CanDisableEffectsProperty);
}

public static void SetCanDisableEffects(
    DependencyObject obj, bool value)
{
    obj.SetValue(CanDisableEffectsProperty, value);
}

private static void onCanDisableEffectsChanged(
    DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
    var enable = (bool)args.NewValue;

    var uiElement = obj as UIElement;
    var fElement = obj as FrameworkElement;

    if (uiElement != null)
    {
        if (enable && uiElement.Effect != null)
        {
            _effects[uiElement] = uiElement.Effect;
        }
    }

    if (fElement != null)
    {
        Action applyToChildren = () => uiElement.GetVisualChildren()
            .ForEach(c => SetCanDisableEffects(c, enable));

        applyToChildren();
        fElement.Loaded += (s, e) => applyToChildren();
    }
}

public static void DisableAllEffects()
{
    _effects.Keys.ForEach(ui => ui.Effect = null);
}

public static void EnableAllEffects()
{
    _effects.ForEach(p => p.Key.Effect = p.Value);
}

So what I did is attach the CanDisableEffects property to all items containing effects. Then, when our child windows with the animations are loaded, I invoke the DisableAllEffects method. Then, when the ChildWindow.Closed event fires, I call EnableAllEffects to re-enable. Since the overlay for the ChildWindow darkens the background anyway, the removal of effects isn't noticeable, but the reduced CPU usage is.

I'm accepting Dan Auclair's answer since it answers my question as asked. I've posted this answer to help anyone else that may be experiencing problems with effects.

Jacob