Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Terrible performance of custom-drawn control

Tags:

c#

wpf

rendering

I am making simple graph control in wpf. And I can't explain nor fix performance problem: it's too slow compared to winforms. Perhaps I am doing something wrong.

I prepare demo to demonstrate the problem.

Here is test control:

public class Graph : FrameworkElement
{
    private Point _mouse;
    private Point _offset = new Point(500, 500);

    public Graph()
    {
        Loaded += Graph_Loaded;
    }

    private void Graph_Loaded(object sender, RoutedEventArgs e)
    {
        // use parent container with background to receive mouse events too
        var parent = VisualTreeHelper.GetParent(this) as FrameworkElement;
        if (parent != null)
            parent.MouseMove += (s, a) => OnMouseMove(a);
    }

    protected override void OnRender(DrawingContext context)
    {
        // designer bugfix
        if (DesignerProperties.GetIsInDesignMode(this))
            return;

        Stopwatch watch = new Stopwatch();
        watch.Start();

        // generate some big figure (try to vary that 2000!)
        var radius = 1.0;
        var figures = new List<LineSegment>();
        for (int i = 0; i < 2000; i++, radius += 0.1)
        {
            var segment = new LineSegment(new Point(radius * Math.Sin(i) + _offset.X, radius * Math.Cos(i) + _offset.Y), true);
            segment.Freeze();
            figures.Add(segment);
        }
        var geometry = new PathGeometry(new[] { new PathFigure(figures[0].Point, figures, false) });
        geometry.Freeze();
        var pen = new Pen(Brushes.Black, 5);
        pen.Freeze();
        context.DrawGeometry(null, pen, geometry);

        // measure time
        var time = watch.ElapsedMilliseconds;
        Dispatcher.InvokeAsync(() =>
        {
            Window.GetWindow(this).Title = string.Format("{0:000}ms; {1:000}ms", time, watch.ElapsedMilliseconds);
        }, DispatcherPriority.Loaded);
    }

    protected override void OnMouseMove(MouseEventArgs e)
    {
        base.OnMouseMove(e);
        var mouse = e.GetPosition(this);
        if (e.LeftButton == MouseButtonState.Pressed)
        {
            // change graph location
            _offset.X += mouse.X - _mouse.X;
            _offset.Y += mouse.Y - _mouse.Y;
            InvalidateVisual();
        }
        // remember last mouse position
        _mouse = mouse;
    }
}

Here is how to use it in xaml:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication1"
        Title="MainWindow" Height="350" Width="525" WindowState="Maximized">
    <Grid Background="White">
        <local:Graph/>
    </Grid>
</Window>

Some remarks: control will draw figure, which can be moved by mouse:

enter image description here

It will display 2 measurements in title: first is how long it took for OnRender() to complete and second one is how long actual rendering took (first invoke after render).

Try to vary that 2000: setting 1000 makes moving comfortable, 3000 is like half-second delay before figure is redrawn (on my PC).

Questions:

  1. Is it good to use InvalidateVisual() to update graph offset in MouseMove? And if bad, what is the right technique to invalidate?
  2. Freezes, there are many of them without any noticeable effect. Do I need to use them or not?
  3. It looks like it takes only 5ms to complete render, but moving subjectively takes much longer (200ms+). Why is that?

And main question is of course performance, why is it so terrible? I could draw few hundred thousands of lines in winform control until it become as sloppy, as mine wpf control does with just 1000... =(


I found an answer on last question. Measuring of rendering time doesn't works correctly when moving with mouse. But if window is resized, then second time become 300ms (on my PC with 2000 figures). So it's not a wrong mouse invalidate (first question), but indeed very slow rendering.

like image 412
Sinatr Avatar asked Mar 19 '23 16:03

Sinatr


2 Answers

This a kind of task WPF is not very good at. I mean vector graphics in general. Thanks to the retained mode. It's good for controls rendering, but not for the busy graphs which you update a lot. I struggled with the same problem trying to render GPS tracks on a WPF map.

I'd suggest using direct2d and hosting it in WPF. Something like that: http://www.codeproject.com/Articles/113991/Using-Direct-D-with-WPF

That will give you high performance.

PS Don't get me wrong. There is nothing bad with WPF. It is designed to solve specific problems. It's very easy to compose controls and build impressive UIs. We take a lot for granted from the automatic layout system. But it cannot be clever in every situation possible and Microsoft didn't do a great job explaining the situations, where it's not a good option. Let me give you an example. IPad is performant because it has the fixed resolution and an absolute layout. If you fix the WPF window size and use canvas panel you'll get the same experience.

like image 138
Pasho Avatar answered Mar 29 '23 05:03

Pasho


here is a rewrite of your code using StreamGeometry this can give you a 5%-10% boost

    protected override void OnRender(DrawingContext context)
    {
        // designer bugfix
        if (DesignerProperties.GetIsInDesignMode(this))
            return;

        Stopwatch watch = new Stopwatch();
        watch.Start();

        // generate some big figure (try to vary that 2000!)
        var radius = 1.0;
        StreamGeometry geometry = new StreamGeometry();

        using (StreamGeometryContext ctx = geometry.Open())
        {
            Point start = new Point(radius * Math.Sin(0) + _offset.X, radius * Math.Cos(0) + _offset.Y);
            ctx.BeginFigure(start, false, false); 
            for (int i = 1; i < 2000; i++, radius += 0.1)
            {
                Point current = new Point(radius * Math.Sin(i) + _offset.X, radius * Math.Cos(i) + _offset.Y);
                ctx.LineTo(current, true, false);
            }
        }
        //var geometry = new PathGeometry(new[] { new PathFigure(figures[0].Point, figures, false) });
        geometry.Freeze();
        Pen pen = new Pen(Brushes.Black, 5);
        pen.Freeze();
        context.DrawGeometry(null, pen, geometry);

        // measure time
        var time = watch.ElapsedMilliseconds;
        Dispatcher.InvokeAsync(() =>
        {
            Window.GetWindow(this).Title = string.Format("{0:000}ms; {1:000}ms", time, watch.ElapsedMilliseconds);
        }, DispatcherPriority.Loaded);
    }

EDIT 2

here is a full rewrite of your class, this implements caching to avoid redraws and translate transform to perform the movements via mouse instead of redrawing again. also used UIElement as base for the element which is bit light weight then FrameworkElement

public class Graph : UIElement
{
    TranslateTransform _transform = new TranslateTransform() { X = 500, Y = 500 };
    public Graph()
    {
        CacheMode = new BitmapCache(1.4); //decrease this number to improve performance on the cost of quality, increasing improves quality 
        this.RenderTransform = _transform;
        IsHitTestVisible = false;
    }

    protected override void OnVisualParentChanged(DependencyObject oldParent)
    {
        base.OnVisualParentChanged(oldParent);

        if (VisualParent != null)
            (VisualParent as FrameworkElement).MouseMove += (s, a) => OnMouseMoveHandler(a);
    }

    protected override void OnRender(DrawingContext context)
    {
        // designer bugfix
        if (DesignerProperties.GetIsInDesignMode(this))
            return;

        Stopwatch watch = new Stopwatch();
        watch.Start();

        // generate some big figure (try to vary that 2000!)
        var radius = 1.0;
        StreamGeometry geometry = new StreamGeometry();

        using (StreamGeometryContext ctx = geometry.Open())
        {
            Point start = new Point(radius * Math.Sin(0), radius * Math.Cos(0));
            ctx.BeginFigure(start, false, false);
            for (int i = 1; i < 5000; i++, radius += 0.1)
            {
                Point current = new Point(radius * Math.Sin(i), radius * Math.Cos(i));
                ctx.LineTo(current, true, false);
            }
        }
        //var geometry = new PathGeometry(new[] { new PathFigure(figures[0].Point, figures, false) });
        geometry.Freeze();
        Pen pen = new Pen(Brushes.Black, 5);
        pen.Freeze();
        context.DrawGeometry(null, pen, geometry);

        // measure time
        var time = watch.ElapsedMilliseconds;
        Dispatcher.InvokeAsync(() =>
        {
            Application.Current.MainWindow.Title = string.Format("{0:000}ms; {1:000}ms", time, watch.ElapsedMilliseconds);
        }, DispatcherPriority.Loaded);
    }

    protected void OnMouseMoveHandler(MouseEventArgs e)
    {
        var mouse = e.GetPosition(VisualParent as FrameworkElement);
        if (e.LeftButton == MouseButtonState.Pressed)
        {
            _transform.X = mouse.X;
            _transform.Y = mouse.Y;
        }
    }
}

in example above I used 5000 to test and I can say that it is quite smooth.

As this enable fluid movements via mouse but actual render may take a bit longer to create the cache(first time only). I can say 1000% boost in moving object via mouse, render remain quite close to my previous approach with little overhead of caching. try this out and share what you feel


EDIT 3

here is a sample using DrawingVisual the lightest approach available in WPF

public class Graph : UIElement
{
    DrawingVisual drawing;
    VisualCollection _visuals;
    TranslateTransform _transform = new TranslateTransform() { X = 200, Y = 200 };
    public Graph()
    {
        _visuals = new VisualCollection(this);

        drawing = new DrawingVisual();
        drawing.Transform = _transform;
        drawing.CacheMode = new BitmapCache(1);
        _visuals.Add(drawing);
        Render();
    }

    protected void Render()
    {

        // designer bugfix
        if (DesignerProperties.GetIsInDesignMode(this))
            return;
        Stopwatch watch = new Stopwatch();
        watch.Start();

        using (DrawingContext context = drawing.RenderOpen())
        {

            // generate some big figure (try to vary that 2000!)
            var radius = 1.0;
            StreamGeometry geometry = new StreamGeometry();

            using (StreamGeometryContext ctx = geometry.Open())
            {
                Point start = new Point(radius * Math.Sin(0), radius * Math.Cos(0));
                ctx.BeginFigure(start, false, false);
                for (int i = 1; i < 2000; i++, radius += 0.1)
                {
                    Point current = new Point(radius * Math.Sin(i), radius * Math.Cos(i));
                    ctx.LineTo(current, true, false);
                }
            }
            geometry.Freeze();
            Pen pen = new Pen(Brushes.Black, 1);
            pen.Freeze();
            // measure time
            var time = watch.ElapsedMilliseconds;
            context.DrawGeometry(null, pen, geometry);

            Dispatcher.InvokeAsync(() =>
            {
                Application.Current.MainWindow.Title = string.Format("{0:000}ms; {1:000}ms", time, watch.ElapsedMilliseconds);
            }, DispatcherPriority.Normal);
        }

    }
    protected override Visual GetVisualChild(int index)
    {
        return drawing;
    }

    protected override int VisualChildrenCount
    {
        get
        {
            return 1;
        }
    }

    protected override void OnMouseMove(MouseEventArgs e)
    {
        if (e.LeftButton == MouseButtonState.Pressed)
        {
            var mouse = e.GetPosition(VisualParent as FrameworkElement);

            _transform.X = mouse.X;
            _transform.Y = mouse.Y;
        }
        base.OnMouseMove(e);
    }
}
like image 21
pushpraj Avatar answered Mar 29 '23 06:03

pushpraj