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.
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 and PyQt: Dynamic figure runs slow after several loads or looks messy
- PyQt4 / matplotlib - how to release memory?
- Closing and deleteLater-ing PyQt tabs with Matplotlib figures on them doesn't free up memory
- PyQt4 / matplotlib - how to release memory?
- Is deleteLater() necessary in PyQt/PySide?
Matplotlib Documentation :