2

The Problem :

I have an application, from which I open a modless child window by clicking on a button. This child window contains an embedded matplotlib figure. I would like this child window to be destroyed, along with the matplotlib figure, after is has been closed.

The problem is that, even though the child window seems to be properly deleted on the Qt side, there seems to be no memory deallocated from the process. The problem seems to be cumulative, i.e. the memory taken up by my application increases if I open multiple child windows and then close them manually with 'X'.

My system : Ubuntu 15.04 + Matplotlib 1.4.2 + python 2.7.9 or 3.4.3 + PySide 1.2.2

Note : I currently circumvent the "memory leak" problem by not destroying the child window and reusing the same artists to update the figure with new data. I would like however to be able to release completely the memory taken up by this child window when it is not needed.

What I've tried so far :

I've tried all the combination I could think of with gc.collect(), setParent(None), deleteLater(), del, set_attributes(QtCore.Qt.WA_DeleteOnClose), I've tried to be extra careful with the namespace and to use only weak links to matplotlib artists, I've also tried with and without using pyplot, but with no real success... I've been able to release some of the memory taken by the matplotlib child window(s) by reimplementing the closeEvent method of the child window class and cleaning stuff manually, but still, it's not working perfect and it's not pretty.

Below is a MCVE that illustrates the problem with a basic implementation of the best "solution" I have so far. I've tried the same code by replacing the FigureCanvasQTAgg widget with a "pure" qt widget (a QLabel hosting a large QPixmap) and all the memory taken up by the child window(s) was released on close.

import sys
import numpy as np
from PySide import QtGui, QtCore
import matplotlib as mpl
mpl.rcParams['backend.qt4']='PySide'
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg
import gc

class MyApp(QtGui.QWidget):
    def __init__(self):
        super(MyApp, self).__init__()

        btn_open = QtGui.QPushButton('Show Figure')
        btn_open.clicked.connect(self.show_a_figure)

        layout = QtGui.QGridLayout()
        layout.addWidget(btn_open, 0, 0)
        self.setLayout(layout)

    def show_a_figure(self):        
        MyFigureManager(self).show()


class MyFigureManager(QtGui.QWidget):    
    def __init__(self, parent=None):
        super(MyFigureManager, self).__init__(parent)

        self.setWindowFlags(QtCore.Qt.Window)
        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)

        layout = QtGui.QGridLayout()
        layout.addWidget(MyFigureCanvas(), 0, 0)
        self.setLayout(layout)

    def closeEvent(self, event):
        fig_canvas = self.findChild(MyFigureCanvas)        

        fig_canvas.figure.clear()
        del fig_canvas.figure

        fig_canvas.renderer.clear()
        del fig_canvas.renderer

        fig_canvas.mpl_disconnect(fig_canvas.scroll_pick_id)
        fig_canvas.mpl_disconnect(fig_canvas.button_pick_id)        
        fig_canvas.close()
        del fig_canvas

        gc.collect()

        super(MyFigureManager, self).closeEvent(event) 

class MyFigureCanvas(FigureCanvasQTAgg):               
    def __init__(self, parent=None):        
        super(MyFigureCanvas, self).__init__(figure=mpl.figure.Figure())
        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)

        #-- plot some data --

        ax = self.figure.add_axes([0.1, 0.1, 0.85, 0.85])
        ax.axis([0, 1, 0, 1])

        N = 100000
        x = np.random.rand(N)
        y = np.random.rand(N)
        colors = np.random.rand(N)
        area = np.pi * (5 * np.random.rand(N)) ** 2

        ax.scatter(x, y, s=area, c=colors, alpha=0.5)

if __name__ == '__main__':

    app = QtGui.QApplication(sys.argv)

    w = MyApp()
    w.show()

    sys.exit(app.exec_()) 

Update 2015-09-01

Example using a QLabel and only FigureCanvasAgg :

This is a simple application I've produced while trying to isolate the problem. When clicking on the "Show Figure" button:

  • if the QSpinBox number equals 1, the child window will contain a QLabel displaying a QPixmap produced from an external image.
  • if the QSpinBox number equals 2, the child window will contain a QLabel displaying a QPixmap produced from a matplotlib figure, using only the non-GUI backend FigureCanvasAgg.

enter image description here

I've also added a button to explicitly call a gc.collect(). In the first case (mode=1), all the memory taken up by the child window(s) are recovered upon closing. In the "matplotlib mode" (mode=2), some memory can be recovered when explicitly forcing a gc.collect(), but some is not. The amount of memory not being released seems to scale with the amount of points plotted in mpl, and somewhat related to the amount of child windows opened at the same time.

I am not very knowledgeable about memory management. I'm currently monitoring the memory used by the app through the System Load Indicator in Ubuntu. Please comment if I'm doing it wrong.

import sys
import numpy as np
from PySide import QtGui, QtCore
import matplotlib as mpl
import gc
from matplotlib.backends.backend_agg import FigureCanvasAgg

class MyApp(QtGui.QWidget):
    def __init__(self):
        super(MyApp, self).__init__()

        btn_open = QtGui.QPushButton('Show Figure')
        btn_open.clicked.connect(self.show_a_figure)

        btn_call4gc = QtGui.QPushButton('Garbage Collect')
        btn_call4gc.clicked.connect(self.collect)

        self.mode = QtGui.QSpinBox()
        self.mode.setRange (1, 2)

        layout = QtGui.QGridLayout()
        layout.addWidget(btn_open, 1, 1)
        layout.addWidget(self.mode, 1, 2)
        layout.addWidget(btn_call4gc, 2, 1)
        self.setLayout(layout)

    def show_a_figure(self):        
        MyFigureManager(mode=self.mode.value(), parent=self).show()

    def collect(self):
        num = gc.collect()
        print(num)

