Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to solve a tkinter memory leak?

I have a dynamic table with a fixed row number (like a FIFO Queue), which updates continuously through tkinter's after() function. Inside the table is a Button, which text is editable.

To make the Button's text editable I used the solution of BrenBarn and reference a loop variable into a function call at the command-attribute.

When the function update_content_items() is cycled, I found, that the memory usage is increasing MB by MB per second. I can confirm that after commenting out the lambda expression, the memory leak was gone. (as seen live running 'top' in the terminal)

It seems I have to use the lambda, otherwise the Button will have a wrong index and the user edits the wrong row, when I just used self.list_items[i], though the user clicked the right one.

Is there a way to solve the problem? How can the user click the right button and edit it while having the right index and getting rid of the leak?

The corresponding code:

    def update_content_items(self):
    """
    Continuously fills and updates the Table with rows and content.
    The size of the table rows is initially fixed by an external value at config.ini
    :return: nothing
    """
    if len(self.list_items) > self.queueMaxlen:
        self.queueMaxlen = len(self.list_items)
        self.build_table()

    try:
        for i in range(len(self.list_items)):
            item = self.list_items[i]
            self.barcodeImgList[i].image = item.plateimage
            orig_image = Image.open(io.BytesIO(item.plateimage))
            ein_image = ImageTk.PhotoImage(orig_image)
            self.barcodeImgList[i].configure(image=ein_image)
            # keeps a reference, because somehow tkinter forgets it...??? Bug of my implementation???
            self.barcodeImgList[i].image = ein_image
            orig_image = None
            ein_image = None
            #FIXME Memory LEAK?
            self.numberList[i].configure(text=item.number,
                                         command=lambda K=i: self.edit_barcode(self.list_items[K]))
            self.timestampList[i].configure(text=item.timestamp)
            self.search_hitlist[i].config(bg='white', cursor="xterm")
            self.search_hitlist[i].unbind("<Button-1>")

            if item.queryresult is not None:
                if item.queryresult.gesamtstatus != 'Gruen':
                    self.search_hitlist[i].insert(tk.END, item.queryresult.barcode +
                                                  '\n' + item.queryresult.permitlevel)
                    self.search_hitlist[i].configure(bg='red', cursor="hand2")
                    self.search_hitlist[i].bind("<Button-1>", item.url_callback)
                else:
                    self.search_hitlist[i].configure(bg='green', cursor="xterm")
            self.search_hitlist[i].configure(state=tk.DISABLED)
        self.on_frame_configure(None)
        self.canvas.after(10, self.update_content_items)
    except IndexError as ie:
        for number, thing in enumerate(self.list_items):
            print(number, thing)
        raise ie

def edit_barcode(self, item=None):
    """
    Opens the number plate edit dialogue and updates the corresponding list item.
    :param item: as Hit DAO
    :return: nothing
    """
    if item is not None:
        new_item_number = EditBarcodeEntry(self.master.master, item)
        if new_item_number.mynumber != 0:
            item.number = new_item_number.mynumber
            self.list_items.request_work(item, 'update')
            self.list_items.edit_hititem_by_id(item)
            self.parent.master.queryQueue.put(item)
    else:
        print("You shouldn't get here at all. Please see edit_barcode function.")

EDIT: It seems there is indeed a deeper memory leak (python itself). The images won't get garbage collected. Memory is slowly leaking in Python 3.x and I do use PIL. Also here: Image loading by file name memory leak is not properly fixed

What can I do, because I have to cycle through a list with records and update Labels with images? Is there a workaround? PhotoImage has no explicit close() function, and if I call del, the reference is gc'ed and no configuring of the Label possible.

like image 957
Semo Avatar asked Feb 15 '26 00:02

Semo


1 Answers

an example of my proposed changes, with indentation fixed:

def update_content_items(self):
    """
    Continuously fills and updates the Table with rows and content.
    The size of the table rows is initially fixed by an external value at config.ini
    :return: nothing
    """
    if len(self.list_items) > self.queueMaxlen:
        self.queueMaxlen = len(self.list_items)
        self.build_table()

    try:
        for i in range(len(self.list_items)):
            item = self.list_items[i]
            self.barcodeImgList[i].image = item.plateimage
            orig_image = Image.open(io.BytesIO(item.plateimage))
            ein_image = ImageTk.PhotoImage(orig_image)
            self.barcodeImgList[i].configure(image=ein_image)
            # keeps a reference, because somehow tkinter forgets it...??? Bug of my implementation???
            self.barcodeImgList[i].image = ein_image
            orig_image = None
            ein_image = None

            self.numberList[i].configure(text=item.number) # removed lambda
            self.numberList[i].bind("<Button-1>", self.edit_barcode_binding) # added binding
            self.timestampList[i].configure(text=item.timestamp)
            self.search_hitlist[i].config(bg='white', cursor="xterm")
            self.search_hitlist[i].unbind("<Button-1>")

            if item.queryresult is not None:
                if item.queryresult.gesamtstatus != 'Gruen':
                    self.search_hitlist[i].insert(tk.END, item.queryresult.barcode +
                                                  '\n' + item.queryresult.permitlevel)
                    self.search_hitlist[i].configure(bg='red', cursor="hand2")
                    self.search_hitlist[i].bind("<Button-1>", item.url_callback)
                else:
                    self.search_hitlist[i].configure(bg='green', cursor="xterm")
            self.search_hitlist[i].configure(state=tk.DISABLED)
        self.on_frame_configure(None)
        self.canvas.after(10, self.update_content_items)
    except IndexError as ie:
        for number, thing in enumerate(self.list_items):
            print(number, thing)
        raise ie

def edit_barcode_binding(self, event): # new wrapper for binding
    K = self.numberList.index(event.widget) # get index from list
    self.edit_barcode(self.list_items[K]) # call the original function

def edit_barcode(self, item=None):
    """
    Opens the number plate edit dialogue and updates the corresponding list item.
    :param item: as Hit DAO
    :return: nothing
    """
    if item is not None:
        new_item_number = EditBarcodeEntry(self.master.master, item)
        if new_item_number.mynumber != 0:
            item.number = new_item_number.mynumber
            self.list_items.request_work(item, 'update')
            self.list_items.edit_hititem_by_id(item)
            self.parent.master.queryQueue.put(item)
    else:
        print("You shouldn't get here at all. Please see edit_barcode function.")
like image 144
James Kent Avatar answered Feb 19 '26 18:02

James Kent



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!