2

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.

wtw
  • 678
  • 4
  • 9
  • Would you have the chance to test this with the newest [development version](https://github.com/matplotlib/matplotlib) of matplotlib? – ImportanceOfBeingErnest Dec 06 '18 at 22:31
  • @ImportanceOfBeingErnest Same behaviour on linux with matplotlib version 3.0.2.post1009+g4c1d5c1 (cloned and build this morning) – wtw Dec 07 '18 at 09:44
  • So my initial guess was this to be fixed by [#11972](https://github.com/matplotlib/matplotlib/pull/11972). But you may still read through this. Possibly try without tkinter? – ImportanceOfBeingErnest Dec 07 '18 at 12:18
  • Testing the example code given in #11972 shows no memory increase - good. My code without plotting any lines also has a stable memory, both with without tkinter. However, when I add the plotting of lines back the memory grows again (both with and without tkinter). I've had to use pyplot to display the figure when getting rid of tkinter - possibly I did something wrong here... – wtw Dec 07 '18 at 13:18
  • As a very coarse work around, periodically storing the state of the plot (lines, labels, limits etc) then calling `ax.clear()`, then re-plotting the lines etc works. Interestingly, the memory doesn't seem to go down at `ax.clear()`, and it starts going up again if clear is not called once the same time has elapsed. Also, this only works against the development version, not the release. – wtw Dec 10 '18 at 17:50

0 Answers0