0

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?

Michael Clerx
  • 2,928
  • 2
  • 33
  • 47
  • Are you using pyplot or the OO interface? – tacaswell Oct 28 '14 at 20:45
  • The OO interface. There's plenty of questions out there about pyplot/pylab :-) – Michael Clerx Oct 29 '14 at 10:01
  • Feel like providing a minimilistic example that demonstrates the problem? – three_pineapples Oct 29 '14 at 10:24
  • Not exactly minimal. But this is what I've been able to reduce it to... – Michael Clerx Oct 29 '14 at 11:13
  • There is _way_ too much code there. Please strip out all of the ABF file code. Make sure you are calling gc in a way that kills circular refs (figs know about axes which know about the figure). – tacaswell Oct 29 '14 at 14:10
  • Thanks for your effort! The abf code is already stripped out, replaced by dummy code that returns random numpy arrays using the same interface. Isn't gc.collect supposed to know how to find cyclical references? Please show me if/how I'm using matplotlib wrong to allow proper garbage collection – Michael Clerx Oct 29 '14 at 21:37

0 Answers0