I've been doing some involved work trying to speed up real-time updating of large PlotCurveItem (and by side effect PlotDataItem) graphics and have been having some good breakthroughs that I think now should be brought upstream and discussed in more detail. Given that we're moving away from old Python (#1473) and embracing newer versions of Qt I think it's time to start pushing hard at making this library a benchmark for interactive data viz.
There's a variety of tricks and techniques that are used internally that give pyqtgraph very good performance within the graphics view framework already, but I believe there is much more juice to squeeze.
I'll start with a layout of a bunch of information I've acquired throughout the process of making some speed improvements in some of my own project work. Some of this may be duplicate from some issues in a project I am helping with: pikers/piker#124, pikers/piker#109. Briefing these issues is not required reading but may contain further links of value as we move towards adopting newer tech for fast graphics inside Qt.
Premises of our current performance
pyqtgraph seems to try to obtain the bulk of it's fast 2d line graphics (without the use of GPUs) from 4 techniques:
- use of
QPainterPath and in particular it's ability to generate graphics very fast inside a C++ tight loop using the stream write operator; the C++ implementation for this is found here
- this method is used inside
functions.arrayToQPath(), of particular note is that a user is free to use this function to generate their own path if they understand the binary protocol
- we can definitely speed this routine up using
numba (I know because I've done it) which will give both large and small array -> path generation a boost
- currently we are not using
QGraphicsItem's cache mode supposedly because it hurts performance - my manual tests definitely show this is not true under certain uses of QPainterPath and in fact improves interactivity latency by miles if the path is not fully re-rendered / re-drawn every update cycle (more on this shortly)
- enabling
DeviceCoordinateCache mode prevents the .paint() routine from being called on mouse interaction and instead only triggers it on transformations other then moving (as is specified in the docs).
- we currently have no facilities to update
PlotCurveItem.path only on input array changes. Currently every .paint() call regenerates the entire path. Ideally we should have support for this using QPainterPath.addPath() and doing path object reuse instead of numpy array caching to append to the existing graphics on updates:
numpy array caching is solving the wrong problem (not that we couldn't improve this - input data diffing can be done fast with numba to feed piecewise/incremental path updates); generating the path is generally speaking faster then drawing the path using QPainter.drawPath() inside .paint()
- Use of
numpy array data bounds tracking
- though I understand the emphasis on keeping the bounds cache up to date and fast (for the purposes of making
.boundingRect() super fast), the irony here is that computing the br using QPainterPath.boundingRect() is already quite fast and can be further sped up using .controlPointRect() and the latency here is already magnitudes faster (without using any bounds caching) then QPainter.drawPath()
- I might even go further and say the added overhead of the Python based bounds cache is negligible compared to the speed of
QPainterPath.controlPointRect() and we can likely just drop all this code in the long run
numpy array data input downsampling
- input
numpy array "clipping"
- again, the funny thing here is that I'm pretty sure
QPainterPath already does this internally (and especially so when using a cached mode) so really we may not even need this (as it's just extra overhead in that case) as long as it's not required for scatter plots
Near-term proposed solution
Looking to the future at OpenGL options
- we're very very behind current
Qt apis for GL stuff, see the following:
- I would suggest that we maybe think more seriously about the port to
vispy which might not actually be that painful if we look at abstracting some of our apis
Hopefully this wasn't too long winded 🏄🏼
I've been doing some involved work trying to speed up real-time updating of large
PlotCurveItem(and by side effectPlotDataItem) graphics and have been having some good breakthroughs that I think now should be brought upstream and discussed in more detail. Given that we're moving away from old Python (#1473) and embracing newer versions ofQtI think it's time to start pushing hard at making this library a benchmark for interactive data viz.There's a variety of tricks and techniques that are used internally that give
pyqtgraphvery good performance within the graphics view framework already, but I believe there is much more juice to squeeze.I'll start with a layout of a bunch of information I've acquired throughout the process of making some speed improvements in some of my own project work. Some of this may be duplicate from some issues in a project I am helping with: pikers/piker#124, pikers/piker#109. Briefing these issues is not required reading but may contain further links of value as we move towards adopting newer tech for fast graphics inside
Qt.Premises of our current performance
pyqtgraphseems to try to obtain the bulk of it's fast 2d line graphics (without the use of GPUs) from 4 techniques:QPainterPathand in particular it's ability to generate graphics very fast inside aC++tight loop using the stream write operator; theC++implementation for this is found herefunctions.arrayToQPath(), of particular note is that a user is free to use this function to generate their own path if they understand the binary protocolnumba(I know because I've done it) which will give both large and small array -> path generation a boostQGraphicsItem's cache mode supposedly because it hurts performance - my manual tests definitely show this is not true under certain uses ofQPainterPathand in fact improves interactivity latency by miles if the path is not fully re-rendered / re-drawn every update cycle (more on this shortly)DeviceCoordinateCachemode prevents the.paint()routine from being called on mouse interaction and instead only triggers it on transformations other then moving (as is specified in the docs).PlotCurveItem.pathonly on input array changes. Currently every.paint()call regenerates the entire path. Ideally we should have support for this usingQPainterPath.addPath()and doing path object reuse instead ofnumpyarray caching to append to the existing graphics on updates:numpyarray caching is solving the wrong problem (not that we couldn't improve this - input data diffing can be done fast withnumbato feed piecewise/incremental path updates); generating the path is generally speaking faster then drawing the path usingQPainter.drawPath()inside.paint()QPainterPath.addPath()can be used for both prepending and appending new path graphics from data diffs - see this working code for OHLC sampled bars as referencenumpyarray data bounds tracking.boundingRect()super fast), the irony here is that computing the br usingQPainterPath.boundingRect()is already quite fast and can be further sped up using.controlPointRect()and the latency here is already magnitudes faster (without using any bounds caching) thenQPainter.drawPath()QPainterPath.controlPointRect()and we can likely just drop all this code in the long runnumpyarray data input downsamplingPlotDataItemI presume because they also work on scatter plots?numpyarray "clipping"QPainterPathalready does this internally (and especially so when using a cached mode) so really we may not even need this (as it's just extra overhead in that case) as long as it's not required for scatter plotsNear-term proposed solution
numba-izearrayToQPath()and downsampling routines in some new modules and offer these versions as an extension / optional dependencyPlotCurveItemsupport input array diffing somehow (likely also usingnumba) and use that to drive fasterQPainterPathgeneration, reuse, update and cachingQGraphicsItemcache mode (tactfully with user controls) iff we have cached paths from the point aboveLooking to the future at OpenGL options
Qtapis for GL stuff, see the following:pyqtgraphhas internal support for the old API which needs to be updated and tested withQtGui.QOpenGLWidgetin the graphics scene code here (which I've tested but haven't had a GPU rich machine to see if it's better yet)vispywhich might not actually be that painful if we look at abstracting some of our apisHopefully this wasn't too long winded 🏄🏼