-
-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Properly deleting a matplotlib figure embedded in a child window in PySide to free memory #5022
Description
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.
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?
