Repository: pyqtgraph/pyqtgraph

Issue:
`QMenu`s not destroyed (memory leak)
I found, that when you repeatedly instantiate `PlotItem`s and delete them again, the corresponding `QMenu` objects for the context menus of the plots never get destroyed. I saw lot's of `QMenu` instances using GammaRay (https://www.kdab.com/development-resources/qt-tools/gammaray/)

But this can simply be reproduced with the following snippet. The more often you create, add and remove new plot instances the more memory is used. Simply increase `N` to e.g. 100, 500, 1000, 2000 and check the memory usage.

```
import pyqtgraph as pg

app = pg.mkQApp()
widget = pg.GraphicsLayoutWidget()
widget.show()

N = 10
for i in range(N):
    plot = pg.PlotItem()
    plot.plot(x=[0,i, N], y=[0.0, 1.0, 0.0])
    widget.addItem(plot)
    app.processEvents()
    widget.removeItem(plot)

if __name__ == '__main__':
    pg.exec()
```


Discussion:
I am not sure that your example is valid as the documentation for `processEvents` (https://doc.qt.io/qt-6/qcoreapplication.html#processEvents) says that using it in the manner that you are using it results in `DeferredDelete` events not getting processed.

Also, what is the use-case for repeatedly instantiating `PlotItem`?
> I am not sure that your example is valid as the documentation for processEvents (https://doc.qt.io/qt-6/qcoreapplication.html#processEvents) says that using it in the manner that you are using it results in DeferredDelete events not getting processed.

I added this call of `processEvents` only to "see" something. The whole loop takes some seconds an to see the progress I added `plot.plot(x=[0,i, ...` and `app.processEvents()`.
You can remove both lines and you'll experience the same memory growth.

> Also, what is the use-case for repeatedly instantiating PlotItem?

The loop here is just to highlight the problem. 
I'm not arguing, that there are a lot of use cases where this really becomes a problem.
It's just that memory, which is not needed anymore, should be freed. And when I found this, I couldn't just ignore it ;)
I have modified your example so that it executes with the event loop running.
I have tested it on PySide2 and PyQt5 and the memory growth is obvious with PySide2 but not with PyQt5.
Which binding(s) were you using?

This reminds me of #2348 where the leak only occurred on PySide bindings.
```python
from pyqtgraph.Qt import QtCore
import pyqtgraph as pg

app = pg.mkQApp()
glw = pg.GraphicsLayoutWidget(show=True)

N = 1000
i = 0
def update():
    global i
    glw.clear()
    pi = glw.addPlot()
    pi.plot(x=[0,i, N], y=[0.0, 1.0, 0.0])
    i += 1
    if i == N:
        app.quit()

timer = QtCore.QTimer()
timer.timeout.connect(update)
timer.start(0)

pg.exec()
```
Okay, this is interesting.
I'm using PySide2.
So this could actually be a bug in PySide2 instead of incorrect usage or missing destruction of these `QMenu` instances?
Unlike #2173, there's nothing left in the `items()` of `GraphicsLayoutWidget`.
The comment in `PlotItem::close()` https://github.com/pyqtgraph/pyqtgraph/blob/master/pyqtgraph/graphicsItems/PlotItem/PlotItem.py#L379 says that historically PySide1 has had issues.
Calling that method (which does have code to destroy the QMenu) doesn't seem to stop the leak.
Shows the leaked objects are `QMenu`, `QAction`, `QIcon`.
By setting `enableMenu=False`, the number is reduced but still present. (`ViewBox` menu items won't be created if not enabled but `PlotItem` creates them anyway)

```python
import PySide6
from shiboken6 import getAllValidWrappers
import pyqtgraph as pg

pg.mkQApp()

glw = pg.GraphicsLayoutWidget()
w1 = getAllValidWrappers()
glw.addPlot(enableMenu=True)
glw.clear()
w2 = getAllValidWrappers()
dw = [x for x in w2 if x not in w1]
print(len(dw))

for x in dw:
    print(x)
```
> So this could actually be a bug in PySide2 instead of incorrect usage or missing destruction of these QMenu instances?

I suppose there are differences in object life-cycle management between PyQt and PySide. Whether the difference in behaviour of one binding with respect to the other binding constitutes a "bug" is arguable.

The fix that got merged only addresses PlotItem and ViewBox. It is not known if the other many uses of QMenu within the library also have this issue. 

Thank you for identifying this issue. 

Thank you for tackling this issue so rapidly! :)

Resolving patch:
diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py
--- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py
+++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py
@@ -201,7 +201,7 @@ def __init__(self, parent=None, name=None, labels=None, title=None, viewBox=None
         self.ctrlMenu.setTitle(translate("PlotItem", 'Plot Options'))
         self.subMenus = []
         for name, grp in menuItems:
-            sm = QtWidgets.QMenu(name, self.ctrlMenu)
+            sm = QtWidgets.QMenu(name)
             act = QtWidgets.QWidgetAction(self)
             act.setDefaultWidget(grp)
             sm.addAction(act)
diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py
--- a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py
+++ b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py
@@ -24,7 +24,7 @@ def __init__(self, view):
         self.widgetGroups = []
         self.dv = QtGui.QDoubleValidator(self)
         for axis in 'XY':
-            m = QtWidgets.QMenu(self)
+            m = QtWidgets.QMenu()
             m.setTitle(f"{axis} {translate('ViewBox', 'axis')}")
             w = QtWidgets.QWidget()
             ui = ui_template.Ui_Form()
@@ -56,7 +56,7 @@ def __init__(self, view):
         self.ctrl[0].invertCheck.toggled.connect(self.xInvertToggled)
         self.ctrl[1].invertCheck.toggled.connect(self.yInvertToggled)
         
-        self.leftMenu = QtWidgets.QMenu(translate("ViewBox", "Mouse Mode"), self)
+        self.leftMenu = QtWidgets.QMenu(translate("ViewBox", "Mouse Mode"))
         group = QtGui.QActionGroup(self)
         
         # This does not work! QAction _must_ be initialized with a permanent 


Test patch: