Performance with multiple LinearAxis and many LineSeries

Jan 18, 2013 at 9:04 PM

I have a plot that can only redraw at about 5 Hz and I'm looking for ideas on how to speed it up. My plot has two vertical LinearAxis, one on the lower half and one on the upper half of my plot. I plot six LineSeries on each LinearAxis. The LineSeries have 1,000 points each. I compile with optimizations enabled.

I tried turning off grid lines and making all the LineStyles = Solid. (One line always ends up being dashed anyway for some reason.) Performance hardly changed.

I tried putting six lines on the bottom axis and none on the top. Performance improved, but not by much.

I tried just having one vertical axis with six lines, and performance was similar to the previous test.

Some of the OxyPlot demos have a single line of 100k points and those are nice and quick compared to my plot. At this point is seems like the bottleneck is just having multiple lines. Does anyone have suggestions for how to get my plot to redraw faster? Thanks!

P.S. Also thanks in general to the OxyPlot devs. It's a great tool!

Jan 21, 2013 at 7:22 PM

I did some more experiments and noticed something interesting. I think redraw rate is inversely proportional to the number of pixels "painted" by lines and markers.

This effect is clearly shown by the "LineSeries 2" examples in the Example Browser ("Performance" section). These plots have y values that alternate between zero and one, which ends up painting the line on nearly every pixel of the plot. I'm getting frame rate numbers of 4-6 Hz, which is similar to what I observed on my plot. My data set was also similar to these examples, with most of the plot pixels painted by a line.

So it seems that this behavior is known, and there's probably not an easy way to speed things up. I'd love to be proven wrong...  :)

Coordinator
Jan 22, 2013 at 8:28 AM

The LineSeries has a property "MinimumSegmentLength" (defined in screen coordinate units) that can be used to reduce the resolution of the plotted curves. Try to increase the number if you don't mind less resolution. 

I think it should be possible to reduce the number of line segments rendered also when the curve is alternating between two values, but I have not tried to implement this yet.

Also see http://oxyplot.codeplex.com/workitem/9989 if you are on WPF

Jan 22, 2013 at 3:33 PM

Thanks for the tips! I am using WPF, so that link you provided is interesting. I tried making those changes and compiling myself. I'm not positive that I did it completely correctly, but I did see the refresh rate improve from 5 to 7 Hz. Panning performance doubled from about 5 to 10 Hz. Is there a reason that OxyPlot hasn't been updated with this change yet?

Increasing "MinimumSegmentLength" also makes a small difference, but even with extreme values my plot refresh rate improves only by 2 Hz. It's not worth it for me.

My data is not actually alternating between two values, it is just noisy. It's pretty much normally distributed. The result is the same though, because most of the plot ends up being covered by line segments. Maybe there is a way to figure out that some segments don't need to be drawn because they are completely covered by other segments.

Jan 22, 2013 at 4:30 PM

I just found something else interesting: increasing LineSeries.StrokeThickness improves refresh rate. Small numbers like 5 improve refresh just a little, but if I use a really large number like 30, my plot is significantly faster. Any theories as to why that happens?

Coordinator
Jan 22, 2013 at 4:36 PM

Yes, I will look at issue 9989 soon, it has been in my backlog for a long time now.

I don't know how to reduce the number of points to render for a noisy line series, good ideas are welcome!

Can you run your application in a profiler to check if the bottleneck is in OxyPlot (while updating data or while updating the visual model) or in the WPF rendering core?

Jan 22, 2013 at 6:18 PM

Just to be clear, because I didn't state it before: the way I've been testing refresh rate is to make a plot, then grab the corner of the window and shake my mouse. OxyPlot redraws the same plot over and over at different sizes, but nothing else about the plot is changing at all.

I profiled while doing that same test on the "LineSeries 2" examples in the Example Browser ("Performance" section)I think that the profiler is telling me that the bottleneck is WPF. Here are the top functions for inclusive samples:

