Repository: pyqtgraph/pyqtgraph

Issue:
Performance degradation in PlotCurveItem with OpenGL by 0.12.4 (#2032)
Hi, 

in our application, we recently updated pyqtgraph from 0.12.3 to 0.12.4 and experienced a strong slow down of the gui, when reacting to user interaction. This holds true for Win10 with OpenGL enabled only.


### Minimum working example and performance comparison

At the end of this report ("MWE test script"), you find a minimum working example, allowing to "measure" the performance. 
It resembles the way of the plot in our application, i.e. using PlotCurveItem with filling but disabled pen. We plot a potentially large vector of data without much updating of the data. The rather interesting part is, how fast the gui reacts on user interaction like panning, e.g. using click'n'drag. 
In fact, this is what is done in the script to measure the frames-per-second (fps): Just pan the view repeatedly, this produces fps values and after enough values are collected, close the window to compute and print the median. 

The following table shows a comparison of the resulting median-fps for three different numbers of samples (`num_samples`), with and without OpenGL (Win10) for the two relevant versions of pyqtgraph. (Note, that I put "n.a." if it was too slow to get a reasonable value.)
Clearly, without OpenGL, the update improves the performance significantly, no question about that. But with OpenGL, the performance degrades by about factor 2 and factor 5 for 50k and 500k samples, respectively.

```
                                USE_OPENGL
num_samples     pyqtgraph   False           True
==============================================================
5000
                0.12.3      1.693 (45)      73.798 (100)
                0.12.4      46.456 (100)    72.987 (100)
--------------------------------------------------------------
50000
                0.12.3      n.a.            75.510 (100)
                0.12.4      23.156 (100)    34.691 (100)
--------------------------------------------------------------
500000
                0.12.3      n.a.            32.180 (100)
                0.12.4      4.200 (100)     6.178 (100)
--------------------------------------------------------------
                            median-fps (no. of measurements)
==============================================================
```

#### Environment
Windows 10
Qt 5.15.2
PySide2 5.15.2.1
numpy 1.22.3

### Reason

It seems, that this comes with #2032, which splits the drawing of the vector into small chunks. Currently the data to be plotted is split in chunks of size 50.
I quickly analyzed the influence of this `chunksize`. The following list shows the resulting median fps, as above.

```
chunksize=50    :   6.178 (100)  (from above)
chunksize=500   :   24.414 (100)
chunksize=5000  :   37.236 (100)
chunksize=50000 :   39.778 (100)
chunksize=500000:   31.448 (100)

USE_OPENGL=True
pyqtgraph 0.12.4
num_samples=500000
```

Maybe `chunksize = 50` is a suitable value if OpenGL is disabled, but if it's enabled, this is clearly no good choice.
@pijyoi how'd you decided for `chunksize==50`? What was your test case? Maybe I'm missing something?

### Potential fixes

Unfortunately, `chunksize` is hard coded in the private method `PlotCurveItem._getFillPathList()`.

Currently, I see a few different approaches to improve this:
* Simply disable the chunking if OpenGL is used, or
* make `chunksize` dependent on OpenGL, i.e. if OpenGL is enabled, set it to a larger value, or
* make the chunking optional with a new value "skipFillingInChunks" in `opts`, or
* make the `chunksize` accessible within `opts` as "fillingChunkSize", which also could accept a magic value "0" for disabling chunking.

I would suggest the last option and could provide a patch to implement this.
Also, if there are other ideas to improve this, I should be able to come up with some patch.

### MWE test script

```python
import time

import numpy as np
from pyqtgraph import (
    GraphicsLayoutWidget,
    PlotCurveItem,
    PlotItem,
    mkBrush,
    mkPen,
    mkQApp,
    exec,
    Qt,
    __version__,
)

app = mkQApp()


num_samples = 500000
USE_OPENGL = True


print(f"pyqtgraph {__version__}")
print(f"{USE_OPENGL=}")
print(f"{num_samples=}")

glw = GraphicsLayoutWidget()
glw.useOpenGL(USE_OPENGL)
glw.show()

pi = PlotItem()
glw.addItem(pi)

pci = PlotCurveItem(fillLevel=0.0, brush=mkBrush(color="w"), pen=mkPen(None))

has_new_func = hasattr(pci, "_getFillPathList")
assert (__version__ == "0.12.3" and not has_new_func) or (__version__=="0.12.4" and has_new_func)


pi.addItem(pci)

data = np.random.normal(size=num_samples)
pci.setData(data)

pi.setLimits(xMin=0, xMax=num_samples-1)
x_min = num_samples//4
x_max = x_min + num_samples//2
pi.setXRange(x_min, x_max, padding=0)

N = 100
result = np.empty(N)
result.fill(np.NAN)

ctr: int = 0
t_last: float = 0.0


def benchmark_view_changed():
    global ctr, t_last, result
    if ctr <= 0:
        t_last = time.perf_counter()
        ctr += 1
        return

    t_new = time.perf_counter()
    dt = t_new - t_last
    t_last = t_new
    fps = 1.0 / dt
    print(f"{ctr}: {fps=:.2f}")
    if ctr <= N:
        result[ctr-1] = fps
    ctr += 1


pi.sigXRangeChanged.connect(benchmark_view_changed)


def compute_result():
    result_valid = result[0:ctr-1]
    print(f"median-fps = {np.median(result_valid):.3f} ({len(result_valid)})")


app.lastWindowClosed.connect(compute_result)


if __name__ == '__main__':
    exec()
```


Discussion:
Hello, thank you for raising this detailed issue.
Your use case is as follows:
1) useOpenGL=True (but not by using `setConfigOption('useOpenGL')`
2) `enableExperimental=False` (indeed, fill level is not implemented in this codepath)
3) Windows platform (as far as we know, the combination of `useOpenGL=True and enableExperimental=False` leads to unusable line plotting performance on Linux and macOS platforms)

We have actually been curious as to the users who do actually use the combination of `useOpenGL=True and enableExperimental=False`! 

>how'd you decided for chunksize==50? What was your test case? Maybe I'm missing something?

It was somewhat arbitrary. i.e. trying out various values on my machine. Mostly the testing was done using the included `PlotSpeedTest.py` example. However, the `useOpenGL=True and enableExperimental=False` codepath was not tested.

To test your use-case, I ran:
`PlotSpeedTest.py --opengl --nsamples=50000 --frequency=1`
followed by unchecking  the `enableExperimental` checkbox.
By fiddling `_shouldUseFillPathList()`, I could verify that under these conditions, there was a performance regression of about 2x.

>Simply disable the chunking if OpenGL is used, or

This could be done by adding an additional condition to `_shouldUseFillPathList()`. However, I am not sure if using `getConfigOption('useOpenGL')` would be adequate to support users like yourself who call `useOpenGL()` directly.

>I would suggest the last option and could provide a patch to implement this.
Also, if there are other ideas to improve this, I should be able to come up with some patch.

From your results, chunksize=5000 and chunksize=50000 yield a gain of 37/32~=1.15 and 39/32~=1.21 respectively. I am not sure if that is worth allowing configuring for. But perhaps others have a different opinion.

Resolving patch:
diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py
--- a/pyqtgraph/graphicsItems/PlotCurveItem.py
+++ b/pyqtgraph/graphicsItems/PlotCurveItem.py
@@ -710,7 +710,7 @@ def _shouldUseFillPathList(self):
             and isinstance(self.opts['fillLevel'], (int, float))
         )
 
-    def _getFillPathList(self):
+    def _getFillPathList(self, widget):
         if self._fillPathList is not None:
             return self._fillPathList
 
@@ -735,9 +735,16 @@ def _getFillPathList(self):
         if len(x) < 2:
             return []
 
+        # Set suitable chunk size for current configuration:
+        #   * Without OpenGL split in small chunks
+        #   * With OpenGL split in rather big chunks
+        #     Note, the present code is used only if config option 'enableExperimental' is False,
+        #     otherwise the 'paintGL' method is used.
+        # Values were found using 'PlotSpeedTest.py' example, see #2257.
+        chunksize = 50 if not isinstance(widget, QtWidgets.QOpenGLWidget) else 5000
+
         paths = self._fillPathList = []
         offset = 0
-        chunksize = 50          # determined empirically
         xybuf = np.empty((chunksize+3, 2))
         baseline = self.opts['fillLevel']
 
@@ -784,7 +791,7 @@ def paint(self, p, opt, widget):
 
         if do_fill:
             if self._shouldUseFillPathList():
-                paths = self._getFillPathList()
+                paths = self._getFillPathList(widget)
             else:
                 paths = [self._getFillPath()]
 


Test patch: