1

is there any way to clear matplotlib memory usage from a tkinter application, the following code is taken from Embedding in Tk, i just put it in a loop to make the memory leak more clear.

import tkinter
import matplotlib
print(matplotlib._version.version)
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import (
    FigureCanvasTkAgg, NavigationToolbar2Tk)
import gc
# Implement the default Matplotlib key bindings.
from matplotlib.backend_bases import key_press_handler
from matplotlib.figure import Figure
import psutil
import os, psutil
process = psutil.Process(os.getpid())
import numpy as np
import time

root = tkinter.Tk()
frame = tkinter.Frame(root)
root.wm_title("Embedding in Tk")
import matplotlib.pyplot as plt
def my_func():
    global root,frame
    fig = Figure(figsize=(5, 4), dpi=100)
    t = np.arange(0, 3, .01)
    ax = fig.add_subplot()
    line = ax.plot(t, 2 * np.sin(2 * np.pi * t))
    ax.set_xlabel("time [s]")
    ax.set_ylabel("f(t)")
    canvas = FigureCanvasTkAgg(fig, master=frame)  # A tk.DrawingArea.
    canvas.draw()

    # pack_toolbar=False will make it easier to use a layout manager later on.
    toolbar = NavigationToolbar2Tk(canvas, frame, pack_toolbar=False)
    toolbar.update()


    toolbar.pack(side=tkinter.BOTTOM, fill=tkinter.X)
    canvas.get_tk_widget().pack(side=tkinter.TOP, fill=tkinter.BOTH, expand=True)
    time.sleep(0.1)
    
    # everything i tried to clear memory
    ax = fig.axes[0]
    ax.clear()
    canvas.get_tk_widget().pack_forget()
    toolbar.pack_forget()
    canvas.figure.clear()
    canvas.figure.clf()
    canvas.get_tk_widget().destroy()
    toolbar.destroy()
    mem = process.memory_info().rss/2**20
    print(mem)  # in bytes
    if mem > 1000:
        root.destroy()
    frame.destroy()
    frame = tkinter.Frame(root)
    root.after(10,my_func)
    gc.collect()

if __name__ == "__main__":
    root.after(1000,my_func)
    root.mainloop()

it just keeps eating memory up to 1000 MBs,

i tried everything to remove this memory leak without hope, i tried the answer here, but it also didn't work How to clear memory completely of all matplotlib plots.

just updating the figure instead of creating a new figure on each loop iteration would "avoid" some of the memory leak, but it doesn't "fix it", how do i reclaim this memory ?

this issue seems related https://github.com/matplotlib/matplotlib/issues/20490 but i am using version 3.6.2 which should have it fixed, i can duplicate it on almost all python versions on windows, (but the code in the issue doesn't produce this problem)

tracemalloc only shows around 1 MB was allocated on python side, so the rest of the leak is on C side ... something isn't getting cleaned up.

Edit: this also seems related Tkinter - memory leak with canvas, but the canvases are correctly reclaimed, so it's not a bug in the canvases or tk.

Edit2: the renderer on the C side is not getting freed ... althought there seems to be no reference to it.

Ahmed AEK
  • 8,584
  • 2
  • 7
  • 23
  • You are creating a lot of objects that you don't free from your memory, so I'm not surprised over your results. Why do you need 100 plots in the first place? Is there a conference room full of people that have to work on a single giant screen ? – Thingamabobs Dec 26 '22 at 19:00
  • @Thingamabobs i find myself needed to draw a variable number of plots, so i cannot just draw one as the application starts, i would be constantly creating and destroying plots, maybe an operation would create 15 plots, which would take 0.15 GB of RAM, which i'd like to reclaim when the figure is "closed" or "replaced", also swapping the `figure` causes Tk to bug out, so i cannot just replace the figures and keep the GUI hidden, Tk only allows creating new `FigureCanvasTkAgg` objects. – Ahmed AEK Dec 26 '22 at 19:11
  • isn't the usual way to plot new data instead of new Figures ? – Thingamabobs Dec 26 '22 at 19:47
  • @Thingamabobs then i'd have to pass Figure objects back and forth from front-end to backend on every call to be filled, who should know how many figures to create ? who should create them ? who should own them and pass them around ? this is all to get around a memory leak bug .... writing my own `FigureCanvasTkAgg` would be easier than introducing this much of complexity to the entire project. – Ahmed AEK Dec 26 '22 at 19:56
  • In my view you have a structural problem and you should work on this instead of hunting memory through the libs you are using. The first step to solve the problem is to locate it and normally you are using libs that you don't have to bother what is within the background. So your choice either you can go something on that you can spot or your searching for an improvement of the libs you are using. – Thingamabobs Dec 26 '22 at 20:09