100% System.Windows.Application.Run(class System.Windows.Window)
100% ExampleBrowser.App.Main()
 18% OxyPlot.Wpf.Plot.OnCompositionTargetRendering(object,class System.Windows.Media.RenderingEventArgs)
 18% OxyPlot.Wpf.Plot.UpdateModelAndVisuals(bool)
 18% OxyPlot.Wpf.Plot.UpdateVisuals()
 18% OxyPlot.Wpf.WeakEventListener`2.System.Windows.IWeakEventListener.ReceiveWeakEvent(class System.Type,object,class System.EventArgs)
 18% OxyPlot.Wpf.WeakEventManagerBase`1.Handler(object,class System.EventArgs)
 18% System.Windows.WeakEventManager.DeliverEvent(object,class System.EventArgs)
 13% OxyPlot.PlotModel.Render(class OxyPlot.IRenderContext)
  6% OxyPlot.Axis.Render(class OxyPlot.IRenderContext,class OxyPlot.PlotModel,valuetype OxyPlot.AxisLayer,int32)
  6% OxyPlot.HorizontalAndVerticalAxisRenderer.Render(class OxyPlot.Axis,int32)
  6% OxyPlot.PlotModel.RenderAxes(class OxyPlot.IRenderContext,valuetype OxyPlot.AxisLayer)

And here are the top functions for exclusive samples:

100% System.Windows.Application.Run(class System.Windows.Window)
  4% System.Windows.UIElement.Measure(valuetype System.Windows.Size)
  4% System.Windows.Controls.UIElementCollection.Clear()
  1% System.Windows.Controls.UIElementCollection.Add(class System.Windows.UIElement)
  1% System.Windows.DependencyObject.SetValue(class System.Windows.DependencyProperty,object)
Jan 28, 2013 at 7:58 AM

I took the Sample LineSeries 2 - miter line joins, but moved the RefreshPlot to another thread:

// --------------------------------------------------------------------------------------------------------------------
// <copyright file="MainWindow.xaml.cs" company="OxyPlot">
//   The MIT License (MIT)
//
//   Copyright (c) 2012 Oystein Bjorke
//
//   Permission is hereby granted, free of charge, to any person obtaining a
//   copy of this software and associated documentation files (the
//   "Software"), to deal in the Software without restriction, including
//   without limitation the rights to use, copy, modify, merge, publish,
//   distribute, sublicense, and/or sell copies of the Software, and to
//   permit persons to whom the Software is furnished to do so, subject to
//   the following conditions:
//
//   The above copyright notice and this permission notice shall be included
//   in all copies or substantial portions of the Software.
//
//   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
//   OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
//   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
//   IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
//   CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
//   TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
//   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// </copyright>
// <summary>
//   Interaction logic for MainWindow.xaml
// </summary>
// --------------------------------------------------------------------------------------------------------------------
namespace ExampleBrowser
{
using System;
using System.Diagnostics;
using System.Threading;
using System.Windows;
using System.Windows.Media;

    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        /// <summary>
        /// The frame count.
        /// </summary>
        private int frameCount;

        /// <summary>
        /// The vm.
        /// </summary>
        private MainWindowViewModel vm = new MainWindowViewModel();

        /// <summary>
        /// The watch.
        /// </summary>
        private Stopwatch watch = new Stopwatch();

        /// <summary>
        /// Initializes a new instance of the <see cref="MainWindow"/> class.
        /// </summary>
        public MainWindow()
        {
            this.InitializeComponent();
            this.DataContext = this.vm;
            StartWorkerThreads();
            InitSyncEvents();
            CompositionTarget.Rendering += this.CompositionTargetRendering;
            this.watch.Start();
        }

        /// <summary>
        /// Handles the Rendering event of the CompositionTarget control.
        /// </summary>
        /// <param name="sender">
        /// The source of the event.
        /// </param>
        /// <param name="e">
        /// The <see cref="System.EventArgs"/> instance containing the event data.
        /// </param>
        private void CompositionTargetRendering(object sender, EventArgs e)
        {
            this.frameCount++;
            if (this.watch.ElapsedMilliseconds > 1000 && this.frameCount > 1)
            {
                this.vm.FrameRate = this.frameCount / (this.watch.ElapsedMilliseconds * 0.001);
                this.frameCount = 0;
                this.watch.Restart();
            }

            if (this.vm.MeasureFrameRate)
            {
                evtRefreshPlot.Set();
            }
        }

        private Thread RefreshThread;

        private void RefreshPlot()
        {
            while (true)
            {
                WaitHandle.WaitAny(this.EvtHandleArrayRefreshPlot);
                if (this.EvtRefreshPlot.WaitOne(0, false))
                {
                    this.EvtRefreshPlot.Reset();
                    this.Plot1.RefreshPlot(true);
                }
            }
        }

        /// <summary>Gets access to attribute function</summary>
        private EventWaitHandle EvtRefreshPlot
        {
            get { return this.evtRefreshPlot; }
        }

        /// <summary>Gets access to attribute function</summary>
        private EventWaitHandle EvtRefreshPlotThreadExit
        {
            get { return this.evtRefreshPlotThreadExit; }
        }

        /// <summary>Gets access to attribute function</summary>
        private WaitHandle[] EvtHandleArrayRefreshPlot
        {
            get { return this.evtHandleArrayRefreshPlot; }
        }

        /// <summary>
        /// Initializes the worker-thread used RefreshPlot. 
        /// </summary>
        /// <returns>true on success</returns>
        private bool StartWorkerThreads()
        {
            try
            {
                // --> create handle for thread to transmit data
                this.RefreshThread = new Thread(this.RefreshPlot);
                this.RefreshThread.Name = "Performance_Thread";
                this.RefreshThread.IsBackground = true;
                this.RefreshThread.Priority = ThreadPriority.BelowNormal;
                this.RefreshThread.Start();
                return true;
            }
            catch 
            {
                return false;
            }
        }

        /// <summary>Threadsyncevent to signal to draw new measurement values</summary>
        private EventWaitHandle evtRefreshPlot;

        /// <summary>Handle to manage access to Refresh plot</summary>
        private WaitHandle[] evtHandleArrayRefreshPlot;

        /// <summary>Threadsyncevent for signal exit of RefreshPlot-Thread</summary>
        private EventWaitHandle evtRefreshPlotThreadExit;

        /// <summary>
        /// Initialize synchonization events
        /// </summary>
        private void InitSyncEvents()
        {
            // 
            // Event which indicates that new values should be painted
            this.evtRefreshPlot = new ManualResetEvent(false);
            // Event which causes the worker-thread SignalDraw to quit
            this.evtRefreshPlotThreadExit = new ManualResetEvent(false);

            // --> for each thread a WaitHandle-struct is used - so a thread can wait
            //     for different events.
            // WaitHandle for SignalDraw
            this.evtHandleArrayRefreshPlot = new WaitHandle[2];
            this.evtHandleArrayRefreshPlot[0] = this.evtRefreshPlot;
            this.evtHandleArrayRefreshPlot[1] = this.evtRefreshPlotThreadExit;
        }
So I got a frame rate from about 150 Hz when I do nothing else and about 45 Hz, if I do the same tevo did (grab the corner of the window and shake my mouse).
But the updates then are not longer smooth.
Now, for my application this is not the big problem, because I have a static size of my plot. But I need many updates of my lineseries.
So to come not in conflict at updating the collection of the lineseries at refresh the plot I use thread monitor objects.
At this time I saw, the RefreshPlot is "slow" instead of the update cylce of the collections (when I use maximal update cycle based on CompositeTargetRendering.
Now I tried to build a method to only update the changed lineseries in the plot without updating all around (axes, labels, ...) but I fail (I tried to directly call the UpdateVisuals).
Maybe I overlooked the correct access to the method.
Can you give me a hint for this? Thx