Tkinter — утечка памяти с canvas

#python #tkinter #tkinter-canvas

#python #tkinter #tkinter-canvas

Вопрос:

У меня есть скрипт на Python, который обрабатывает связь по Modbus. Одной из функций, которую я добавил, был «график», который показывает время отклика вместе со строкой с цветовой кодировкой, указывающей, был ли ответ успешным, имело ли место исключение или ошибка. График — это просто прокручиваемый виджет canvas от Tkinter.

После построения графика определенного количества строк старые строки будут удалены, а затем в конец будет добавлена новая. Для этого примера я установил значение 10, что означает, что на canvas никогда не будет больше 10 строк одновременно.

Код работает корректно, но где-то в этой функции есть утечка памяти. Я позволил ему работать около 24 часов, и через 24 часа потребовалось примерно в 6 раз больше памяти. Функция является частью более крупного класса.

Мое текущее предположение заключается в том, что мой код заставляет размер canvas постоянно «расширяться», что медленно съедает память.

 self.lineList = []
self.xPos = 0

def UpdateResponseTimeGraph(self):
    if not self.graphQueue.empty():
        temp = self.graphQueue.get() #pull from queue. A separate thread handles calculating the length and color of the line. 
        self.graphQueue.task_done()

        lineName     = temp[0] #assign queue values to variables
        lineLength   = temp[1]
        lineColor    = temp[2]

        if len(self.lineList) >= 10: #if more than 10 lines are on the graph, delete the first one.
            self.responseTimeCanvas.delete(self.lineList[0])
            del self.lineList[0]

        #Add line to canvas and a list so it can be referenced.
        self.lineList.append(self.responseTimeCanvas.create_rectangle(self.xPos, self.responseWidth, self.xPos   4, self.responseWidth-lineLength, 
                                                fill=lineColor, outline=''))

        self.xPos  = 5 #will cause the next line to start 5 pixels later. MEMORY LEAK HERE?

        self.responseTimeCanvas.config(scrollregion=self.responseTimeCanvas.bbox(ALL))

        self.responseTimeCanvas.xview_moveto(1.0) #move to the end of the canvas which is scrollable.



    self.graphFrame.after(10, self.UpdateResponseTimeGraph)
  

Одним из решений может быть возврат к началу графика после достижения предела, но я бы предпочел этого не делать, поскольку это может сбить с толку, где начинается график. Обычно у меня гораздо больше ответов, чем 10.

Редактировать:

Я все еще занимаюсь отслеживанием и ошибками, но, похоже, утечку памяти можно устранить с помощью предложения Брайана, если атрибуты строки не изменены с помощью itemconfig. Приведенный ниже код должен быть способен выполняться как есть, если вы используете python 2.7, измените инструкцию import с tkinter на Tkinter (нижний регистр вместо верхнего t). В этом коде будет утечка памяти. Закомментируйте строку itemconfig, и она будет устранена.

 import tkinter
from tkinter import Tk, Frame, Canvas, ALL
import random

def RGB(r, g, b):
    return '#{:02x}{:02x}{:02x}'.format(r, g, b)

class MainUI:
    def __init__(self, master):
        self.master = master
        self.lineList = []
        self.xPos = 0

        self.maxLine = 122

        self.responseIndex = 0 


        self.responseWidth = 100
        self.responseTimeCanvas = Canvas(self.master, height=self.responseWidth)
        self.responseTimeCanvas.pack()

        self.UpdateResponseTimeGraph()

    def UpdateResponseTimeGraph(self):
        self.lineLength   = random.randint(10,99)

        if len(self.lineList) >= self.maxLine:
            self.lineLength = random.randint(5,95)
            self.responseTimeCanvas.coords(self.lineList[self.responseIndex % self.maxLine], self.xPos, self.responseWidth, self.xPos   4, self.responseWidth-self.lineLength)

            #if i comment out the line below the memory leak goes away.
            self.responseTimeCanvas.itemconfig(self.lineList[self.responseIndex % self.maxLine], fill=RGB(random.randint(0,255), random.randint(0,255), random.randint(0,255)))
        else:
            self.lineList.append(self.responseTimeCanvas.create_rectangle(self.xPos, self.responseWidth, self.xPos   4, self.responseWidth-self.lineLength, 
                                                fill=RGB(random.randint(0,255), random.randint(0,255), random.randint(0,255)), outline=''))


        self.xPos  = 5 #will cause the next line to start 5 pixels later. MEMORY LEAK HERE?
        self.responseIndex  = 1

        self.responseTimeCanvas.config(scrollregion=self.responseTimeCanvas.bbox(ALL))

        self.responseTimeCanvas.xview_moveto(1.0) #move to the end of the canvas which is scrollable.



        self.responseTimeCanvas.after(10, self.UpdateResponseTimeGraph)


mw = Tk()
mainUI = MainUI(mw)
mw.mainloop()
  

Комментарии:

