1

I've got a simple program that creates a GUI using tkinter. The GUI contains a button that creates a new tkinter Toplevel window each time it is pressed. Each toplevel window contains a matplotlib plot created from a custom Figure class.

from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from Tkinter import *

class MainGUI(Tk):
    def __init__(self):
        Tk.__init__(self)
        Button(self, text="new_child", command=self.new_child).pack()
        self.mainloop()

    def new_child(self):
        self.tl = TL()

class TL(Toplevel):
    def __init__(self, **kw):
        Toplevel.__init__(self, **kw)
        self.data = "00000000000"*10000000  # just some data to analyze memory allocation 
        self._figure = MyFigure(self.data)
        self._canvas = FigureCanvasTkAgg(self._figure, master=self)
        self._canvas.get_tk_widget().grid(row=0, column=0, sticky="NSWE")

class MyFigure(Figure):
    def __init__(self, data):
        super(MyFigure, self).__init__()
        self._data = data

if __name__ == '__main__':
    MainGUI()

The program works as expected, the only problem is that closing a window doesn't free up any memory.

When removing the Figure from the toplevel window, used memory gets freed correctly, so I assume that the Figure causes the memory leak.

I read that reference counting doesn't work for Figure objects that are created using matplotlibs pyplot interface, but this should not apply to my example. (See here for details)

I don't understand what's going on here, so any help would greatly be appreciated.

Thanks in advance.

Edit

I forgot to mention that I already tried to manually invoke the garbage collector with gc.collect(), but that didn't help.

Felix
  • 6,131
  • 4
  • 24
  • 44
  • How do you do your test? I would expect the garbage collector to clean up the memory eventually, assuming the object is no longer in use. – pingul May 31 '17 at 11:53
  • I already tried to manually invoke the gc, but that didn't help. The test is pretty straight-forward. Open/Close child windows, you'll see that the memory used by the program increases with each created child window. – Felix May 31 '17 at 12:03
  • could you not override the `destroy` method of you `TL` class to remove the figure before destroying the window? – James Kent Jun 01 '17 at 12:00

2 Answers2

0

Try calling gc.collect() after the toplevel window has been closed:

def new_child(self):
    tl = TL()
    self.wait_window(tl)   # Wait for tl to be closed before continuing execution
    del tl
    gc.collect()
Josselin
  • 2,593
  • 2
  • 22
  • 35
0

Thank you for this question, and for the answer which helped me a lot. I have found myself in a similar situation with a slightly more complex GUI. I experienced memory leaks despite applying the solution given here. It seems that adding traces to Tkinter variables (e.g. IntVar, DoubleVar, etc) prevents the Toplevel window from being properly destroyed, and causes leaks. Manually removing the traces on destroy fixes this issue. Let me give an example of a code which suffers a leak, and the fix I found. I hope it helps others.

Preliminary: First of all, use the corrected version of new_child given by Josselin:

def new_child(self):
    tl = TL()
    self.wait_window(tl)   # Wait for tl to be closed before continuing execution
    del tl
    gc.collect()

Then, the code with a memory leak (caused by the trace):

class TL(Toplevel):
    def __init__(self, **kw):
        Toplevel.__init__(self, **kw)
        self.number = IntVar()
        self.data = "00000000000"*10000000  # just some data to analyze memory allocation 
        self._figure = MyFigure(self.data)
        self._canvas = FigureCanvasTkAgg(self._figure, master=self)
        self._canvas.get_tk_widget().grid(row=0, column=0, sticky="NSWE")
        Spinbox(self, from_=0, to=10, textvariable=self.number).grid(row=1,column=0)
        self.number.trace('w', self.updateSomething)
    def updateSomething(self, *args): return

And finally, the fix (remove manually the traces on destroy):

class TL(Toplevel):
    def destroy(self):
        for var, s, trid in self.allTraces: var.trace_vdelete(s, trid) #manually removes traces. Otherwise: memory leak!
        super(TL, self).destroy()
    def __init__(self, **kw):
        Toplevel.__init__(self, **kw)
        self.allTraces = []
        self.number = IntVar()
        self.data = "00000000000"*10000000  # just some data to analyze memory allocation 
        self._figure = MyFigure(self.data)
        self._canvas = FigureCanvasTkAgg(self._figure, master=self)
        self._canvas.get_tk_widget().grid(row=0, column=0, sticky="NSWE")
        Spinbox(self, from_=0, to=10, textvariable=self.number).grid(row=1,column=0)
        trid = self.number.trace('w', self.updateSomething)
        self.allTraces.append((self.number, 'w', trid))
    def updateSomething(self, *args): return
Pierric
  • 1
  • 1