1 Answers1

2

seems like tkinter keeps reference to the canvases callbacks, which prevents matplotlib from deleting its objects, and as callbacks are tied to the toplevel in use, making a new toplevel for each plot seems to correctly delete the references and therefore prevent the memory leak.

import tkinter
import matplotlib
print(matplotlib._version.version)
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import (
    FigureCanvasTkAgg, NavigationToolbar2Tk)
import gc
from matplotlib.figure import Figure
import os, psutil
process = psutil.Process(os.getpid())
import numpy as np
import time

root = tkinter.Tk()
frame = tkinter.Frame(root)
root.wm_title("Embedding in Tk")
new_window = tkinter.Toplevel(master=root)

import matplotlib.pyplot as plt
def test_func():
    global root,frame, new_window
    fig = Figure(figsize=(5, 4), dpi=100)
    canvas = FigureCanvasTkAgg(fig, master=frame)  # A tk.DrawingArea.
    canvas.draw()

    canvas.get_tk_widget().pack(side=tkinter.TOP, fill=tkinter.BOTH, expand=True)
    time.sleep(0.1)
    ax = fig.axes[0]
    ax.clear()
    canvas.get_tk_widget().pack_forget()
    canvas.figure.clear()
    canvas.figure.clf()
    canvas.get_tk_widget().destroy()
    mem = process.memory_info().rss/2**20
    print(mem)  # in bytes
    if mem > 1000:
        root.destroy()
    frame.destroy()
    new_window.destroy()
    new_window = tkinter.Toplevel(master=root)
    frame = tkinter.Frame(new_window)
    gc.collect()
    root.after(10,test_func)

if __name__ == "__main__":
    root.after(1000,test_func)
    root.mainloop()

there are multiple issues on github for matplotlib where they disucessed these problems, but i hope it gets fixed in the future.

https://github.com/matplotlib/matplotlib/pull/22002

for now the workaround is to only create a finite number of FigureCanvasTkAgg and keep modifying them, and hope you never need to reclaim their memory, or just destroy your entire window momentarily, or keep the plots separate from your main window.

canvas = FigureCanvasTkAgg(master=frame)  # A tk.DrawingArea.
toolbar = NavigationToolbar2Tk(canvas, frame, pack_toolbar=False)
toolbar.pack(side=tkinter.BOTTOM, fill=tkinter.X)
canvas.get_tk_widget().pack(side=tkinter.TOP, fill=tkinter.BOTH, expand=True)
fig = canvas.figure

def my_func():
    ax = fig.add_subplot()
    t = np.arange(0, 3, .01)
    line = ax.plot(t, 2 * np.sin(2 * np.pi * t))
    ax.set_xlabel("time [s]")
    ax.set_ylabel("f(t)")
    canvas.draw()
    canvas.get_tk_widget().update()

    # pack_toolbar=False will make it easier to use a layout manager later on.
    toolbar.update()

    time.sleep(0.1)

    # everything i tried to clear memory
    mem = process.memory_info().rss / 2 ** 20
    print(mem)  # in bytes
    if mem > 1000:
        root.destroy()
    root.after(10, my_func)
    fig.clear()
    gc.collect()


if __name__ == "__main__":
    root.after(1000, my_func)
    root.mainloop()

Edit: going with my answer, you could manually reclaim the memory by deleting the references to matplotlib objects yourself using

canvas.get_tk_widget().pack_forget()
toolbar.pack_forget()
canvas.get_tk_widget().destroy()
toolbar.destroy()
frame.destroy()
[delattr(canvas,x) for x in vars(canvas).copy() if x != "_tkcanvas"]

which is a very hacky way to fix the memory leak.

Ahmed AEK
  • 8,584
  • 2
  • 7
  • 23
  • This is an extremely strong answer, particularly the edit. That pull request seems to have been merged by now, and there has been a release, it might be fixed! – Tom Swirly Apr 26 '23 at 11:37