Description
Bug summary
Memory leak from repeated live plotting: seems to be caused by transform._parents
growing continuously over time, as in #11956 and #11972.
Code for reproduction
import datetime
#import tkinter for GUI
import tkinter as tk
from tkinter import W, LEFT
#font types
TITLE_FONT = ("Verdana", 14, 'bold')
LARGE_FONT = ("Verdana", 12)
MEDIUM_FONT = ("Verdana", 10)
SMALL_FONT = ("Verdana", 8)
#import stuff for graph
import matplotlib
from matplotlib import ticker as mticker
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
matplotlib.use('TkAgg')
from matplotlib import figure
from matplotlib import dates as mdates
#import animation to make graph live
import matplotlib.animation as animation
from matplotlib import style
style.use("seaborn-darkgrid")
import random as r
num_contacts = 5
import cProfile
import pstats
import io
#create figure for plots and set figure size/layout
f = figure.Figure(figsize=(16.6,15), dpi=100, facecolor='white')
f.subplots_adjust(top=0.993, bottom=0.015, left=0.04, right = 0.96, hspace=0.65)
param_dict = {}
param_list = ['pH', 'TDS (ppm)', 'Rela. Humidity (%)', 'Air Temp (\N{DEGREE SIGN}C)', 'Water Temp (\N{DEGREE SIGN}C)', 'Water Level (cm)']
param_ylim = [(0, 15), (0, 15), (0, 15), (0, 15), (0, 15), (0, 15)]
live_dict = {}
class Live_Text:
def __init__(self, label):
self.label = label
class Sensor_Plot:
def __init__(self, plot, tList, x_ax, ylim, param, incoming_data, plot_color):
self.plot = plot
self.tList = tList
self.x_ax = x_ax
self.ylim = ylim
self.param = param
self.incoming_data = incoming_data #<- graph is bound by incoming data and Data Summary Table displays most recent value 20 of them
self.plot_color = plot_color #initially 'b' for all
def make_plot(self):
self.plot.clear()
self.plot.set_xlabel('Time')
self.plot.set_ylabel(self.param)
self.plot.set_ylim(self.ylim)
self.x_ax.xaxis_date()
self.x_ax.xaxis.set_major_formatter(mdates.DateFormatter('%a %I:%M:%S %p'))
[tk.set_visible(True) for tk in self.x_ax.get_xticklabels()]
[label.set_rotation(10) for label in self.x_ax.xaxis.get_ticklabels()] #slant the x axis tick labels for extra coolness
if len(self.tList) > 4:
self.x_ax.set_xlim(self.tList[-2], self.tList[0])
self.x_ax.xaxis.set_major_locator(mticker.MaxNLocator(nbins = 4))
self.plot.fill_between(self.tList, self.incoming_data, #where=(self.incoming_data > [0]*len(self.incoming_data))
facecolor=self.plot_color, edgecolor=self.plot_color, alpha=0.5) #blue @initilization
def initialize_plots(): #intiailizes plots...
global initialize_plots
try:
most_recent = [(int(round(datetime.datetime.now().timestamp())),) + tuple([r.randint(1,10), r.randint(1,10), r.randint(1,10), r.randint(1,10), r.randint(1,10), r.randint(1,10)])]
for i, param in enumerate(param_list, 1):
tList = []
most_recent_any_size = []
for j in range(len(most_recent)):
#time_f = datetime.strptime(most_recent[j][0], "%m/%d/%Y %H:%M:%S")
time_f = datetime.datetime.fromtimestamp(most_recent[j][0])
tList.append(time_f)
most_recent_any_size.append(most_recent[j][i])
subplot = f.add_subplot(6, 2, i) # sharex?
x_ax = f.get_axes()
current_plot = Sensor_Plot(subplot, tList, x_ax[i-1], param_ylim[i-1], param, most_recent_any_size, 'b')
param_dict[param] = current_plot
current_plot.make_plot()
except: #if there is no data points available to plot, initialize the subplots
for i, param in enumerate(param_list, 1):
subplot = f.add_subplot(6, 2, i)
x_ax = f.get_axes()
current_plot = Sensor_Plot(subplot, [], x_ax[i-1], param_ylim[i-1], param, [], 'b')
param_dict[param] = current_plot
#current_plot.make_plot()
#reader.commit()
initialize_plots = _plots_initialized
def _plots_initialized(): #ensures plots only intialized once though!
pass
initialize_plots()
###ANIMATE FUNCTION, REMOVE LAST ITEM FROM MOST_RECENT_ANY LIST AND INSERT FRESHLY CALLED VALUE TO BE FIRST IN LIST
def animate(ii):
profile = cProfile.Profile()
profile.enable()
while True:
most_recent_time_graphed = param_dict[param_list[0]] #first, pulls up first plot
#most_recent = reader.query_by_num(table="SensorData", num=1)
#generate fake sensor data here
most_recent = [(int(round(datetime.datetime.now().timestamp())),) + tuple([r.randint(1,10), r.randint(1,10), r.randint(1,10), r.randint(1,10), r.randint(1,10), r.randint(1,10)])] #log time in unix as int
print(most_recent)
#reader.commit() #if identical, do not animate
#then checks that plot's time list
if (len(most_recent) == 0):
break
#time_reader = datetime.strptime(most_recent[0][0], "%m/%d/%Y %H:%M:%S")
time_reader = datetime.datetime.fromtimestamp(most_recent[0][0])
if (len(most_recent_time_graphed.tList) != 0) and (time_reader == most_recent_time_graphed.tList[0]):
for i, param in enumerate(param_list, 1):
current_text = live_dict[param]
current_text.label.config(text=most_recent[0][i], fg="black", bg="white")
break #checks if the timestamp is exactly the same as prior, i.e. no new data points have been logged in this frame
#do I have to add an else?
else:
for i, key in enumerate(param_dict, 1):
current_plot = param_dict[key]
current_param_val = float(most_recent[0][i])
current_text = live_dict[key] #update to live text data summary
current_text.label.config(text=most_recent[0][i], fg="black", bg="white")
current_plot.plot_color = 'g'
data_stream = current_plot.incoming_data
time_stream = current_plot.tList
data_stream.insert(0, most_recent[0][i])
#time_f = datetime.strptime(most_recent[0][0], "%m/%d/%Y %H:%M:%S")
time_f = datetime.datetime.fromtimestamp(most_recent[0][0])
time_stream.insert(0, time_f)
if len(data_stream) < 20: #graph updates, growing to show 20 points
current_plot.make_plot()
else: #there are 20 points and more available, so animation occurs
data_stream.pop()
time_stream.pop()
current_plot.make_plot()
break
if time_reader.minute == 50 and most_recent_time_graphed.tList[1].minute == 49:
profile.disable()
result = io.StringIO()
pstats.Stats(profile,stream=result).print_stats()
result=result.getvalue()
# chop the string into a csv-like buffer
result='ncalls'+result.split('ncalls')[-1]
result='\n'.join([','.join(line.rstrip().split(None,5)) for line in result.split('\n')])
# save it to disk
with open('testing.csv', 'r') as f:
result = f.read() + 'hour: ' + str(time_reader.hour) + '\n' + result
with open('testing.csv', 'w+') as f:
f.write(result)
f.close()
else:
profile.disable()
#initialization
class AllWindow(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
#add title
tk.Tk.wm_title(self, "Matplotlib Live Plotting")
container = tk.Frame(self)
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
#show the frames
self.frames = {}
#remember to add page to this list when making new ones
frame = HomePage(container, self)
#set background color for the pages
frame.config(bg='white')
self.frames[HomePage] = frame
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame(HomePage)
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
#end program fcn triggered by quit button
def die(self):
exit()
#add home page
class HomePage(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self,parent)
canvas = FigureCanvasTkAgg(f, self)
#background = canvas.copy_from_bbox(f.bbox)
canvas.draw()
#embed graph into canvas
canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand = True)
#add navigation bar
toolbar = NavigationToolbar2Tk(canvas, self)
toolbar.update()
#data table labels
for i, param in enumerate(param_list): #tk.Label self refers to Homepage
param_label = tk.Label(self, text=param, fg="black", bg="white",
font = MEDIUM_FONT, borderwidth = 2, relief = "ridge",
width=16, height=1, anchor=W, justify=LEFT)
param_label.place(x=5, y=500+22*i)
for i, param in enumerate(param_list):
loading_text = tk.Label(self, text="Loading", fg="black", bg="white",
font = MEDIUM_FONT, borderwidth = 2, relief = "ridge",
width=7, height=1)
loading_text.place(x=140, y=500+22*i)
current_text = Live_Text(loading_text)
live_dict[param] = current_text
app = AllWindow()
#app.geometry('1280x623')
app.geometry('1917x970')
#update animation first
ani = animation.FuncAnimation(f, animate, interval=100)
#mainloop
app.mainloop()
Actual outcome
The program eventually crashes after running for several days. Memory usage gradually increases throughout the program runtime. Additionally, profiling the animate function revealed that _invalidate_internal
in Matplotlib seemed to be growing in its execution time. This is in line with #11956 and #11972, since _invalidate_internal
is searching through transform._parents
, which grows over time. However, these issues were resolved, so I am unsure why I am encountering the same problem.
Expected outcome
Expect the program to run continuously and indefinitely without memory usage increasing over time.
Operating system
Raspbian GNU/Linux 10 (buster)
Matplotlib Version
3.4.3
Matplotlib Backend
Qt5Agg
Python version
3.9
Jupyter version
No response
Other libraries
No response
Installation
pip
Conda channel
No response