I've created a simple PyQt gui that can open/close files containing time-series data and display graphs using matplotlib. Each new file is displayed on a new tab. When a tab is closed, all references to the figure etc. should be deleted. To tell PyQt to destroy the Qt items, I'm calling deleteLater() on the closing tab.
However, someone's not letting go of the memory :(
I've tried overriding deleteLater() and clearing the figure/axes before calling deleteLater() on the parent but that only frees up a fraction of the memory.
Anyone?
Update: Managed to make a debug example that reproduces some of the behaviour:
#!/usr/bin/env python
# AbfView debug example
# Copyright 2014 Michael Clerx (michael@myokit.org)
import gc
import sys
# PyQt for python 2
import sip
sip.setapi('QString', 2)
sip.setapi('QVariant', 2)
from PyQt4 import QtGui, QtCore
from PyQt4.QtCore import Qt
QtCore.Signal = QtCore.pyqtSignal
QtCore.Slot = QtCore.pyqtSlot
# Matplotlib
import matplotlib
matplotlib.use('Qt4Agg')
import matplotlib.backends.backend_qt4agg as backend
import matplotlib.figure
class AbfView(QtGui.QMainWindow):
"""
Main window
"""
def __init__(self):
super(AbfView, self).__init__()
# Set size, center
self.resize(800,600)
qr = self.frameGeometry()
cp = QtGui.QDesktopWidget().availableGeometry().center()
qr.moveCenter(cp)
self.move(qr.topLeft())
self.create_toolbar()
# Add widget for Abf file tabs
self._tabs = QtGui.QTabWidget()
self._tabs.setTabsClosable(True)
self._tabs.tabCloseRequested.connect(self.action_close)
self.setCentralWidget(self._tabs)
def action_open(self, event):
"""
Mock-up file opening
"""
for i in xrange(1):
filename = 'file_' + str(i) + '.txt'
abf = AbfFile(filename)
self._tabs.addTab(AbfTab(self, abf), filename)
def action_close(self, index):
"""
Called when a tab should be closed
"""
tab = self._tabs.widget(index)
self._tabs.removeTab(index)
if tab is not None:
tab.deleteLater()
gc.collect()
del(tab)
def create_toolbar(self):
"""
Creates this widget's toolbar
"""
self._tool_open = QtGui.QAction('&Open', self)
self._tool_open.setShortcut('Ctrl+O')
self._tool_open.setStatusTip('Open a file')
self._tool_open.setIcon(QtGui.QIcon.fromTheme('document-open'))
self._tool_open.triggered.connect(self.action_open)
self._toolbar = self.addToolBar('tools')
self._toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
self._toolbar.addAction(self._tool_open)
class AbfTab(QtGui.QTabWidget):
"""
A Qt widget displaying an ABF file.
"""
def __init__(self, parent, abf):
super(AbfTab, self).__init__(parent)
self.setTabsClosable(False)
self.setTabPosition(self.East)
self._abf = abf
self._abf.fold_sweeps()
self._abf.set_time_scale(1000)
self._figures = []
self._axes = []
for i in xrange(self._abf.count_data_channels()):
self.addTab(self.create_graph_tab(i), 'AD' + str(i))
for i in xrange(self._abf.count_protocol_channels()):
self.addTab(self.create_protocol_tab(i), 'DA' + str(i))
self.addTab(self.create_info_tab(), 'Info')
def create_graph_tab(self, channel):
"""
Creates a widget displaying the main data.
"""
widget = QtGui.QWidget(self)
# Create figure
figure = matplotlib.figure.Figure()
figure.suptitle(self._abf.filename())
canvas = backend.FigureCanvasQTAgg(figure)
canvas.setParent(widget)
axes = figure.add_subplot(1,1,1)
toolbar = backend.NavigationToolbar2QTAgg(canvas, widget)
# Draw lines
for i, sweep in enumerate(self._abf):
c = sweep[channel]
axes.plot(c.times(), c.values())
# Create a layout
vbox = QtGui.QVBoxLayout()
vbox.addWidget(canvas)
vbox.addWidget(toolbar)
widget.setLayout(vbox)
self._figures.append(figure)
self._axes.append(axes)
return widget
def create_protocol_tab(self, channel):
"""
Creates a widget displaying a stored D/A signal.
"""
widget = QtGui.QWidget(self)
# Create figure
figure = matplotlib.figure.Figure()
figure.suptitle(self._abf.filename())
canvas = backend.FigureCanvasQTAgg(figure)
canvas.setParent(widget)
axes = figure.add_subplot(1,1,1)
toolbar = backend.NavigationToolbar2QTAgg(canvas, widget)
# Draw lines
for i, sweep in enumerate(self._abf.protocol()):
c = sweep[channel]
axes.plot(c.times(), c.values())
# Create a layout
vbox = QtGui.QVBoxLayout()
vbox.addWidget(canvas)
vbox.addWidget(toolbar)
widget.setLayout(vbox)
self._figures.append(figure)
self._axes.append(axes)
return widget
def create_info_tab(self):
"""
Creates a tab displaying information about the file.
"""
widget = QtGui.QTextEdit(self)
widget.setText(self._abf.info(show_header=True))
widget.setReadOnly(True)
return widget
def deleteLater(self):
"""
Deletes this tab (later).
"""
for figure in self._figures:
figure.clear()
for axes in self._axes:
axes.cla()
del(self._abf, self._figures, self._axes)
gc.collect()
super(AbfTab, self).deleteLater()
class AbfFile(object):
"""
Mock-up for abf file class
"""
def __init__(self, filename):
import numpy as np
self._filename = filename
n = 500000
s = 20
self._time = np.linspace(0,6,n)
self._data = []
self._prot = []
for i in xrange(s):
self._data.append(AbfFile.Sweep(self._time,
np.sin(self._time + np.random.random() * 36)))
self._prot.append(AbfFile.Sweep(self._time,
np.cos(self._time + np.random.random() * 36)))
def count_data_channels(self):
return 1
def count_protocol_channels(self):
return 4
def info(self, show_header=False):
return 'fake info'
def fold_sweeps(self):
pass
def set_time_scale(self, scale):
pass
def __iter__(self):
return iter(self._data)
def protocol(self):
return iter(self._prot)
def filename(self):
return self._filename
class Sweep(object):
def __init__(self, time, data):
self._channel = AbfFile.Channel(time, data)
def __getitem__(self, index):
return self._channel
class Channel(object):
def __init__(self, time, data):
self._time = time
self._data = data
def times(self):
return self._time
def values(self):
return self._data
def load():
"""
Loads the Gui, and adds signal handling.
"""
import sys
import signal
app = QtGui.QApplication(sys.argv)
# Close with last window
app.connect(app, QtCore.SIGNAL('lastWindowClosed()'),
app, QtCore.SLOT('quit()'))
# Close on Ctrl-C
def int_signal(signum, frame):
app.closeAllWindows()
signal.signal(signal.SIGINT, int_signal)
# Create main window and show
window = AbfView()
window.show()
# For some reason, PyQt needs the focus to handle the SIGINT catching...
timer = QtCore.QTimer()
timer.start(500) # Flags timeout every 500ms
timer.timeout.connect(lambda: None)
# Wait for app to exit
sys.exit(app.exec_())
if __name__ == '__main__':
load()
To reproduce: Run the program, then click "Open" (or Ctrl-O), and then "Open" again. Now close all tabs. No memory is freed. I'm wondering if this is some kind of performance hack in matplotlib. If so, is there a way to tell it to let the memory go?