Repository: pyqtgraph/pyqtgraph

Issue:
plotWidget Memory Leak, pyqtgraph 0.12.4, PySide2 5.15.02
### Short description
I've run into a memory leak in plotWidget using pyqtgraph 0.12.4, and PySide2 5.15.02.  UpdateIntensity method clears and then plots on the plotWidget.  Plots display correctly, but see constant memory growth.  Confirmed disappears if I disable UpdateIntensity method.

### Code to reproduce

```python
from PySide2.QtCore import Qt
from Qt import (QtGui, QtCompat, QtCore, QtWidgets, __binding__, __binding_version__,
                __qt_version__, __version__)

import pyqtgraph as pg

    def UpdateIntensity(self):
        # Plot pixel intensities
        self.plotWidgetPixelIntensity.clear()

        self.plotWidgetPixelIntensity.showGrid(x = True, y = True, alpha = 0.75)
        self.plotWidgetPixelIntensity.setLabel('bottom', 'Pixel Intensity (counts)')
        self.plotWidgetPixelIntensity.addLegend()

        penRed = pg.mkPen(color=(255, 0, 0), width=2.0)
        penBlue = pg.mkPen(color=(0, 0, 255), width=2.0)
        penGreen = pg.mkPen(color=(150, 0, 0), width=5.0, style=QtCore.Qt.DashLine)
        penPurple = pg.mkPen(color=(0, 0, 150), width=5.0, style=QtCore.Qt.DashLine)

        self.plotWidgetPixelIntensity.enableAutoRange(axis='y')
        self.plotWidgetPixelIntensity.setAutoVisible(y=True)

        self.plotWidgetPixelIntensity.plot(
            self.vr.grayBins,
            self.vr.grayPDF,
            pen = penRed,
            brush = (255, 0, 0, 100),
            fillLevel=0,
            name = 'PDF Gray'
        )

        self.plotWidgetPixelIntensity.plot(
            self.vr.grayBins,
            self.vr.grayCDF,
            pen = penGreen,
            name = 'CDF Gray'
        )

        self.plotWidgetPixelIntensity.plot(
            [
                self.vr.centroidTrackThreshold,
                self.vr.centroidTrackThreshold
            ],
            [0, 1],
            pen = penBlue
        )

        self.plotWidgetPixelIntensity.plot(
            self.vr.threshGrayBins,
            self.vr.threshGrayPDF,
            pen = penBlue,
            brush = (0, 0, 255, 100),
            fillLevel=0,
            name = 'PDF Thresh'
        )

        self.plotWidgetPixelIntensity.plot(
            self.vr.threshGrayBins,
            self.vr.threshGrayCDF,
            pen = penPurple,
            name = 'CDF Thresh'
        )

        # self.plotWidgetPixelIntensity.setLogMode(False, True)
        # self.plotWidgetPixelIntensity.setRange(xRange=[0, 255], yRange=[-3, 0])
        self.plotWidgetPixelIntensity.setRange(xRange=[0, 255], yRange=[0, 1])
```


### Expected behavior
I get to keep my memory :)

