Repository: pyqtgraph/pyqtgraph

Issue:
Memory Leak when adding GLScatterPlotItem
I have found that adding and removing the same `GLScatterPlotItem` in a `GLViewWidget` causes a memory leak. Here is a script to demonstrate the behavior:

``` python
import numpy as np
from PySide import QtCore, QtGui
import sys

import pyqtgraph.opengl as gl

num_points = 10
refresh_interval = 1  # ms


class PlotWidget(QtGui.QWidget):

    def __init__(self, parent=None):
        super().__init__(parent)

        # Basic layout setup
        self.viewWidget = gl.GLViewWidget()
        layout = QtGui.QVBoxLayout()
        layout.addWidget(self.viewWidget)
        self.setLayout(layout)

        # Create out scatter plot item. Note that this leak only happens with
        # scatter items, not with line or mesh items.
        pos = np.random.rand(num_points, 3)
        self.scatter = gl.GLScatterPlotItem(pos=pos)
        self.viewWidget.addItem(self.scatter)

        # Create and run timer to repeatedly add and remove scatter item.
        self.timer = QtCore.QTimer()
        self.timer.timeout.connect(self._add_remove)
        self.timer.start(refresh_interval)

    def _add_remove(self):
        self.viewWidget.addItem(self.scatter)
        self.viewWidget.removeItem(self.scatter)

    def sizeHint(self):
        return QtCore.QSize(800, 800)


if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)

    widget = PlotWidget()
    widget.show()

    sys.exit(app.exec_())
```

I have tried using other plot items (like `GLLinePlotItem` and `GLAxisItem`), and I do not see the memory leak, so it seems only GLScatterPlotItem creates the leak. I have also tried using PyQt4 and the script still generates a memory leak.

Could this be a problem with binding data to the OpenGL context, but then that data does not get released when the plot item is removed? This is my current theory, because I used the `objgraph` package to find new Python objects every few seconds, and it couldn't find any. Furthermore, `GLScatterPlotItem` is the only item I tested that has a non-trivial `initializeGL` method.

I will do more investigation. If there is a workaround, I would appreciate that too :)

Thanks in advance for any help!



Discussion:
I've narrowed down the problem to the `glGenTextures` call on [line 69 of GLScatterPlotItem.py](https://github.com/pyqtgraph/pyqtgraph/blob/develop/pyqtgraph/opengl/items/GLScatterPlotItem.py#L69). If we add a check to see if the texture exists already, the memory leak for this example disappears:

``` python
if not hasattr(self, "pointTexture"):
    self.pointTexture = glGenTextures(1)
```

Of course, that won't work if we use the scatter plot item in another context. I can think of one solution:

Override `_setView` to make `self.pointTexture = None` if the view is `None` (the item is being removed). Then, only use `glGenTextures` if `self.pointTexture` is `None`:

``` python
...

if getattr(self, "pointTexture", None) is None:
    self.pointTexture = glGenTextures(1)

...

def _setView(self, v):
    super()._setView(v)
    if v is None:
        self.pointTexture = None
```

We might also benefit from a strategic use of `glDeleteTextures` too.

Maybe someone else has a better idea? Either way, I'm glad we know the exact cause of the leak: repeated, unnecessary  calls to `glGenTextures`.

Thanks John, the first solution you proposed is a good start. In general, none of the GL items explicitly support use in multiple contexts, so I don't think we need to worry about that for now. Would you like to open a PR?

Hey Luke!

I currently have another PR open (#102) that fixes the multiple GLViewWidget issue I found on PySide about a week ago. Is it possible to have multiple PRs from the same fork on Github? Otherwise I would be happy to open a PR.

Thanks!


Resolving patch:
diff --git a/pyqtgraph/opengl/items/GLScatterPlotItem.py b/pyqtgraph/opengl/items/GLScatterPlotItem.py
--- a/pyqtgraph/opengl/items/GLScatterPlotItem.py
+++ b/pyqtgraph/opengl/items/GLScatterPlotItem.py
@@ -66,7 +66,8 @@ def fn(x,y):
         #print pData.shape, pData.min(), pData.max()
         pData = pData.astype(np.ubyte)
         
-        self.pointTexture = glGenTextures(1)
+        if getattr(self, "pointTexture", None) is None:
+            self.pointTexture = glGenTextures(1)
         glActiveTexture(GL_TEXTURE0)
         glEnable(GL_TEXTURE_2D)
         glBindTexture(GL_TEXTURE_2D, self.pointTexture)


Test patch: