Skip to content

Properly deleting a matplotlib figure embedded in a child window in PySide to free memory #5022

@jnsebgosselin

Description

@jnsebgosselin

Introduction

This issue comes from a question I asked on StackOverflow about how to properly delete a matplotlib figure embedded in a child window in PySide to free memory. Since then, I've worked numerous hours trying to understand the problem by testing and studying the codebase of matplotlib. User tcaswell suggested that I should probably escalate this issue here. Therefore, below I present a reformulated and updated problem description based on my original StackOverflow question.

Problematic

I have an application, from which I open a modless child window by clicking on a button. Within this child window, I produce a matplotlib figure with the object oriented API using the non-GUI backend FigureCanvasAgg. I then convert the figure into a QPixmap, which is then displayed on the child window using a QLabel.

The problem is that, once the figure has been converted into a QPixmap, or after a child window has been closed, the garbage collector seems unable to fully clean the matplotlib figure and a certain amount of memory is not deallocated from the process.

A Minimal Working Example

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

  • if the number in the spin box equals 1, the child window will contain a QLabel displaying a QPixmap produced from an external image. This was to verify this was not a problem with the way I was destroying the child window in qt.
  • if the number in the spin box equals 2, the child window will contain a QLabel displaying a QPixmap produced from a matplotlib figure, using only the non-GUI backend FigureCanvasAgg.

mpl_pyside_memory_rel_qlabel_test py_006

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)
        self.mode.setValue(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 = 10**6
            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)

        num = gc.collect()
        print(num)

if __name__ == '__main__':

    app = QtGui.QApplication(sys.argv)

    w = MyApp()
    w.show()

    sys.exit(app.exec_())

I tested the above application in Ubuntu 15.04 + Python 2.7.9 + Matplotlib 1.4.2 + PySide 1.2.2. A similar behaviour also occurs in Python 3.4.3. When opened from the terminal, the memory used by the application on startup is around 50 MiB. Then if, in mode 2:

  • I, several times, open a single child window and close it, the memory used by the application will gradually rise from around 200 MiB and will stabilize at around 300 MiB after about 5 cycles.
  • I, several times, open a single child window, close it, and explicitly call a gc.collect(), the memory used by the application will vary between 70 and 90 MiB. I've had a run where it got stuck at around 150 MiB for no apparent reason.
  • I do the same process as the above but by opening multiple child windows at the same time and then closing them, the "problem" seems to be worse.

I've just dive recently in the memory-management aspect of Python, so I'm not very knowledgeable in this area. Is it an expected behaviour that I should just get on with or is it worth investigating further or is there something wrong in my approach?

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions