Repository: pyqtgraph/pyqtgraph

Issue:
Autoscale - Performance Regression
### Short description
Between PyQtGraph 0.11.0 and further versions there is a **performance regression** noticeable for plots with **autoscale activated**. The older version is significantly faster.

Analysis of regression yields to PR !1435 

### Code to reproduce

Use the delivered PlotSpeedTest and activate autoscale by playing with the example.
```
https://github.com/pyqtgraph/pyqtgraph/blob/master/examples/PlotSpeedTest.py
```

### Tested environment(s)

 * PyQtGraph version: master
 * Qt Python binding: 5.12.3
 * Python version: 3.8
 * NumPy version: any



### Additional context



Discussion:

Resolving patch:
diff --git a/examples/PlotSpeedTest.py b/examples/PlotSpeedTest.py
--- a/examples/PlotSpeedTest.py
+++ b/examples/PlotSpeedTest.py
@@ -3,47 +3,45 @@
 """
 Update a simple plot as rapidly as possible to measure speed.
 """
-
-## Add path to library (just for examples; you do not need this)
 import initExample
-
-
+## Add path to library (just for examples; you do not need this)
+from time import perf_counter
+from collections import deque
 from pyqtgraph.Qt import QtGui, QtCore
 import numpy as np
 import pyqtgraph as pg
-from pyqtgraph.ptime import time
+
 app = pg.mkQApp("Plot Speed Test")
 
 p = pg.plot()
 p.setWindowTitle('pyqtgraph example: PlotSpeedTest')
-p.setRange(QtCore.QRectF(0, -10, 5000, 20)) 
+p.setRange(QtCore.QRectF(0, -10, 5000, 20))
 p.setLabel('bottom', 'Index', units='B')
 curve = p.plot()
 
-#curve.setFillBrush((0, 0, 100, 100))
-#curve.setFillLevel(0)
+data = np.random.normal(size=(50, 5000))
+ptr = 0
+rollingAverageSize = 1000
 
-#lr = pg.LinearRegionItem([100, 4900])
-#p.addItem(lr)
+elapsed = deque(maxlen=rollingAverageSize)
 
-data = np.random.normal(size=(50,5000))
-ptr = 0
-lastTime = time()
-fps = None
 def update():
-    global curve, data, ptr, p, lastTime, fps
-    curve.setData(data[ptr%10])
+    global curve, data, ptr, elapsed, ptr
+
     ptr += 1
-    now = time()
-    dt = now - lastTime
-    lastTime = now
-    if fps is None:
-        fps = 1.0/dt
-    else:
-        s = np.clip(dt*3., 0, 1)
-        fps = fps * (1-s) + (1.0/dt) * s
-    p.setTitle('%0.2f fps' % fps)
-    app.processEvents()  ## force complete redraw for every plot
+    # Measure
+    t_start = perf_counter()
+    curve.setData(data[ptr % 10])
+    app.processEvents(QtCore.QEventLoop.ProcessEventsFlag.AllEvents)
+    elapsed.append(perf_counter() - t_start)
+
+    # update display every 50-updates
+    if ptr % 50 == 0:
+        average = np.mean(elapsed)
+        fps = 1 / average
+        p.setTitle('%0.2f fps - %0.1f ms avg' % (fps, average * 1_000))
+
+
 timer = QtCore.QTimer()
 timer.timeout.connect(update)
 timer.start(0)
diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py
--- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py
+++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py
@@ -279,20 +279,17 @@ def close(self):
     def implements(self, interface):
         return interface == 'ViewBox'
 
-    # removed due to https://bugreports.qt-project.org/browse/PYSIDE-86
-    #def itemChange(self, change, value):
-        ## Note: Calling QWidget.itemChange causes segv in python 3 + PyQt
-        ##ret = QtGui.QGraphicsItem.itemChange(self, change, value)
-        #ret = GraphicsWidget.itemChange(self, change, value)
-        #if change == self.ItemSceneChange:
-            #scene = self.scene()
-            #if scene is not None and hasattr(scene, 'sigPrepareForPaint'):
-                #scene.sigPrepareForPaint.disconnect(self.prepareForPaint)
-        #elif change == self.ItemSceneHasChanged:
-            #scene = self.scene()
-            #if scene is not None and hasattr(scene, 'sigPrepareForPaint'):
-                #scene.sigPrepareForPaint.connect(self.prepareForPaint)
-        #return ret
+    def itemChange(self, change, value):
+        ret = super().itemChange(change, value)
+        if change == self.GraphicsItemChange.ItemSceneChange:
+            scene = self.scene()
+            if scene is not None and hasattr(scene, 'sigPrepareForPaint'):
+                scene.sigPrepareForPaint.disconnect(self.prepareForPaint)
+        elif change == self.GraphicsItemChange.ItemSceneHasChanged:
+            scene = self.scene()
+            if scene is not None and hasattr(scene, 'sigPrepareForPaint'):
+                scene.sigPrepareForPaint.connect(self.prepareForPaint)
+        return ret
 
     def prepareForPaint(self):
         #autoRangeEnabled = (self.state['autoRange'][0] is not False) or (self.state['autoRange'][1] is not False)
@@ -426,7 +423,6 @@ def removeItem(self, item):
         if scene is not None:
             scene.removeItem(item)
         item.setParentItem(None)
-
         self.updateAutoRange()
 
     def clear(self):
@@ -453,7 +449,6 @@ def resizeEvent(self, ev):
             self.sigStateChanged.emit(self)
             self.sigResized.emit(self)
 
-
     def viewRange(self):
         """Return a the view's visible range as a list: [[xmin, xmax], [ymin, ymax]]"""
         return [x[:] for x in self.state['viewRange']]  ## return copy
@@ -627,7 +622,6 @@ def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=Tru
                 self._autoRangeNeedsUpdate = True
             elif changed[1] and self.state['autoVisibleOnly'][0] and (self.state['autoRange'][1] is not False):
                 self._autoRangeNeedsUpdate = True
-
             self.sigStateChanged.emit(self)
 
     def setYRange(self, min, max, padding=None, update=True):
@@ -1608,7 +1602,6 @@ def updateMatrix(self, changed=None):
         self.sigTransformChanged.emit(self)  ## segfaults here: 1
 
     def paint(self, p, opt, widget):
-        self.prepareForPaint()
         if self.border is not None:
             bounds = self.shape()
             p.setPen(self.border)


Test patch: