3

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.

Semo
  • 783
  • 2
  • 17
  • 38
  • 1
    you are using more and more memory because you are continuously creating anonymous functions (thats what lambda does) that have to be kept in memory. the only way around this would be using a binding instead of the command callback, and then in your `edit_barcode` figuring out what index the widget is in the list (using event.widget), then using that index to get the item – James Kent Oct 30 '17 at 11:21
  • 2
    as your your comment about tkinter "forgetting" your image, thats not a bug, that is a feature of python, the garbage collector will delete items from memory that are no longer referenced, thats why you have to assign it to a property of the widget, to keep a reference to it. – James Kent Oct 30 '17 at 11:22

1 Answers1

1

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.")
James Kent
  • 5,763
  • 26
  • 50
  • 1
    Thank you James. Your changes work, but the mem-leak persists. I started the application in Debug mode and used "import objgraph" and invoked: "objgraph.show_most_common_types(limit=25)". Here I saw that the method type was incredibly fast growing, though the application was idling around. – Semo Nov 01 '17 at 08:18