### Real behavior
plotWidget eats my memory :(

### Tested environment(s)
 * PyQtGraph version: 0.12.4
 * Qt Python binding: PySide2 5.15.2 Qt 5.15.2
 * Python version: 3.7.9
 * NumPy version: 1.21.6
 * Operating system: Win 10
 * Installation method: pip

### Additional context
Also using Qt.py, and QtCompat.loadUi if that's meaningful.



Discussion:
Hi @csmotion I updated your post slightly to reflect syntax highlighting and to replace the tab characters with 4 spaces and a slight reformat to make it easier to read in markdown here.

How are you measuring the memory consumption?

EDIT: also what is the parent class you're using?  you've provided a method to an object, but I'm not sure what object you're using...

EDIT 2: On further looking at this, there are two attributes to the class which I'm not sure about; if you could update your example here with something that we can reproduce that would be helpful.
Howdy @j9ac9k, thanks for the quick response!

I'm measuring memory consumption with mprof and tracemalloc, but it's fast enough to watch in Windows task manager.

- The parent class for the UpdateIntensity method is MainWindow(QtWidgets.QWidget).
- self.plotWidgetPixelIntensity is a QWidget that has been promoted to a pyqtgraph PlotWidget in a Qt Designer .ui file.
- The self.vr class instance encapsulates all the non-GUI functionality.

I'll work on a minimum reproducible example.
Howdy again, @j9ac9k,

Minimum reproducible example located [here](https://github.com/csmotion/helloQt).

Looks like **self.plotWidget.addLegend()** is the offender.  Commenting this out entirely eliminates the memory leak.

However, even if I move the addLegend() call to be called once in init, I still observe the memory leak!
I can reproduce the leak. It is a problem if you repeatedly create new plot objects for each update.
In practice, the plot objects are reused and the leak doesn't cause a problem.
```python
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore
import numpy as np

pg.mkQApp()
pw = pg.PlotWidget()
pw.addLegend()
pw.show()

yyy0 = np.sin(2*np.pi*np.linspace(0, 1, 1000))
curve = pw.plot(name='CURVE')

cnt = 0
def update():
    global cnt
    y = np.roll(yyy0, -cnt)
    cnt = (cnt + 1) % yyy0.size

    if 1:
        # create fresh curve every iteration
        pw.clear()
        pw.plot(y, name='REPLOT')
    else:
        # usual way, reuse existing curve
        curve.setData(y)

timer = QtCore.QTimer()
timer.timeout.connect(update)
timer.start(0)

pg.exec()
```
Oh yeah; recreating new plot objects instead of calling `setData` is going to have adverse effects.
Note that the leak only occurs for PySide{2,6} but not for PyQt{5,6}, and only if legend is enabled.
@pijyoi @j9ac9k,

Confirmed setData works as desired!  Thank you very much for the help, and sorry for the operator error!
Not operator error, just a reflection of poor documentation! Hopefully you'll see a significant performance boost now with migrating to calling `setData` as well!
There is a leak that only occurs on PySide{2,6}

The following legend items are left behind in the scene:
```
<class 'PySide6.QtWidgets.QGraphicsTextItem'>
<class 'pyqtgraph.graphicsItems.LabelItem.LabelItem'>
<class 'pyqtgraph.graphicsItems.LegendItem.ItemSample'>
```

The script:
```python
import pyqtgraph as pg

pg.mkQApp()
pw = pg.PlotWidget()
pi = pw.getPlotItem()
legend = pi.addLegend()

base_items = pi.scene().items()

pdi = pi.plot([0, 1, 2], name='aaa')
pi.removeItem(pdi)

# items have been removed from LegendItem
assert len(legend.items) == 0
assert legend.layout.count() == 0

# but some items persist in the scene on PySide{2,6}
items = pi.scene().items()
items = [x for x in items if x not in base_items]
for item in items:
    print(type(item))
```

Now if only someone could demonstrate it without using pyqtgraph...
I have a pyqtgraph-free example here that demonstrates the difference between PyQt and PySide.
```python
from pyqtgraph.Qt import QtWidgets

qapp = QtWidgets.QApplication([])

# adapted from QGraphicsGridLayout example

scene = QtWidgets.QGraphicsScene()
textEdit = scene.addWidget(QtWidgets.QTextEdit())
pushButton = scene.addWidget(QtWidgets.QPushButton())

layout = QtWidgets.QGraphicsGridLayout()
layout.addItem(textEdit, 0, 0)
layout.addItem(pushButton, 0, 1)

form = QtWidgets.QGraphicsWidget()
form.setLayout(layout)
scene.addItem(form)

# remove all items from the layout
while layout.count() != 0:
    layout.removeAt(0)

# this is where PyQt differs from PySide
# On PyQt, when the last python reference is removed,
# the item gets removed from the scene.
# On PySide, the item persists in the scene.
if 1:
    del textEdit
    del pushButton

for item in scene.items():
    print(type(item))
```
The C++ equivalent of the above Python script shows that:
1) Removing something from a layout does *not* remove it from the scene.
2) Destroying that something will get it removed from the scene.

So the PyQt behavior is correct and the PySide behavior is incorrect.
```c++
#include <QtWidgets/QtWidgets>

int main(int argc, char *argv[])
{
    QApplication qapp(argc, argv);

    QGraphicsScene scene;
    QGraphicsWidget *textEdit = scene.addWidget(new QTextEdit());
    QGraphicsWidget *pushButton = scene.addWidget(new QPushButton());

    auto layout = new QGraphicsLinearLayout();
    layout->addItem(textEdit);
    layout->addItem(pushButton);

    auto form = new QGraphicsWidget();
    form->setLayout(layout);
    scene.addItem(form);

    while (layout->count() != 0) {
        layout->removeAt(0);
    }

    qDebug() << "before:" << scene.items().count();

    delete textEdit;
    delete pushButton;

    qDebug() << "after:" << scene.items().count();
}
```


Resolving patch:
diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py
--- a/pyqtgraph/graphicsItems/LegendItem.py
+++ b/pyqtgraph/graphicsItems/LegendItem.py
@@ -268,6 +268,18 @@ def getLabel(self, plotItem):
         except IndexError:
             return None
 
+    def _removeItemFromLayout(self, *args):
+        for item in args:
+            self.layout.removeItem(item)
+            item.close()
+            # Normally, the item is automatically removed from
+            # its scene when it gets destroyed.
+            # this doesn't happen on current versions of
+            # PySide (5.15.x, 6.3.x) and results in a leak.
+            scene = item.scene()
+            if scene:
+                scene.removeItem(item)
+
     def removeItem(self, item):
         """Removes one item from the legend.
 
@@ -279,20 +291,14 @@ def removeItem(self, item):
         for sample, label in self.items:
             if sample.item is item or label.text == item:
                 self.items.remove((sample, label))  # remove from itemlist
-                self.layout.removeItem(sample)  # remove from layout
-                sample.close()  # remove from drawing
-                self.layout.removeItem(label)
-                label.close()
-                self.updateSize()  # redraq box
+                self._removeItemFromLayout(sample, label)
+                self.updateSize()  # redraw box
                 return  # return after first match
 
     def clear(self):
         """Remove all items from the legend."""
         for sample, label in self.items:
-            self.layout.removeItem(sample)
-            sample.close()
-            self.layout.removeItem(label)
-            label.close()
+            self._removeItemFromLayout(sample, label)
 
         self.items = []
         self.updateSize()


Test patch: