Skip to content

Speeding up PlotCurveItem: QPainterPath caching, appending, pixel buffer caching, array diffing #1478

@goodboy

Description

@goodboy

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:

  1. 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()
  1. 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
  1. numpy array data input downsampling
  1. 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

Hopefully this wasn't too long winded 🏄🏼

Metadata

Metadata

Assignees

No one assigned

    Labels

    performanceProblems or questions related to speed, efficiency, etc.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions