Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generating plots with many lines is slow #1039

Open
kangalio opened this issue Sep 20, 2019 · 14 comments · May be fixed by #2777
Open

Generating plots with many lines is slow #1039

kangalio opened this issue Sep 20, 2019 · 14 comments · May be fixed by #2777
Labels
performance Problems or questions related to speed, efficiency, etc.

Comments

@kangalio
Copy link

Short description

Plotting lots of curves in a single plot (>500 lines) is really slow, i.e. the .plot(x, y) calls take a lot of time to complete. Also, navigating the resulting plot is really sluggish.

It's not just the number of data points; when plotting the same number of data points with a single line, performance is good. That's also visible in the code to reproduce

Code to reproduce

This opens two plots with the same number of data points - but the first one consists of 1000 small lines while the second plot consists of just one long line.

The above-described performance issues are clearly noticable with this code on my machine.

import pyqtgraph as pg
import numpy as np

print("Generating 1000 lines with 50 data points each (total 50,000 data points)..")
win1 = pg.plot()
for i in range(1000):
	win1.plot(np.arange(50), np.random.random(50))

print("Generating one line with 50,000 data points..")
win2 = pg.plot()
win2.plot(np.arange(50000), np.random.random(50000))

print("Done")

pg.QtGui.QApplication.exec_()

Expected behavior

Both generating and navigating plots with many lines should be as performant as usual in PyQtGraph.

Real behavior

Generating and navigating plots with many lines is slow and sluggish.

Tested environment(s)

  • PyQtGraph version: 0.10.0
  • Qt Python binding: PyQt5 5.13.1 Qt 5.13.1
  • Python version: 3.7.4
  • NumPy version: 1.16.4
  • Operating system: Antergos Linux 5.2.14
  • Installation method: pip3

Additional context

Am I using the library wrong? Or maybe there is a workaround?

@ixjlyons
Copy link
Member

Basically, each curve is an object where painting involves the overhead of calling a Python method, so lots of curves means lots of Python callbacks. The lines of a single curve are held in a cached QPainterPath and handed to a QPainter which iterates over the points to do rendering work in a low-level compiled (and I'd imagine fairly optimized) loop. As it stands now, pyqtgraph isn't really set up to display more than a few dozen PlotCurveItems without starting to feel it, and this is what you're seeing.

There are the enableExperimental and useOpenGL config options to use the Python OpenGL library for painting, but it won't do much in this case. The workaround is essentially to find some other way to represent the data you're trying to visualize -- there aren't many situations I can think of where you'd need 1000 curves on a plot all at once. If you do have a ton of data, you might build up a GUI that allows you to interactively select subsets. Or do pre-computation to show an approximated form like an average with confidence interval bounds.

As for a proposed solution/improvement: I could imagine PlotCurveItem (or a subclass) accepting a 2D x/y arrays, generating multiple QPainterPath objects, and then painting them all in one callback. I think that would improve performance over having many individual PlotCurveItems, but I'm not sure how much and it wouldn't match plotting all the points in one curve.

Also, I wouldn't be surprised if this has come up either here in the issues or on the mailing list. I can take a look around later to see if this has been discussed before.

@ixjlyons ixjlyons added the performance Problems or questions related to speed, efficiency, etc. label Sep 20, 2019
@kangalio
Copy link
Author

kangalio commented Sep 20, 2019

Thanks for the detailed explanation and possible solutions.

One thing I noticed that may be interesting: I tried drawing the lines in real time, i.e. call QApplication.processEvents() after every 20th line drawn to get a live view. While the lines were drawn one by one, I panned just a little, and with it the rendering suddenly accelerated greatly. That effect faded out after a few seconds, but panning again gave another temporary speed boost.

I'm currently in the process of doing some measurements, but the results are pretty clear already. A little drag every 60 seconds speed up the total rendering time by a factor of 14 (!). Can that behavior be explained somehow and is that information of any use?

EDIT: Benchmarks

@ixjlyons
Copy link
Member

Not sure off-hand what would cause that.

Out of curiosity I hacked together a sort of minimal implementation to test the improvement if you paint multiple paths in one paint callback like I described above. It turned out to be pretty easy but it's a just-make-it-work effort: ixjlyons@3fd61b0

Demo script:

import pyqtgraph as pg
import numpy as np

pg.plot(np.arange(50), np.random.random((1000, 50)))
pg.mkQApp().exec_()

So it's a lot better, but I'd still want to understand the usecase(s) to consider implementing it properly and adding it in. Usually you want different drawing options for each curve, different x values for each one, etc. You could implement a PlotCurveGroupItem or somesuch, but just calling plot() multiple times works fine until you get so many curves that they aren't really distinguishable anyway.

@kangalio
Copy link
Author

That's really nice, thank you for the effort. I'm probably going to do some sort of downsampling as a workaround, maybe also look into the PyQtGraph source code a bit to get an idea of what's going on (though I don't think I'm skilled enough to orientate myself and work on a PlotCurveGroupItem or similar)

@warrenbocphet
Copy link

I ran into the same issue, I hope this get improve one day :).

@2xB
Copy link
Contributor

2xB commented May 9, 2020

For issue #1193, I made a demonstration of how to use the connect keyword in functions plot and setData to plot and update multiple lines with a single call to one of those functions. This lets you instantly plot large amounts of lines. See the issue for a minimal working example.

Since this library is about performance, maybe we should expose this more prominently in the documentation and examples or adapt the library so the user does not have to care about this, e.g. by automatically merging PlotDataItems before plotting. Since the latter one is probably quite difficult with all the possible style options of a PlotDataItem, probably the documentation and examples would be a way to go?

@mbudris
Copy link

mbudris commented Dec 14, 2020

Try setting ranges of your plot:

plot.setYRange(0, nPlots)
plot.setXRange(0, nSamples)

It sped my case dramatically.
By accident I found this playing with MultiPlotSpeedTest.py example and removing any of those lines reduced the fps by a hundredfold.

Hope it helps someone.

@goodboy
Copy link

goodboy commented Dec 23, 2020

Try setting ranges of your plot

I was going to say some of this might be the axis contents generation.

Another thing to try is #1418 which for me makes the single line case work decently well.

I think we need a new issue that's a request to speed up PlotCurveItem in general.
This is something I will be working on shortly.

@j9ac9k
Copy link
Member

j9ac9k commented Nov 12, 2021

@kangalioo Over 0.12.x cycle we've made some massive performance improvements to our line plot capabilities; I know it's been a while since this issue was brought up, but if you can see if current versions work to your performance needs, that would be appreciated 👍🏻

@kangalio
Copy link
Author

Thank you for the info.

Performance feels unchanged, unfortunately, both on 0.12.3 and on master (bc4d40a). I ran the reproducible code snippet in the first comment.

@j9ac9k
Copy link
Member

j9ac9k commented Nov 12, 2021

I made a slight modification to your code to the following for benchmark purposes:

import pyqtgraph as pg
import numpy as np

app = pg.mkQApp()

lines = 1000

xs_short = np.arange(50)
ys_short = np.random.random(size=(lines, 50))

xs_long = np.arange(50_000)
ys_long = np.random.random(50_000)

win1 = pg.plot()
win2 = pg.plot()

profiler = pg.debug.Profiler(disabled=False, delayed=False)
for i in range(1000):
	win1.plot(xs_short, ys_short[i,:])
profiler("Finished adding 1000 plots of 50 points")
win2.plot(xs_long, ys_long)
profiler("Finished adding 1 line of 50000 points")

pg.QtGui.QApplication.exec_()

On 0.11.0 I get

  Finished adding 1000 plots of 50 points: 38378.3369 ms
  Finished adding 1 line of 50000 points: 3.6287 ms

On master I get:

  Finished adding 1000 plots of 50 points: 16527.3832 ms
  Finished adding 1 line of 50000 points: 5.7249 ms

That's puzzling why it's faster for the single line on 0.11.0; but regardless we're seeing a 2-2.5x speedup of many lines of 50 points.

That said, I'm not seeing a good way to get the two down to be closer together; adding many lines has significantly more operations than just adding one bigger line.

@kangalio
Copy link
Author

Oh there seems to be a misunderstanding: I was talking about performance of navigating the plots

output-2021-11-13_00.20.01.mp4

.

@j9ac9k
Copy link
Member

j9ac9k commented Nov 12, 2021

oh! 🤦🏻 that makes a bit more sense.

@j9ac9k
Copy link
Member

j9ac9k commented Nov 12, 2021

yeah we're calling paint for each PlotCurveItem in the plot here, instead of one paint call for the one plot; I'm not sure of a good way to speed that up (potentially the openGL renderer, but that has tons of other potential downsides).

Suppose this isn't a bad area to investigate with respect to performance improvements, recently there have been substantial improvements to .setData calls; but the redraw bits; I don't have a good feeling for how efficient/inefficient we are and what we can do to further improve things in this regard.

EDIT: and now I took an extra 2 minutes to read through the comments above, yeah @ixjlyons 's solution is absolutely the correct one here; I'm not sure if a good way to incorporate this into the library without other side-effects (being able to change pen/color and so on).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
performance Problems or questions related to speed, efficiency, etc.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants