Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is Wpf's DrawingContext.DrawText so expensive?

Tags:

c#

wpf

drawing

In Wpf (4.0) my listbox (using a VirtualizingStackPanel) contains 500 items. Each item is of a custom Type

class Page : FrameworkElement
...
protected override void OnRender(DrawingContext dc)
{
   // Drawing 1000 single characters to different positions
   //(formattedText is a static member which is only instantiated once and contains the string "A" or "B"...)
   for (int i = 0; i < 1000; i++)
     dc.DrawText(formattedText, new Point(....))


  // Drawing 1000 ellipses: very fast and low ram usage
    for (int i = 0; i < 1000; i++)     
    dc.DrawEllipse(Brushes.Black, null, new Point(....),10,10)


}

Now when moving the scrollbar of the listbox back and forth so that every item's visual is created at least once the ram usage goes up to 500 Mb after a while and then - after a while - goes back to ca 250 Mb but stays on this level. Memory leak ? I thought the advantage of a VirtualizingStackPanel is that visuals which are not needed/visible get disposed...

Anyway, this extreme ram usage only appears when drawing text using "DrawText". Drawing other objects like "DrawEllipse" does not consume so much memory.

Is there a more efficient way to draw many text items than using Drawing.Context's "DrawText" ?

Here is the complete sample (just create a new Wpf Application project and replace the window1 code): (I know there are FlowDocument and FixedDocument but they are no alternative) Xaml:

<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="900" Width="800">
<Grid Background="Black">
    <ListBox Name="lb" ScrollViewer.CanContentScroll="True"   Background="Black">
        <ListBox.ItemsPanel>
            <ItemsPanelTemplate>
                <VirtualizingStackPanel Orientation="Horizontal" />
            </ItemsPanelTemplate>
        </ListBox.ItemsPanel>
    </ListBox>
</Grid>
</Window>

And the Window1.xaml.cs:

public partial class Window1 : Window
{
    readonly ObservableCollection<FrameworkElement> collection = new ObservableCollection<FrameworkElement>();

  public Window1()
    {
        InitializeComponent();

        for (int i = 0; i < 500; i++)
        {
            collection.Add(new Page(){ Width = 500, Height = 800 });
        }

        lb.ItemsSource = collection;
    }
}

 public class Page : FrameworkElement
{
    static FormattedText formattedText = new FormattedText("A", CultureInfo.GetCultureInfo("en-us"),
                                              FlowDirection.LeftToRight,
                                              new Typeface(new FontFamily("Arial").ToString()),
                                              12,Brushes.Black);
    protected override void OnRender(DrawingContext dc)
    {
        dc.DrawRectangle(Brushes.White, null, new Rect(0, 0, Width, Height));
        double yOff = 0;
        for (int i = 0; i < 1000; i++) // draw 1000 "A"s 
        {
            dc.DrawText(formattedText, new Point((i % 80) * 5, yOff ));
            if (i % 80 == 0) yOff += 10;

        }

    }

}
like image 965
fritz Avatar asked Nov 01 '10 09:11

fritz


3 Answers

A big contributor is the fact(based on my experience with GlyphRun which I think gets used behind the scenes) that it uses at least 2 dictionary look-ups per character to get the glyph index and width. One hack I used in my project was I figured out the offset between the ASCII value and the glyph index for alphanumeric characters for the font I was using. I then used that to calculate the glyph indexes for each character rather than doing the dictionary look up. That gave me a decent speed up. Also the fact that I could reuse the glyph run moving it around with a translate transform without recalculating everything or those dictionary lookups. The system can't do this hack on it's own because it is not generic enough to be used in every case. I imagine a similar hack could be done for other fonts. I only tested with Arial,other fonts could be indexed differently. May be able to go even faster with a mono-spaced font since you may be able to assume the glyph widths would all be the same and only do one look up rather than one per character, but I haven't tested this.

The other slow down contributor is this little code, I haven't figured out how to hack it yet. typeface.TryGetGlyphTypeface(out glyphTypeface);

