Mind Your Freeze and Q’s
January 4, 2009
New York, N.Y.
Whenever the subject of performance in the Windows Presentation Foundation comes up, the issue of Freezables is likely to be mentioned. But regardless how much we think we know about the subject, it's still easy for experienced WPF programmers to get burned. I'll be describing something that happened to me recently that reinforced how much we need to pay diligent attention to objects of type Freezable.
Some background: The Freezable class derives from DependencyObject and adds extra layers of change notifications. A dependency property of type Freezable will fire a change notification whenever the object itself changes, of course, but also when any property or sub-property of the object changes, as long as those properties are also of type Freezable.
For example, suppose you have a class containing a dependency object of type PathGeometry. Your class will receive notifications whenever a different PathGeometry is assigned to that property, of course, but also when any component of that PathGeometry changes, for example, the Point1 property of a BezierSegment object that is a member of a Segments collection that belongs to a PathFigure object that is a member of the Figures collection that belongs to the PathGeometry. The PathGeometry, PathFigure, BezierSegment, PathFigureCollection, and PathSegmentCollection classes all derive from Freezable.
The WPF graphics system is filled with classes that derive from Freezable, and it is this sub-property notification feature of Freezable that allows WPF graphical objects to respond so well to animations.
However, this notification scheme has a cost, so whenever you have a Freezable object that won't be changed, it is recommended that you freeze it and make it unchangeable. You can freeze a Freezable in code simply by calling the Freeze method. (You can also freeze a Freezable in XAML but it's rather messy and is described here.) You must freeze a Freezable when accessing it from a thread other than the one that created it.
Recently I was working on an article for the March 2009 issue of MSDN Magazine about ItemsControl performance. I had an occasion to write a class that used the x:Array markup extension — which I hadn't previously used much at all — and all of a sudden performance just got terrible.
Of course, I was positive that x:Array was at fault, so like a good programmer I tried to isolate the problem in a small project I naturally called ArrayIssue.
The ArrayIssue project contains a small FrameworkElement derivative named RandomPlot that displays a simple scatterplot. The class has two dependency properties: Num of type int, which is the number of points displayed, and BrushArray of type Brush[]. The constructor initializes the BrushArray property:
-
public RandomPlot()
{
BrushArray = new Brush[3];
BrushArray[0] = Brushes.Red;
BrushArray[1] = Brushes.Green;
BrushArray[2] = Brushes.Blue;
}
The OnRender method displays a bunch of random points, randomly colored with one of the elements of the BrushArray array:
-
protected override void OnRender(DrawingContext dc)
{
dc.DrawRectangle(Brushes.Gray, null, new Rect(RenderSize));
if (BrushArray == null)
return;
Random rand = new Random();
for (int i = 0; i < Num; i++)
dc.DrawEllipse(BrushArray[rand.Next(BrushArray.Length)], null,
new Point(rand.NextDouble() * RenderSize.Width,
rand.NextDouble() * RenderSize.Height), 1, 1);
}
A XAML file creates two Buttons and two instances of RandomPlot. The second instance uses x:Array to redefine a different array of Brush objects:
-
<Window
x:Class="ArrayIssue.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:src="clr-namespace:ArrayIssue"
SizeToContent="WidthAndHeight">
<StackPanel Orientation="Horizontal">
<Button Content="Update #1"
Click="OnUpdateOneClick"
VerticalAlignment="Center"
Margin="12" />
<src:RandomPlot x:Name="randomPlot1"
Width="300" Height="300" Margin="12" />
<Button Content="Update #2"
Click="OnUpdateTwoClick"
VerticalAlignment="Center"
Margin="12" />
<src:RandomPlot x:Name="randomPlot2"
Width="300" Height="300" Margin="12">
<src:RandomPlot.BrushArray>
<x:Array Type="Brush">
<SolidColorBrush Color="Cyan" />
<SolidColorBrush Color="Magenta" />
<SolidColorBrush Color="Yellow" />
</x:Array>
</src:RandomPlot.BrushArray>
</src:RandomPlot>
</StackPanel>
</Window>
The Click handlers for the buttons set the Num property of the corresponding RandomPlot to 10000, which triggers a re-render. If you compile and run this program, you'll discover a very dramatic performance difference. The first RandomPlot with the array defined in code updates very quickly, but the second RandomPlot with the x:Array in XAML requires a few seconds.
The difference has nothing to do with the x:Array element but everything to do with the way the children of x:Array were defined. Recall that the RandomPlot class initialized the BrushArray with statements like this:
-
BrushArray[0] = Brushes.Red;
The static members of the Brushes class return objects of type SolidColorBrush but these objects are frozen. The XAML file, however, creates unfrozen objects of type SolidColorBrush:
-
<SolidColorBrush Color="Cyan" />
Who cares whether the brush used to color the dots of the scatterplot is frozen or unfrozen? The composition system cares. If you pass an unfrozen Freezable into the composition system (which occurs during the OnRender method), the system has to treat this is in a special way because it must respond to changes in the properties of the object. (See my blog entry WPF Retained Graphics and the SubPropertiesDoNotAffectRender Flag for ways to exploit this.) When the objects are frozen, however, there's no way they can be changed and the composition system can handle them more simply with no performance penalty.
Try adding this little piece of code to the OnRender method in RandomPlot after the check for a non-null BrushArray:
-
foreach (Brush brush in BrushArray)
brush.Freeze();
That fixes the problem. So will defining the children of the x:Array element like this:
-
<x:Array Type="Brush">
<x:Static Member="Brushes.Cyan" />
<x:Static Member="Brushes.Magenta" />
<x:Static Member="Brushes.Yellow" />
</x:Array>
This object element syntax of x:Static is rather unusual, of course, and I don't think I would have remembered the name of the Member attribute on my own, so I suspect my laziness in this regard was what caused me to use elements of SolidColorBrush instead.