Problem:
I have a tkinter window with a matplotlib graph showing some lines (part of a much larger program). Periodically I add a new line, deleting an oldest one to limit the number of lines shown. Unfortunately the program grows in size continuously. I've seen this on both Windows and Linux.
Question:
How should I write a long-running graph frame to show the last n lines which doesn't use up all my memory? Preferably by tweaking the code below, but I'm prepared for a complete re-write if required.
I've stripped the offending parts and been doing some testing.
The crucial call appears to be canvas.draw()
. Even if no lines are added to the figure the memory used gradually increases with time. Without this call the memory does not increase. The memory use increases faster when more lines are present in the plot. If the axes are not created (i.e. no fig.add_subplot(1, 1, 1)
) then there is no increase in memory.
Deleting lines and reclaiming memory from matplotlib has been discussed before (How to remove lines in a Matplotlib plot). A weakref to the line being deleted confirms that it is being removed, and as the issue stil exists even when no lines are plotted I suspect that this is not the root cause.
The closest similar problem seems to be Declaration of FigureCanvasTkAgg causes memory leak, where calls to the axes for plotting (rather than to pyplot) solved the issue. This does not seem to apply in this case - I'm not using pyplot for my plotting, I still see the memory increase if I plot directly on the axes (ax.plot(x, y, '.-')
), and again, I still see the issue when there are no lines plotted.
Memory usage was monitored using mprof, and I've also watched it happen using system diagnostics on Windows7. An example of the memory usage as seen by mprof: mprof plot of memory usage increasing
Here's the code I've presently got. This has been much cut down from the initial use case (_refresh_plot
would not normally do anything unless new data had been added to a queue from another thread, etc), but still shows the issue.
import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib as mpl
import weakref
class PlotFrame:
def __init__(self, parent):
self.fig = mpl.figure.Figure()
self.canvas = FigureCanvasTkAgg(self.fig, master=parent)
self.canvas.get_tk_widget().pack()
self.ax = self.fig.add_subplot(1, 1, 1)
self.ax.set_ylim((0, 150))
self.ax.set_xlim((0, 12))
self.counter = 0
timer = self.fig.canvas.new_timer(interval=20)
timer.add_callback(self._refresh_plot, ())
timer.start()
def _refresh_plot(self, arg):
if True: # True to plot 1 or more lines, False for no lines at all
if len(self.ax.lines) > 2: # Remove the oldest line
existing_lines = self.ax.get_lines()
# r = weakref.ref(existing_lines[0]) # weakref to check that the line is being correctly removed
old_line = existing_lines.pop(0)
old_line.remove()
del old_line, existing_lines
# print('Old line dead? {}'.format(r() is None)) # Prints Old line dead? True
# Define a line to plot
x = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
y = [(v - abs(len(x) - self.counter % (2 * len(x)))) ** 2 for v in x]
self.counter += 1
new_line = mpl.lines.Line2D(x, y, linestyle='-', marker='.')
self.ax.add_line(new_line)
del new_line, x, y
self.canvas.draw()
if __name__ == "__main__":
root = tk.Tk()
cpp = PlotFrame(root)
root.mainloop()
Apologies for the long post. I've been staring at this for so long now that it may well be a silly error on my part.