GauiStori / PyQt-Qwt

Python PyQt wrapper for Qwt6
Other
53 stars 18 forks source link

Performance of QwtPlotCurve.setSamples #7

Closed otonck closed 5 years ago

otonck commented 5 years ago

Hi again,

this is not a bug but a performance improvement proposition.

I often need for my applications to display data changing over time, like in this simple example :

#!/usr/bin/python

import sys
from PyQt5 import Qwt
import numpy as np
from PyQt5.QtCore import Qt,  QSize, QTimer
from PyQt5.QtGui import QBrush, QPen
from PyQt5.QtWidgets import QApplication

import time
import numpy as np

NPOINTS = 500000
x = np.linspace(0., 4*np.pi, NPOINTS)
NB_FRAMES_FOR_MEAN_FPS = 10

class TestApp(QApplication):

    def __init__(self, argv):
        super(QApplication, self).__init__(argv)

        self.last_phase = 0
        self.last_time = time.time();
        self.count = 0;

    def updateDataToDraw(self):
        self.last_phase = (self.last_phase + np.pi / 16) % (2 * np.pi)

        y=np.sin(x+self.last_phase)
        curve.setSamples(x,y)
        self.count=self.count+1
        if self.count%NB_FRAMES_FOR_MEAN_FPS == 0:
            current_time = time.time()
            print("FPS : " + str(NB_FRAMES_FOR_MEAN_FPS/(current_time - self.last_time)))
            self.last_time = current_time

a = TestApp(sys.argv)

plot=Qwt.QwtPlot()
plot.setAutoReplot()
curve = Qwt.QwtPlotCurve()

y=np.sin(x)
curve.setSamples(x,y)
curve.attach(plot)

plot.resize(600,400)
plot.replot()
plot.show()

timer = QTimer()
timer.timeout.connect(a.updateDataToDraw)
timer.start(1)

sys.exit(a.exec_())

This prints on my computer a number of displayed frames per second of ~9fps,which is quite less that the ~25fps I get writing the same example in C++.

This seems related to the QwtPlotCurve setSamples method wrapper, which takes as input two python iterables (variables x and y in the example), converts them to QVector<double> objects (which is a costly operation when we have a lot of points) and finally passes them to the real Qwt setSamples method.

One possible workaround would be to use a type which has a direct C++ <-> Python wrapper to pass the samples data and avoid this kind of performance hit : QPolygonF (which is just an explicit definition of the template type QVector<QPointF>).

So, using this code :

#!/usr/bin/python

import sys
from PyQt5 import Qwt
import numpy as np
from PyQt5.QtCore import Qt,  QSize, QTimer
from PyQt5.QtGui import QBrush, QPen, QPolygonF
from PyQt5.QtWidgets import QApplication

import time
import numpy as np

NPOINTS = 500000
x = np.linspace(0., 4*np.pi, NPOINTS)
NB_FRAMES_FOR_MEAN_FPS = 10

class TestApp(QApplication):

    def __init__(self, argv):
        super(QApplication, self).__init__(argv)

        self.last_phase = 0
        self.last_time = time.time();
        self.count = 0;

    def updateDataToDraw(self):
        self.last_phase = (self.last_phase + np.pi / 16) % (2 * np.pi)

        y=np.sin(x+self.last_phase)

        size = len(x)
        polyline = QPolygonF(size)

        pointer = polyline.data()
        dtype, tinfo = np.float, np.finfo
        pointer.setsize(2 * size * tinfo(dtype).dtype.itemsize)
        memory = np.frombuffer(pointer, dtype)
        memory[:(size - 1) * 2 + 1:2] = x
        memory[1:(size - 1) * 2 + 2:2] = y

        curve.setSamples(polyline)

        self.count=self.count+1
        if self.count%NB_FRAMES_FOR_MEAN_FPS == 0:
            current_time = time.time()
            print("FPS : " + str(NB_FRAMES_FOR_MEAN_FPS/(current_time - self.last_time)))
            self.last_time = current_time

a = TestApp(sys.argv)

plot=Qwt.QwtPlot()
plot.setAutoReplot()
curve = Qwt.QwtPlotCurve()

y=np.sin(x)
curve.setSamples(x,y)
curve.attach(plot)

plot.resize(600,400)
plot.replot()
plot.show()

timer = QTimer()
timer.timeout.connect(a.updateDataToDraw)
timer.start(1)

sys.exit(a.exec_())

and changing the sip file qwt_plot_curve.sip from this:

//#ifndef QWT_NO_COMPAT
    void setRawSamples( const double *xData, const double *yData, int size );
    void setSamples( const double *xData, const double *yData, int size );
    void setSamples( const QVector<double> &xData, const QVector<double> &yData );
    void setSamples( const QVector<QPointF> & );
    void setSamples( QwtSeriesDataQPointF * );

to this :

//#ifndef QWT_NO_COMPAT
    void setRawSamples( const double *xData, const double *yData, int size );
    void setSamples( const double *xData, const double *yData, int size );
    void setSamples( const QVector<double> &xData, const QVector<double> &yData );
    void setSamples( const QPolygonF & );
    void setSamples( const QVector<QPointF> & );
    void setSamples( QwtSeriesDataQPointF * );

we get a great performance increase of the example above (from ~9fps to ~21fps in my computer, much closer to the performance of the C++ only solution).

Note that the added line must not be added after the void setSamples( const QVector<QPointF> & ); line, so that the sip wrapper tests if the input is a QPolygonF before testing if it is a QVector<QPointF>, in which case we have even lower performance (the sip part still iterates over the QPolygonF input to create a QVector<QPointF>, and the python code that builds the QPolygonF takes additional time (compared to the first initial example) for nothing.

Do you think this little change to qwt_plot_curve.sip could be merged ? I know we would take a lot of benefits from this for our projects.

Thanks again (again),

best regards, Olivier

GauiStori commented 5 years ago

Hi Olivier

Thanks a lot for the hint. I have added void setSamples( const QPolygonF & );

The performance changes from 16 frames/sec to 45 frames/sec on my computer.

Regards Gudjon

otonck commented 5 years ago

Hi Gudjon,

thanks a lot. Closing this.

Regards, Olivier