Home > other >  Matplotlib memory leak using FigureCanvasTkAgg
Matplotlib memory leak using FigureCanvasTkAgg

Time:12-29

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.

CodePudding user response:

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.

  • Related