class MyFigureManager(QtGui.QWidget):    
    def __init__(self, mode, parent=None):
        super(MyFigureManager, self).__init__(parent)

        self.setWindowFlags(QtCore.Qt.Window)
        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)

        layout = QtGui.QGridLayout()
        layout.addWidget(MyFigureCanvas(mode=mode, parent=self), 0, 0)
        self.setLayout(layout)

class MyFigureCanvas(QtGui.QLabel):               
    def __init__(self, mode, parent=None):        
        super(MyFigureCanvas, self).__init__()
        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
        self.setMaximumSize(1000, 650)

        if mode == 1:

            #-- import a large image from hardisk --

            qpix = QtGui.QPixmap('aLargeImg.jpg') 

        elif mode == 2:

            #-- plot some data with mpl --

            canvas = FigureCanvasAgg(mpl.figure.Figure())
            renderer = canvas.get_renderer()

            ax = canvas.figure.add_axes([0.1, 0.1, 0.85, 0.85])
            ax.axis([0, 1, 0, 1])

            N = 50000
            x = np.random.rand(N)
            y = np.random.rand(N)
            colors = np.random.rand(N)
            area = np.pi * (5 * np.random.rand(N)) ** 2

            ax.scatter(x, y, s=area, c=colors, alpha=0.5)

            #-- convert mpl imag to pixmap --

            canvas.draw()
            imgbuf = canvas.figure.canvas.buffer_rgba()
            imgwidth = int(renderer.width)
            imgheight =int(renderer.height)
            qimg = QtGui.QImage(imgbuf, imgwidth, imgheight,
                                QtGui.QImage.Format_ARGB32)
            qimg = QtGui.QImage.rgbSwapped(qimg)
            qpix = QtGui.QPixmap(qimg)

        self.setPixmap(qpix)

if __name__ == '__main__':

    app = QtGui.QApplication(sys.argv)

    w = MyApp()
    w.show()

    sys.exit(app.exec_())

Related Documentation :

SO posts :

Matplotlib Documentation :

Community
  • 1
  • 1
Jean-Sébastien
  • 2,649
  • 1
  • 16
  • 21
  • what version of mpl + python + pyside? IIRC there is a bug in pyside related to pyside not correctly reference counting on the QPixmap buffer used to copy the plot from the Agg buffer to screen. Mpl has a work around, but I don't remember if it is released or not. – tacaswell Aug 31 '15 at 20:06
  • See here abouts https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/backends/backend_qt5agg.py#L114 – tacaswell Aug 31 '15 at 20:08
  • @tcaswell Matplotlib 1.4.2 + python 2.7 or 3.3 + PySide 1.2.2. Thanks for the link, I'll take a look at it and will comment back. – Jean-Sébastien Aug 31 '15 at 20:25
  • I have a hunch that it is sloppy clean up of qt level signals/slots. – tacaswell Aug 31 '15 at 21:13
  • @tcaswell I've tested tested the mpl workaround by reimplementing the `painEvent` of `MyFigureCanvas` class as in the link you provided above with no real impact. – Jean-Sébastien Aug 31 '15 at 21:21
  • @tcaswell Thanks for the lead about qt signals/slots, I'll look into this. I have a feeling that the problem resides on the qt side because, in an other version of the code example I've provided above, I was able to have `num = gc.collect()` count to zero in the reimplementation of the `closeEvent` method by deleting manually all the references I could find in mpl, but with no real impact on the memory allocation. I'll see if I can find anything else useful. – Jean-Sébastien Aug 31 '15 at 21:26
  • http://stackoverflow.com/questions/32280140/cannot-delete-matplotlib-animation-funcanimation-objects/32288569#32288569 That is where my hunch comes from. mpl _does_ have internal cycles. You should check if you can reproduce this in py3.4. I will try to reproduce this when I get home. – tacaswell Aug 31 '15 at 21:30
  • Also, this should probably be escalated to an issue on GH – tacaswell Aug 31 '15 at 21:31
  • @tcaswell I've tried to short-circuit all the signal/slots connections from qt to mpl with no avail. Then, since I don't need the interactive capability of mpl for this, I've tried to use a non-GUI backend, convert manually the mpl figure in a qpixmap and display the later in a QLabel. There still seems to be some memory not released when closing the child window (see the update), but maybe this is normal... – Jean-Sébastien Sep 01 '15 at 15:59
  • @tcaswell To give a follow up to one of your comment, I'm using python 2.7.9 or 3.4.3, not 3.3 as I said earlier. I don't know why I was thinking I was using 3.3... – Jean-Sébastien Sep 01 '15 at 21:18
  • @tcaswell If I may add a note, **MemoryError** started to appear after some 50+ python commands/calls to a plain & dumb plotting utility function, which uses "File "C:\Python27.anaconda\lib\site-packages\matplotlib\backends\backend_qt5agg.py", line 91, in paintEvent ... stringBuffer = self.renderer._renderer.tostring_bgra()". The ordinary GUI-window was [X] closed and after another call the plotting started with a blank/headless TkWindow and finally killed python session to DOS. These was trivial matplotlib XY-point without any interactions / event handlers / no embedding (some 30-50 datapts). – user3666197 Jan 26 '16 at 21:29
  • @Jean-Sébastien have you solved the problem, I am trapped by a similar problem, when embedding matplotlib figure into a PyQt5 GUI, it cost memory but never release even when I close the figure – bactone Nov 25 '21 at 08:29

0 Answers0