4

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.

Sinatr
  • 20,892
  • 15
  • 90
  • 319
  • 1
    I created a RealTime graph using WritablebitmapExtensions.it has much better performance . writeablebitmapextensions http://writeablebitmapex.codeplex.com/ – eran otzap Aug 22 '14 at 16:18
  • Using StreamGeometry can improve performance to some extent, perhaps 5%-10%. BTW if you are just moving the drawing on mouse move and not updating the actual drawing then caching the same and painting it as a picture would improve performance significantly. – pushpraj Aug 25 '14 at 14:16

3 Answers3

3

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.

Pasho
  • 488
  • 3
  • 15
  • I plan to draw special info on top of graph (perhaps by making it container), so that *direct2d* without overlapping doesn't sounds good. I could host my old winforms fast graph inside wpf, but I am looking for a *native* solution (native to wpf) with all its greatness. – Sinatr Aug 25 '14 at 07:39
  • Cool, I'd love to see that too. But as I said, that's not something WPF was designed for. These types of tasks should be solved the same way as in WinForms. You need a targetted optimization. Perhaps there are controls which do that. Found something interesting here http://dynamicdatadisplay.codeplex.com/wikipage?title=D3v1 – Pasho Aug 26 '14 at 11:03
  • Thanks, but I am not sure in performance of that one (mine can also draw million of *lines*, just give it few seconds =D). User interactivity is the main issue. The @pushpraj answer is so far the closest, he is trying to optimize that *mouse lag*. – Sinatr Aug 26 '14 at 11:12
  • So far I have a feeling that you are putting a square peg into a round hole. You could cut down the symptoms slightly, but there is an inherent design problem. Personally I don't see any reason to use WPF just for the sake of using WPF where WPF is known to fail. You get user interactivity in primitive winforms, right? Not too difficult. Perhaps you'd already have an alternative solution working by now. – Pasho Aug 26 '14 at 12:07
  • When moving winform graph, it feels pretty: moving is smooth and it only become laggy for a huge number of points (roughly over 1.000.000 **points**), it is also ok to move and update graph **simultaneously**. In wpf, same graph (render-based) is already laggy even with just 1 point, because of delayed rendering. @Pushpray transformation based graph can be moved smoothly but **only after rendering**. Which means it is impossible to move and update wpf graph. User interactivity for me means: moving and zooming graph freely, getting hit-test info, while updating graph frequently. – Sinatr Aug 26 '14 at 13:09
  • I had a similar problem building an interactive map tool. Tried to render hundreds and thousands of gps tracks on a map and be able to zoom in/out, move. The performance was poor. I tried to optimize it in many ways. Then I abandoned the project and returned to it a couple of months later rewriting it in html5 and typescript. Just wanted the tool to work in my Android phone. I was really surprised by the SVG performance. But that's probably an optimization too far :) – Pasho Aug 26 '14 at 15:49
2

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);
    }
}
pushpraj
  • 13,458
  • 3
  • 33
  • 50
  • 10% boost is useful, thanks, will check it tomorrow. – Sinatr Aug 25 '14 at 14:53
  • I would be able to optimize it further based on the following questions. How frequently is the drawing/graph updated? (at least not with every refresh/mouse move) is the drawing based on data(like ploting engines) or functions(like you have now)? Wondering if I can help you with this project. – pushpraj Aug 26 '14 at 02:26
  • In the meanwhile you may perhaps want to look at [Speed up adding objects to Canvas In WPF](http://stackoverflow.com/questions/23976163/speed-up-adding-objects-to-canvas-in-wpf) & [How to speed up rendering of vertical scrollbar markers](http://stackoverflow.com/questions/24318971/how-to-speed-up-rendering-of-vertical-scrollbar-markers) – pushpraj Aug 26 '14 at 02:34
  • Graph control will sometimes have static content, sometimes will be used to draw dynamic plot data (updated several times per second). I thought about caching already. My first idea was to cache geometry, but then I realized it will only take few `ms`, while mouse move lag is in the range of hundreds `ms`. Yesterday I tried [RenderTargetBitmap](http://msdn.microsoft.com/en-us/library/system.windows.media.imaging.rendertargetbitmap.aspx) to perform async rendering (in another thread). It still lags a lot when moving with the mouse, so really don't know how to apply your bitmap suggestion here. – Sinatr Aug 26 '14 at 06:59
  • generating geometry is not the actual cost of performance, but the rendering and composting with the UI is the one. updating several time per second may not be big issue, if we can optimize it nicely. I am sure we can achieve nice performance in pure wpf too, all we need to find a right combination of the available techniques. do you have a working prototype? may I give a try. – pushpraj Aug 26 '14 at 07:09
  • I really appreciate your help. I've tested `StreamGeometry`, it gives same performance (~`330ms` with `PathGeometry` when resizing window and same numbers with `StreamGeometry`). Thanks for `CacheMode` and `BitmapCache`, I didn't know they exists, but.. or BUT (big *but*). Once it is rendered - applying transformation to it is very cheap, but the rendering itself take same time as before. If, to example, rendering takes `300ms` and I am updating graph (so it needs to be rendered) twice a second, it will be still same *laggy* feeling. But I don't think it will be possible to overcome. – Sinatr Aug 26 '14 at 07:42
  • I found another problem - clipping. If `ClipToBounds` is set, then such tranformation-based graph will not display anything outside of `RenderSize`, it gets clipped and looks cut (but you can still pretty nicely move around). If unset, then I have to handle clipping. How? Other thing is how big can be that `BitmapCache`? What if I draw a graph with just 2 connected point: `(-10000000,-1000000)` and `(+1000000,+1000000)` ? Same goes with `line`: in render-based graph I can calculate where to *clip* line and simply connect points from `0` to `RenderSize`. What in transformation-based one? – Sinatr Aug 26 '14 at 13:00
  • you can clip based on the available size, usually obtained via [FrameworkElement.MeasureOverride](http://msdn.microsoft.com/en-us/library/system.windows.frameworkelement.measureoverride(v=vs.110).aspx). secondly it is not wise to draw that huge geometry, better to apply a clip on the geometry using boolean intersection. [CombinedGeometry](http://msdn.microsoft.com/en-us/library/system.windows.media.combinedgeometry(v=vs.110).aspx) can help you here and then render only the visible area. lastly size of BitmapCache depends on various factors including available memory, operating system etc. – pushpraj Aug 26 '14 at 13:54
  • If I can't render complete geometry, then I will need to render something again when moving graph. Which means I can't use your method, right? I was thinking to cache content of 3x size of current `RenderSize` (to have 1 screen data in each direction), so that when moving graph of that size there is no need to render again until mouse is released. Still I don't understand how to deal with clipping when using cache, could you help? – Sinatr Aug 26 '14 at 14:13
  • Clipping is easy: host graph inside other control, which has `ClipToBounds = true`. Btw, using `CacheBitmap` with low resolution, to example `0.1`, speeds up drawing significantly. Perhaps I could use this effect when there are too many points have to be drawn, do a calculation pass to get approximate amount of points, if it's too big - reduce *quality* somehow. Is it possible to dynamically apply different cache? Or changing cache will cause rendering? – Sinatr Aug 26 '14 at 14:47
  • Noticed your 3d update. I was using `DrawingVisual` in my attempt to cache render. I didn't noticed any difference in rendering speed. At all. And it's also using `CacheBitmap` to have smooth mouse movement. Right now I am playing with dynamic `RenderAtScale`: estimate geometry complexity (atm by counting total length of lines) and if it's too complex - reduce quality (which boost speed significantly). I think this will be an acceptable solution for the end user - if displaying too much, it will be *blurred*. Still have to deal with clipping and re-rendering. And once again,thank for your help – Sinatr Aug 27 '14 at 08:28
  • Typical graph has static and dynamic content. I think several `DrawingVisual` might be a good idea to separate those. Then rendering time might be reduced if only one of visuals is updated. It also fits well with dynamic `RanderAtScale` - if graph is blurred, you don't want to see coordinate numbers (text) getting blurred as well. Really useful idea! – Sinatr Aug 27 '14 at 08:41
  • That's great! I have similar concept in mind, I'll post a sample idea for the same too. – pushpraj Aug 27 '14 at 08:50
2

It's strange and nobody here mentioned, but it is possible to use gdi draw in wpf natively (without hosting container).

I found this question first, which become normal render-based graph (use InvalidateVisuals() to redraw).

protected override void OnRender(DrawingContext context)
{
    using (var bitmap = new GDI.Bitmap((int)RenderSize.Width, (int)RenderSize.Height))
    {
        using (var graphics = GDI.Graphics.FromImage(bitmap))
        {
            // use gdi functions here, to ex.: graphics.DrawLine(...)
        }
        var hbitmap = bitmap.GetHbitmap();
        var size = bitmap.Width * bitmap.Height * 4;
        GC.AddMemoryPressure(size);
        var image = Imaging.CreateBitmapSourceFromHBitmap(hbitmap, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
        image.Freeze();
        context.DrawImage(image, new Rect(RenderSize));
        DeleteObject(hbitmap);
        GC.RemoveMemoryPressure(size);
    }
}

This approach is capable to draw hundred thousands of lines. Very responsive.

Drawbacks:

  • not as smooth, as pure gdi one graph, DrawImage occurs some times after, will flickers a bit.
  • necessary to convert all wpf objects to gdi ones (sometimes is impossible): pens, brushes, points, rectangles, etc.
  • no animations, graph itself can be animated (to example, transformed), but drawings are not.
Community
  • 1
  • 1
Sinatr
  • 20,892
  • 15
  • 90
  • 319