1. Я не вижу ничего явно неправильного… но вы могли бы попробовать изменить существующий Canvas объект в lineList[0] с помощью itemconfigure() метода виджета и модифицировать прямоугольник, который уже существует, вместо того, чтобы удалять его и создавать новый, как вы сейчас делаете.

2. Это мой план Б прямо сейчас. Я думаю, что это выглядит лучше, когда постоянно добавляется в конец, и это делает более четким порядок того, что произошло. У меня более старая версия этого скрипта, и утечка существует, но не вызывает проблем, если я не запускаю его в течение нескольких дней при выполнении быстрого чтения. После этого он зависнет или выйдет из строя.

3. Вы можете изменить порядок существующих элементов в Canvas отображаемом списке, вызвав методы виджета tag_Lower() or tag_raise() .

Ответ №1:

Базовый tk canvas не использует повторно идентификаторы объектов. Всякий раз, когда вы создаете новый объект, генерируется новый идентификатор. Память этих объектов никогда не восстанавливается.

Примечание: это память внутри встроенного интерпретатора tcl, а не память, управляемая python.

Решение заключается в перенастройке старых, больше не используемых элементов, а не в их удалении и создании новых.

Комментарии:

1. Я не знаю внутренностей так, как вы, но, похоже, вы подтверждаете, что мое предложение позволит избежать проблемы.

2. @martineau: Я не видел вашего предложения до сих пор, но да, мы согласны с решением.

3. Спасибо. @martineau Возможно, я неверно истолковал ваше предложение. Я изменил свой код, чтобы, надеюсь, исправить эту проблему. Вместо того, чтобы удалять строку, когда она достигает предела, я беру первую строку и помещаю ее в конец. На данный момент я не изменяю никаких других атрибутов. Это означает, что canvas все еще «расширяется», но похоже, что утечки памяти больше нет. Мне придется дать ему поработать несколько часов, прежде чем я смогу быть уверен. Я обновлю этот поток, как только он будет работать достаточно долго

4. @Dave1551: FWIW, это звучит в принципе правильно, поскольку я не думаю , что отсутствие вызова itemconfigure() для изменения атрибутов объекта также будет иметь значение — но опять же, я мало что знаю о внутренних деталях.

5. Утечка памяти исчезла, если я не использую itemconfig для изменения цвета строк. При простом перемещении строки объем памяти начинался с 6,8 МБ, затем довольно быстро увеличился до 7,2, а затем в конечном итоге снизился до 2 МБ, что я не могу объяснить. При изменении цвета с помощью itemcofig объем памяти медленно увеличивается. Я добавил автономный код к своему исходному сообщению, если кто-нибудь захочет поэкспериментировать и с этим.

Ответ №2:

Вот код без утечки памяти. Первоначальным источником утечки было то, что я удалил старую строку, а затем создал новую. Это решение перемещает первую строку в конец, а затем изменяет ее атрибуты по мере необходимости. У меня была вторая «утечка» в моем примере кода, где я каждый раз выбирал случайный цвет, что приводило к тому, что количество используемых цветов съедало много памяти. Этот код просто печатает зеленые строки, но длина будет случайной.

 import tkinter
from tkinter import Tk, Frame, Canvas, ALL
import random

def RGB(r, g, b):
    return '#{:02x}{:02x}{:02x}'.format(r, g, b)

class MainUI:
    def __init__(self, master):
        self.master = master
        self.lineList = []
        self.xPos = 0

        self.maxLine = 122

        self.responseIndex = 0 


        self.responseWidth = 100
        self.responseTimeCanvas = Canvas(self.master, height=self.responseWidth)
        self.responseTimeCanvas.pack()

        self.UpdateResponseTimeGraph()

    def UpdateResponseTimeGraph(self):
        self.lineLength   = random.randint(10,99)

        if len(self.lineList) >= self.maxLine:
            self.lineLength = random.randint(5,95)
            self.responseTimeCanvas.coords(self.lineList[self.responseIndex % self.maxLine], self.xPos, self.responseWidth, self.xPos   4, self.responseWidth-self.lineLength)

            self.responseTimeCanvas.itemconfig(self.lineList[self.responseIndex % self.maxLine], fill=RGB(100, 255, 100))
        else:
            self.lineList.append(self.responseTimeCanvas.create_rectangle(self.xPos, self.responseWidth, self.xPos   4, self.responseWidth-self.lineLength, 
                                                fill=RGB(100, 255, 100), outline=''))


        self.xPos  = 5 #will cause the next line to start 5 pixels later. 
        self.responseIndex  = 1

        self.responseTimeCanvas.config(scrollregion=self.responseTimeCanvas.bbox(ALL))

        self.responseTimeCanvas.xview_moveto(1.0) #move to the end of the canvas which is scrollable.



        self.responseTimeCanvas.after(10, self.UpdateResponseTimeGraph)


mw = Tk()
mainUI = MainUI(mw)
mw.mainloop()