Here is my code for my alphanumeric Arial hack(compatibility with other characters unknown)

public  GlyphRun CreateGlyphRun(string text,double size)
    {
        Typeface typeface = new Typeface("Arial");
        GlyphTypeface glyphTypeface;
        if (!typeface.TryGetGlyphTypeface(out glyphTypeface))
            throw new InvalidOperationException("No glyphtypeface found");          

        ushort[] glyphIndexes = new ushort[text.Length];
        double[] advanceWidths = new double[text.Length];

        for (int n = 0; n < text.Length; n++) {
            ushort glyphIndex = (ushort)(text[n] - 29);
            glyphIndexes[n] = glyphIndex;
            advanceWidths[n] = glyphTypeface.AdvanceWidths[glyphIndex] * size;
        }

        Point origin = new Point(0, 0);

        GlyphRun glyphRun = new GlyphRun(glyphTypeface, 0, false, size, glyphIndexes, origin, advanceWidths, null, null, null,
                                         null, null, null);
        return glyphRun;
    }
like image 92
user638350 Avatar answered Oct 27 '22 05:10

user638350


While this isn't entirely useful to you, my experience with VirtualizingStackPanel isn't that it disposes of objects not in view, but that it allows objects not in view to be disposed to recover memory when the application needs more memory, which should result in your memory usage ballooning when there is memory available.

Is it possible that dc.DrawText is firing BuildGeometry() for each formattedText object, and that you can bring that outside the loop? I don't know how much work BuildGeometry is, but it's possible that the DrawingContext is only capable of accepting geometry, and the BuildGeometry call is being called unnecessarily 999 times in your sample. Have a look at:

http://msdn.microsoft.com/en-us/library/system.windows.media.formattedtext.aspx

to see whether there are any other optimizations you can make.

Can you output some memory profile data and some timing data within your loops to give a sense of whether it's slowing down, or the memory is increasing in a non-linear fashion during the loop?

like image 2
David Hagan Avatar answered Oct 27 '22 05:10

David Hagan


I found user638350's solution to be very useful; in my case i only use one font size so the following optimizations reduced the time to less than 0.0000 over 20,000 frames down from 0.0060ms every frame. Most of the slow down is from 'TryGetGlyphTypeface' and 'AdvanceWidths' and so these two are cached. Also, added calculating an offset position and tracking a total width.

    private static Dictionary<ushort,double> _glyphWidths = new Dictionary<ushort, double>();
    private static GlyphTypeface _glyphTypeface;
    public static GlyphRun CreateGlyphRun(string text, double size, Point position)
    {
        if (_glyphTypeface == null)
        {
            Typeface typeface = new Typeface("Arial");
            if (!typeface.TryGetGlyphTypeface(out _glyphTypeface))
                throw new InvalidOperationException("No glyphtypeface found");                
        }

        ushort[] glyphIndexes = new ushort[text.Length];
        double[] advanceWidths = new double[text.Length];

        var totalWidth = 0d;
        double glyphWidth;

        for (int n = 0; n < text.Length; n++)
        {
            ushort glyphIndex = (ushort)(text[n] - 29);
            glyphIndexes[n] = glyphIndex;

            if (!_glyphWidths.TryGetValue(glyphIndex, out glyphWidth))
            {
                glyphWidth = _glyphTypeface.AdvanceWidths[glyphIndex] * size;
                _glyphWidths.Add(glyphIndex, glyphWidth);
            }
            advanceWidths[n] = glyphWidth;
            totalWidth += glyphWidth;
        }

        var offsetPosition = new Point(position.X - (totalWidth / 2), position.Y - 10 - size);

        GlyphRun glyphRun = new GlyphRun(_glyphTypeface, 0, false, size, glyphIndexes, offsetPosition, advanceWidths, null, null, null, null, null, null);

        return glyphRun;
    }
like image 1
jv_ Avatar answered Oct 27 '22 05:10

jv_