KivyMD: Potential memory leak when adding and deleting dynamic widgets

Description of the Bug

First off, I want to say thank you to all the developers who spent their time working on KivyMD. I have used it many times for work and pleasure. It really simplifies simple UI development.

Now for my issue. I am working on a large KivyMD project that involves lots of screens and adding and deleting dynamic widgets from said screens. I noticed that every time I add and clear widgets, that the system memory usage increases. I have let the program run to the point where it will use gigabytes of memory and start eating into the paging/swap file of the computer it is running on.

I have tested this issue on several different configurations and they all suffer from from infinitely increasing RAM usage

  1. Windows 10, Ubuntu 20.04, Ubuntu 22.04
  2. Python 3.8.8, Python 3.10.4
  3. KivyMD 1.0.2, KivyMD 1.0.0dev0
  4. Kivy 2.0.0, Kivy 2.1.0

I looked into some memory profiling tools(Guppy, GC…), but I am not skilled enough in the underlying memory management of python to understand how to troubleshoot it further. I also tried manual garbage collection using gc.collect() in various places in the program and the memory was still not relinquished back to the OS.

My current theory is that calls to self.clear_widgets() are deleting the UI elements from the screen just fine, but there are references to these widgets that are not being destroyed, causing python to never actually release the memory. This problem seems to occur only with KivyMD. I tried running an almost identical program with vanilla Kivy and the memory management issues are not present.

I have two different code snippets below:

  1. kivymd_test.py: This is the KivyMD test program. Run this code and open up Task Manager(System Monitor for Ubuntu) and observe as the RAM usage continuously increases until the program is killed.
  2. kivy_test.py: This is the vanilla Kivy test program. Run this code and you will see that memory is increased for awhile and then eventually memory is given back to the OS. This code seems to have a memory “plateau”. Where the memory usage will climb to a fixed point and not increase after.

Code and Logs

kivymd_test.py

from kivymd.app import MDApp
from kivymd.uix.button import MDRectangleFlatButton
from kivymd.uix.label import MDLabel
from kivy.clock import Clock
from kivymd.uix.floatlayout import MDFloatLayout
from kivymd.uix.textfield import MDTextField

counter = 0

class MainApp(MDApp):
        
    def build(self):
        #Add MDFloatLayout as root widget
        layout = MDFloatLayout()
        #Schedule add_items() function to be called every second 
        self.clock_obj = Clock.schedule_interval(self.add_items, 1)
        return layout
            
    def add_items(self, dt):
        #Increment global counter
        global counter
        counter += 1
        #Clear all widgets
        self.root.clear_widgets()
        label = MDLabel(text="Count " + str(counter), 
                        pos_hint={"x": 0.5, "top": 0.5}, size_hint=(.1, .1))
        button= MDRectangleFlatButton(text="Count " + str(counter),
                                      pos_hint={"x": 0.5, "top": 0.6}, 
                                      size_hint=(.1, .1))
        field = MDTextField(hint_text="Count " + str(counter), mode="fill", 
                            pos_hint={"x": 0.5, "top": 0.7},
                            size_hint=(.1, .1))
        #Add all widgets to parent layout
        self.root.add_widget(label)
        self.root.add_widget(button)
        self.root.add_widget(field)
        return
        
MainApp().run()

kivy_test.py

from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.label import Label
from kivy.clock import Clock
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.textinput import TextInput

counter = 0

class MainApp(App):
        
    def build(self):
        #Add FloatLayout as root widget
        layout = FloatLayout()
        #Schedule add_items() function to be called every second 
        self.clock_obj = Clock.schedule_interval(self.add_items, 0.1)
        return layout
            
    def add_items(self, dt):
        #Increment global counter
        global counter
        counter += 1
        #Clear all widgets
        self.root.clear_widgets()
        label = Label(text="Count " + str(counter), 
                        pos_hint={"x": 0.5, "top": 0.5}, size_hint=(.1, .1))
        button= Button(text="Count " + str(counter),
                                      pos_hint={"x": 0.5, "top": 0.6}, 
                                      size_hint=(.1, .1))
        field = TextInput(hint_text="Count " + str(counter), 
                            pos_hint={"x": 0.5, "top": 0.7},
                            size_hint=(.1, .1))
        #Add all widgets to parent layout
        self.root.add_widget(label)
        self.root.add_widget(button)
        self.root.add_widget(field)
        return
        
MainApp().run()

Versions

  1. Windows 10, Ubuntu 20.04, Ubuntu 22.04
  2. Python 3.8.8, Python 3.10.4
  3. KivyMD 1.0.2, KivyMD 1.0.0dev0
  4. Kivy 2.0.0, Kivy 2.1.0

Closing Remarks

I opened a Stack Overflow question about this issue to see if was something obvious I was missing, but it does not appear to be the case. I also think it is a bug, because an almost identical vanilla Kivy program does not suffer from this issue. Please let me know if I am missing any information. I would also be willing to help troubleshoot the issue further. Thank you for your consideration.

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 50 (28 by maintainers)

Commits related to this issue

Most upvoted comments

@bryant-saunders The problem is binding properties to methods. Links are not cleared.

from kivymd.app import MDApp
from kivymd.theming import ThemableBehavior
from kivy.clock import Clock
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.widget import Widget


class SomeWidget(ThemableBehavior, Widget):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.theme_cls.bind(primary_color=self.some_method)

    def some_method(self, *args):
        pass


class SomeContainer(BoxLayout):
    def remove_widget(self, widget):
        super().remove_widget(widget)
        callabacks = widget.theme_cls.get_property_observers("primary_color")
        print(len(callabacks))


class Test(MDApp):
    def build(self):
        Clock.schedule_interval(self.add_items, 1)
        return SomeContainer()

    def add_items(self, *args):
        self.root.clear_widgets()
        self.root.add_widget(SomeWidget())


Test().run()

This PR tried to solve this problem - https://github.com/kivymd/KivyMD/pull/740/commits/3b94b19b0616fcf5b65ee583abe64856c2ade317 But I no longer remember why these changes were removed from the master branch…