Performance issues with pyqtgraph

Viewed 3396

I'm trying to convert some code from using matplotlib to pyqtgraph since it's supposed to be much more efficient, and it provides a lot more interactive features. I've got the code mostly converted, but I'm running into issues where it just runs slowly. Some code that reproduces this:

import numpy as np
import qtpy.QtWidgets as qt
import pyqtgraph as pg


class GraphWidget(qt.QWidget):
    """A widget for simplifying graphing tasks

    :param qt.QWidget parent:
    :param Dict[str, dict] layout: A mapping from title to row/col/rowspan/colspan kwargs
    """
    def __init__(self, parent, layout_spec):
        super(GraphWidget, self).__init__(parent=parent)

        self.axes = {}
        glw = pg.GraphicsLayoutWidget(parent=self)
        for name, layout in layout_spec.items():
            self.axes[name] = pg.PlotItem(name=name, title=name)
            glw.addItem(self.axes[name], **layout)
        box_layout = qt.QVBoxLayout()
        box_layout.addWidget(glw, 1)
        self.setLayout(box_layout)

    @property
    def normal_pen(self):
        return pg.mkPen(color='w', width=2)

    @property
    def good_pen(self):
        return pg.mkPen(color='g', width=2)

    @property
    def bad_pen(self):
        return pg.mkPen(color='r', width=2)

    def plot(self, mode, x, y, axis):
        if mode == 'normal':
            pen = self.normal_pen
        elif mode == 'good':
            pen = self.good_pen
        elif mode == 'bad':
            pen = self.bad_pen

        plot_item = pg.PlotCurveItem(x, y, pen=pen)
        self.axes[axis].addItem(plot_item)


if __name__ == '__main__':
    import random
    import time

    # qt.QApplication.setGraphicsSystem('opengl')

    app = qt.QApplication([])
    window = qt.QWidget(parent=None)
    layout = qt.QVBoxLayout()

    gw = GraphWidget(
        window,
        {
            'A': dict(row=1, col=1, rowspan=2),
            'B': dict(row=1, col=2),
            'C': dict(row=2, col=2),
            'D': dict(row=1, col=3),
            'E': dict(row=2, col=3),
            'F': dict(row=1, col=4),
            'G': dict(row=2, col=4),
        }
    )
    layout.addWidget(gw, 1)

    def plot():
        start = time.time()
        for axis in 'ABCDEFG':
            gw.plot(
                random.choice(['normal', 'good', 'bad']),
                np.arange(2000),
                np.random.rand(2000),
                axis,
            )
        # necessary because without it, the "plotting" completes in ms, 
        # but the UI doesn't update for a while still
        app.processEvents()
        print('Plotting time: {}'.format(time.time() - start))

    button = qt.QPushButton(parent=window, text='Plot')
    button.pressed.connect(plot)
    layout.addWidget(button)

    window.setLayout(layout)
    window.showMaximized()
    app.exec_()

The number and layout of plots, and the number of points, is chosen to reflect the real-world use case I have. If I run this as is and click the plot button twice I see

Plotting time: 3.61599993706
Plotting time: 7.04699993134

I stopped at this point because the application in general was suddenly very slow and took several seconds just to close. If I uncomment the one line to enable opengl rendering, I can easily run it more times and it looks like

Plotting time: 0.0520000457764
Plotting time: 0.328999996185
Plotting time: 0.453000068665
Plotting time: 0.55999994278
Plotting time: 0.674000024796
Plotting time: 1.21900010109
Plotting time: 0.936000108719
Plotting time: 1.06100010872
Plotting time: 1.19899988174
Plotting time: 1.35100007057

At this point I can also tell that the times reported here are not really accurate, it takes longer than these times for the UI to actually reflect the update.

This is a fairly typical number of graphs for me to be able to deal with, this application could easily see 16 sets of graphs (one additional line per axis) in about 20 seconds. If my UI is getting progressively slower this just isn't sufficiently fast.

Down-sampling doesn't seem to apply with the PlotCurveItem (unlike PlotDataItem), but I did discover that doing

plot_item = pg.PlotCurveItem(x, y, pen=pen, connect='pairs')

results in much faster times:

Plotting time: 0.0520000457764
Plotting time: 0.0900001525879
Plotting time: 0.138000011444
Plotting time: 0.108000040054
Plotting time: 0.117000102997
Plotting time: 0.12299990654
Plotting time: 0.143000125885
Plotting time: 0.15499997139

This still seems pretty slow to me though, and I'm wondering if there's anything that can be done to speed it up further. It's also still very slow (on the order of several seconds per plot) if I don't set it to opengl. I'm really looking for as fast of plotting as I can possibly manage. My target would be <100ms per plot.

It's hard to tell where to slowdowns really are though, profiling this code seems like it will be difficult since some of it happens at the OpenGL level, some deep inside Qt code, and some in pyqtgraph itself.

Is there any other way to speed up this code further?

Note: I am currently using Python 2.7 64bit, PyQt 4.10.4, and pyqtgraph 0.10.0 installed via conda, although this code will need to work equally well on Python 3.5+.

1 Answers

Hi @bheklilr this is a known issue in pyqtgraph, and by extension the Qt framework. On accident a pyqtgraph contributor came across a different drawing method that did not have the performance impact when drawing lines with greater than 1 pixel. Here is a draft PR that implements this workaround.

https://github.com/pyqtgraph/pyqtgraph/pull/2011

If you modify your example slightly you can regain the performance:

plot_item = pg.PlotCurveItem(x, y, pen=pen, skipFiniteCheck=True)

You need to pass the skipFiniteCheck=True argument, not use non-finite values, and of course, run your example on the PR.

We're trying to get some more users of this example to try and come up with edge cases where things break; when we get some confidence, we'll merge this feature and have it be part of the next release.

Related