Slow redraws with noisy data in WPF
tevo wrote at 2013-09-12 15:58:
tevo wrote at 2013-09-12 15:58:
I recently discovered a seemingly simple data set that slows down OxyPlot redraws to 1 or 2 FPS. I'm using OxyPlot.WPF 2013.1.67.1. I have a plot that takes up a full window and the window is nearly full screen (1680 x 1050). There is one LineSeries with 500 points generated like so:
const int n = 500;
var points = new List<IDataPoint>(n);
var rng = new System.Random();
for (int i = 0; i < n; i++)
points.Add(new DataPoint(i + 1, rng.NextDouble()));
As I adjust the window size, OxyPlot redraw performance is very slow. This was quite surprising since I've plotted LineSeries with 10,000 points before and had much better performance. My only guess as to why it degrades so much is that the noisy data requires
painting more pixels. Does anyone have a better understanding of why performance is so bad in this case? Even better, is there a way to fix my code or OxyPlot?
tevo wrote at 2013-09-12 15:58:
The code is added to the example library.
You can compare performance in WPF, SL and Windows Forms.
Also see the "Performance" examples using a zig-zag (sawtooth) curve.
Random data are difficult to optimize - all pixels must be drawn.
Are you binding to the data points using reflection?
tevo wrote at 2013-09-12 15:59:
I am not binding to the data points. My xaml just has this:
<oxy:Plot Model="{Binding PlotModel}" Title="OxyPlot" />
Then in C# I create the points and dolineSeries.Points = points;
Regarding optimization: Does OxyPlot already skip data points in some cases or are you suggesting something new?everytimer wrote at 2013-09-12 16:57:
tevo wrote at 2013-09-12 17:05:
everytimer wrote at 2013-09-13 21:15:
tevo wrote at 2013-09-16 17:07:
I profiled (sampling method) a very simple test case and found that the most expensive methods are:
- Canvas.Children.Add() (13%) - called in ShapesRenderContext.Add(), which is called most often from ShapesRenderContext.DrawText()
- Canvas.Children.Clear() (12%) - called in Plot.UpdateVisuals()
- TextBlock.Measure() (11%) - called in ShapesRenderContext.DrawText() and ShapesRenderContext.MeasureText()
There may be another way to address all of these problems at once but it would be a major change to OxyPlot. Instead of clearing the canvas and adding new objects for every update, existing objects could be updated and reused. This would eliminate calls to Canvas.Children.Clear(), reduce calls to Canvas.Children.Add() and TextBlock.Measure(), and reduce the number of objects that get created and destroyed. It would be a big change and I'm not even sure it would be a net win. I'd love to hear some thoughts on this idea, especially from objo.
For now, I'm going to seriously consider everytimer's suggestion to use WinForms. Performance is significantly better and probably acceptable for my app.
Lastly, I found some interesting links while looking into WPF performance. Maybe others will be interested as well...
objo wrote at 2013-09-17 00:03:
I have tried:
- using DrawingContext (not much performance gain and problems with aliasing/antialiasing if I remember correctly)
- disconnect Canvas from visual tree when updating (minor improvement, the DisconnectCanvasWhileUpdating property should be enabled in the Plot control)
- setting CacheMode on the Canvas (does not look good)
Avoiding Add and Clear of the Canvas.Children collection would make a big difference - but it is difficult to avoid this since the order of the elements is important. I think this improvement needs some logic to insert new and remove old elements. I guess this is also needed in the major refactoring (retained graphics mode) you suggest.
Measurement of text should be avoided in the DrawText method, it should be possible to solve this by applying horizontal/vertical alignment only.
FontFamily could be cached - but I am not sure if this is noticeable.
Here is another interesting link:
tevo wrote at 2013-09-17 20:35:
- Enabled DisconnectCanvasWhileUpdating. I couldn't see or measure any change in performance.
- Implemented a FontFamily cache (just like the brush cache). No change.
- Eliminate all calls to TextBlock.Measure() by commenting out the whole if{} block in DrawText(). Still no change!
- Turn off all ticks and tick labels on the axes. Still no change???
I think all of this means that optimizing the existing code just can't make that much of a difference. So what about switching to retained graphics mode? My tests don't give any data about that. Is there some way to test the concept without doing a major rewrite first?
a5r wrote at 2013-09-18 14:29:
http://msdn.microsoft.com/en-us/library/ee230083.aspx
and http://msdn.microsoft.com/en-us/library/ee230085.aspx
tevo wrote at 2013-09-18 16:15:
I also did another test where I let Plot.UpdateModelAndVisuals() run normally the first 10 times (just to get the plot drawn on screen), and after that always immediately returned. This is effectively a retained mode because the canvas and its children get reused for each frame. The plot was not changing at all but redraw performance was identical. Then I turned on the BitmapCache and redraws became nearly instant. The cache could do its job now because the canvas wasn't changing.
My conclusion is that WPF rendering is really slow in some cases, and any plot update requires WPF to render. I don't think there's any way to work around it.
Dang.
tevo wrote at 2013-09-19 16:03:
I couldn't help myself and kept on playing with my simple test plot of random data between 0 and 1. I varied the number of points to find out if performance gradually decreased or if there was a cliff somewhere. On my machine, performance is good up to about 50 points and then degrades rapidly. 200 points is pretty darn bad.
Then for some reason I decided to try two series of 50 points and performance was still good. I added two more series of 50 points and performance only degraded a little. These four series with 200 total points performed much, much better than one series of 200 points. Interesting.
That made me look into what was actually being used to draw these lines. ShapesRenderContext.DrawLine() uses a System.Windows.Shapes.Polyline. I thought that maybe the number of points in a polyline was the limiting factor, so I updated the method to make multiple polylines with a limited number of points. On a test case with 500 points, this change results in a huge speedup. It's at least 10x faster redraws! I experimented with the number of points to allow in a polyline, and I think there was improvement all the way down to about 16.
So for my test case this change is unquestionably a huge improvement. I tested "LineSeries, 100k points" from the ExampleBrowser and unfortunately my change degraded performance a bit. I think that's because 100k / 16 = 6250 polylines which is a lot. I tried to come up with a way to balance the number of points per polyline and the number of polylines and it seems to be working well now. Here's the updated DrawLine() method:
public void DrawLine(
IList<ScreenPoint> points,
OxyColor stroke,
double thickness,
double[] dashArray,
OxyPenLineJoin lineJoin,
bool aliased)
{
// balance the number of points per polyline and the number of polylines
var numPointsPerPolyline = Math.Max(points.Count / MaxPolylinesPerLine, MinPointsPerPolyline);
var polyline = new Polyline();
this.SetStroke(polyline, stroke, thickness, lineJoin, dashArray, aliased);
var pc = new PointCollection(numPointsPerPolyline);
foreach (var p in points)
{
pc.Add(p.ToPoint(aliased));
// use multiple polylines with limited number of points to improve WPF performance
if (pc.Count >= numPointsPerPolyline)
{
polyline.Points = pc;
this.Add(polyline);
// start a new polyline at last point so there is no gap
polyline = new Polyline();
this.SetStroke(polyline, stroke, thickness, lineJoin, dashArray, aliased);
var startPoint = pc.Last();
pc = new PointCollection(numPointsPerPolyline);
pc.Add(startPoint);
}
}
if (pc.Count > 1 || points.Count == 1)
{
polyline.Points = pc;
this.Add(polyline);
}
}
And of course you also need to define:/// <summary>
/// The maximum number of polylines per line.
/// </summary>
const int MaxPolylinesPerLine = 64;
/// <summary>
/// The minimum number of points per polyline.
/// </summary>
const int MinPointsPerPolyline = 16;
It was interesting to find that there is already a MaxFiguresPerGeometry variable. It is there there to "limit the number of figures, otherwise drawing errors...", but it seems like a similar thing.
Anyway, are there other things to think about and test or can this go straight into official OxyPlot?
objo wrote at 2013-09-19 23:20:
The only problem is dashed lines, I think. See the new example.
Can you calculate the correct dash offset?
objo wrote at 2013-09-19 23:55:
- not clear the canvas, instead set a child element index = 0
- when creating/adding: check if the canvas contains an element of the given type at the current index, reuse the element if type matches
- otherwise create new instance and replace or add to the canvas
- when finished, remove unused elements from canvas
tevo wrote at 2013-09-20 16:44:
I took a crack at calculating dash offset and it's trickier than I expected. Maybe that's because I'm not sure how Shape.StrokeDashOffset works. MSDN documentation doesn't give details but I did find a book that explains things more. Below is my ugly test code that doesn't quite work yet.
Note that this code does not even try to handle a dashArray with odd length. That new example you added should probably have two lines to verify that even and odd length dash arrays work properly.
public void DrawLine(
IList<ScreenPoint> points,
OxyColor stroke,
double thickness,
double[] dashArray,
OxyPenLineJoin lineJoin,
bool aliased)
{
// balance the number of points per polyline and the number of polylines
var numPointsPerPolyline = Math.Max(points.Count / MaxPolylinesPerLine, MinPointsPerPolyline);
double lineLength = 0;
double dashPatternLength = (dashArray != null) ? dashArray.Sum() : 0;
double[] dashArrayCumsum = null;
if (dashArray != null)
{
dashArrayCumsum = new double[dashArray.Length + 1];
for (int i = 0; i < dashArray.Length; i++)
{
dashArrayCumsum[i + 1] = dashArrayCumsum[i] + dashArray[i];
}
}
var polyline = this.CreateAndAdd<Polyline>();
this.SetStroke(polyline, stroke, thickness, lineJoin, dashArray, 0, aliased);
var pc = new PointCollection(numPointsPerPolyline);
var n = points.Count;
var last = new Point();
for (int i = 0; i < n; i++)
{
var p = points[i].ToPoint(aliased);
pc.Add(p);
if (dashArray != null)
{
if (i == 0)
{
last = p;
}
var delta = p - last;
var dist = Math.Sqrt((delta.X * delta.X) + (delta.Y * delta.Y));
lineLength += dist * thickness;
lineLength %= dashPatternLength;
last = p;
}
// use multiple polylines with limited number of points to improve WPF performance
if (pc.Count >= numPointsPerPolyline)
{
polyline.Points = pc;
if (i < n - 1)
{
// start a new polyline at last point so there is no gap
polyline = this.CreateAndAdd<Polyline>();
double dashOffset = 0;
if (dashArray != null)
{
for (int k = 1; k < dashArrayCumsum.Length; k++)
{
if (lineLength <= dashArrayCumsum[k])
{
dashOffset = (k - 1) + (lineLength - dashArrayCumsum[k - 1]) / (dashArrayCumsum[k] - dashArrayCumsum[k - 1]);
break;
}
}
}
this.SetStroke(polyline, stroke, thickness, lineJoin, dashArray, dashOffset, aliased);
pc = new PointCollection(numPointsPerPolyline) { pc.Last() };
}
}
}
if (pc.Count > 1 || n == 1)
{
polyline.Points = pc;
}
}
objo wrote at 2013-09-21 15:25:
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Canvas> <Line X1="100" Y1="100" X2="800" Y2="100" Stroke="Black" StrokeThickness="2" StrokeDashArray="10,5,20,10,30,15"/> <Line X1="110" Y1="110" X2="800" Y2="110" Stroke="Black" StrokeThickness="2" StrokeDashArray="10,5,20,10,30,15" StrokeDashOffset="5"/> <Line X1="130" Y1="120" X2="800" Y2="120" Stroke="Black" StrokeThickness="2" StrokeDashArray="10,5,20,10,30,15" StrokeDashOffset="15"/> <Line X1="150" Y1="130" X2="800" Y2="130" Stroke="Black" StrokeThickness="2" StrokeDashArray="10,5,20,10,30,15" StrokeDashOffset="25"/> <Line X1="250" Y1="140" X2="800" Y2="140" Stroke="Black" StrokeThickness="2" StrokeDashArray="10,5,20,10,30,15" StrokeDashOffset="75"/> <Line X1="350" Y1="150" X2="800" Y2="150" Stroke="Black" StrokeThickness="2" StrokeDashArray="10,5,20,10,30,15" StrokeDashOffset="125"/> </Canvas> </Page>
tevo wrote at 2013-09-24 14:58:
http://i.imgur.com/IHc3PYh.png
Unfortunately, I also discovered a problem with using multiple Polylines to draw one line: LineJoins. The places where one Polyline ends and another begins can have artifacts. Here are some examples.
- http://i.imgur.com/fkV5Wxo.png (look in a vertical line under the red arrow)
- http://i.imgur.com/VHnSQkF.png (points highlighted in yellow)
My only idea to overcome this issue is for Polylines to overlap by two points. That way the end of a Polyline will be covered by the first LineJoin of the next line. This might lead to other problems, however. If the line color is semi-transparent, the overlapping parts would appear to be a different color.
Any other ideas about what to do here?
Here's my current code:
public void DrawLine(
IList<ScreenPoint> points,
OxyColor stroke,
double thickness,
double[] dashArray,
OxyPenLineJoin lineJoin,
bool aliased)
{
// balance the number of points per polyline and the number of polylines
var numPointsPerPolyline = Math.Max(points.Count / MaxPolylinesPerLine, MinPointsPerPolyline);
var polyline = this.CreateAndAdd<Polyline>();
this.SetStroke(polyline, stroke, thickness, lineJoin, dashArray, 0, aliased);
var pc = new PointCollection(numPointsPerPolyline);
var n = points.Count;
double lineLength = 0;
var dashPatternLength = (dashArray != null) ? dashArray.Sum() : 0;
var last = new Point();
for (int i = 0; i < n; i++)
{
var p = points[i].ToPoint(aliased);
pc.Add(p);
if (dashArray != null)
{
if (i > 0)
{
var delta = p - last;
var dist = Math.Sqrt((delta.X * delta.X) + (delta.Y * delta.Y));
lineLength += dist;
}
last = p;
}
// use multiple polylines with limited number of points to improve WPF performance
if (pc.Count >= numPointsPerPolyline)
{
polyline.Points = pc;
if (i < n - 1)
{
// start a new polyline at last point so there is no gap
polyline = this.CreateAndAdd<Polyline>();
var dashOffset = (dashPatternLength > 0) ? (lineLength / thickness) % dashPatternLength : 0;
this.SetStroke(polyline, stroke, thickness, lineJoin, dashArray, dashOffset, aliased);
pc = new PointCollection(numPointsPerPolyline) { pc.Last() };
}
}
}
if (pc.Count > 1 || n == 1)
{
polyline.Points = pc;
}
}
objo wrote at 2013-09-24 19:18:
I suggest we add a property on the Wpf Plot control: LineDrawingMode { HighQuality, HighSpeed }.
tevo wrote at 2013-09-25 19:46:
In my earlier tests I didn't see artifacts in narrow lines. Perhaps we should always use "HighSpeed" mode when thickness is less than some value, say 3.5.
objo wrote at 2013-09-25 21:51:
I added a "BalancedLineDrawingThicknessLimit" property. The default value is 3.5, use 0 to get old method.
Can you send the code for the "Dashed Line test" example?
tevo wrote at 2013-09-25 22:58:
Here's the dashed line test code:
[Example("Dashed line test")]
public static PlotModel DashedLineTest()
{
var model = new PlotModel("Dashed line test");
for (int y = 1; y <= 24; y++)
{
var line = new LineSeries()
{
StrokeThickness = y,
LineStyle = LineStyle.Dash,
Dashes = new double[] { 1, 2, 3 } // has no effect
};
for (int i = 0; i < 20; i++)
line.Points.Add(new DataPoint(i + 1, y));
model.Series.Add(line);
}
return model;
}
objo wrote at 2013-09-25 23:32:
The NoisyData example seems to be much faster (10x ?).
tevo wrote at 2013-09-26 15:14:
Customer support service by UserEcho