-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
FigureCanvasTkAgg memory leak #24820
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
This version of the tk memory leak is even more fundamental than some of the other open issues, because it doesn't involve If we write the example in a more conventional tkinter style: Longish codeimport itertools
import tkinter as tk
from tkinter import ttk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import numpy as np
counter = itertools.count()
def DisplayGraph(
passed_frame, x_limit=(), delta_x=0.0, y_input="", figsize_x=6, figsize_y=5
):
fig = Figure(figsize=(figsize_x, figsize_y), dpi=100)
ax = fig.add_subplot(111)
ax.grid()
x = np.arange(x_limit[0], x_limit[1], delta_x)
y = eval(y_input)
ax.plot(x, y)
canvas_mat = FigureCanvasTkAgg(fig, master=passed_frame)
canvas_mat.get_tk_widget().pack(anchor="center", expand=True, padx=2, pady=2)
canvas_mat.count = counter.__next__()
# canvas_mat.draw() # redundant!
def update_graph():
for widget in bottom_frame.winfo_children():
widget.destroy()
DisplayGraph(bottom_frame, x_limit=(-10, 10), delta_x=0.1, y_input="2*x+2")
root.after_idle(report)
def report():
import gc, psutil, objgraph
print(gc.collect())
def filt(o):
return isinstance(o, FigureCanvasTkAgg)
objgraph.show_growth(filter=filt)
objgraph.show_backrefs(
[x for x in gc.get_objects() if filt(x) and not x.count],
max_depth=10,
filename="stuff.png",
)
print(psutil.Process().memory_full_info().uss)
root = tk.Tk()
root.geometry("500x500")
top_frame = tk.Frame(root)
ttk.Button(top_frame, text="Update graph", command=update_graph).pack()
top_frame.pack()
bottom_frame = tk.Frame(root)
DisplayGraph(bottom_frame, x_limit=(-10, 10), delta_x=0.1, y_input="2*x+2")
bottom_frame.pack()
root.mainloop() It's no secret that there is a reference cycle here, namely It would be better if this reference didn't live so long, but we can overwrite it by clicking somewhere in the plot before hitting the update button again. This cleans up the graph and reveals the only other things holding From the perspective of the python object graph, these are "leaks" since they have no referents. but what it actually means in this specific case is that the references are "owned" by the tkinter c code. Fortunately, we can see which methods are referenced by the leaks specifically, directing us to the culprit binds: matplotlib/lib/matplotlib/backends/_backend_tk.py Lines 201 to 208 in 84cc898
These are not cleaned up by the But at this point, I'm stuck, because we sorta need those?! and it's not obvious how we could sneak an |
Can we use a weakref in the callbacks rather than |
This works but it's a bit ugly? root = self._tkcanvas.winfo_toplevel()
weakself = weakref.ref(self)
def scroll_event_windows(event):
self = weakself()
if self is None:
return
return self.scroll_event_windows(event)
root.bind("<MouseWheel>", scroll_event_windows, "+")
# Can't get destroy events by binding to _tkcanvas. Therefore, bind
# to the window and filter.
def filter_destroy(event):
self = weakself()
if self is None:
return
if event.widget is self._tkcanvas:
CloseEvent("close_event", self)._process()
root.bind("<Destroy>", filter_destroy, "+") Also it would be good to perform an unbind rather than just short-circuiting the existing bind function. |
un-bind as part of the short-circuit so we will run those at most once? |
Bug summary
I'm trying to update a plot in tkinter by destroying the frame where the FigureCanvasTkAgg tk widget is packed and creating another instance of FigureCanvasTkAgg, but the memory of the script only goes up after repeating the process several times.
Code for reproduction
Actual outcome
Memory after running the script for the first time

Memory after clicking the button 20 times:

I've seen that some of the memory is freed after a while but not immediately after updating the frame.
Expected outcome
I would expect that destroying the frame where the FigureCanvasTkAgg widget is located would allow me to create another instance of FigureCanvasTkAgg without increasing the memory (or at least too much).
Additional information
I've seen posts as far back as 13 years ago reporting the issue in different places, but I have not been able to find a way to solve it. I know that you can create only one instance of the FigureCanvasTkAgg object and clear/plot each time, but I'm working on a project that involves several figures at the same time and closing the tkinter toplevel window does not seem to clear the memory.
Operating system
Windows
Matplotlib Version
3.6.2
Matplotlib Backend
TkAgg
Python version
3.11.0
Jupyter version
No response
Installation
pip
The text was updated successfully, but these errors were encountered: