Kivy window going to not responding state

105 views
Skip to first unread message

Amitha J Gowda

unread,
Feb 24, 2022, 3:42:57 AM2/24/22
to Kivy users support
Hi,

I'm using Kivy TreeView to present the hierarchical data from a JSON object on the UI. One of the structure takes 10seconds to get displayed on the screen in this duration the UI is going to not responding state until the data is completely presented on the screen. Could you please help me how to get rid of non-responsiveness in my Kivy application?

Thanks!

Elliot Garbus

unread,
Feb 24, 2022, 12:09:03 PM2/24/22
to kivy-...@googlegroups.com

For performance issues the first step is to profile the code there is a discussion here: https://kivy.org/doc/master/api-kivy.app.html#profiling-with-on-start-and-on-stop

Also see: https://docs.python.org/3/library/profile.html

 

Performance issues are often caused by creating lots of widget instances over and over.  For example lets assume you have 500 items you want in the TreeView, you might be creating those 500 widgets when parsing the data and displaying the treeview.  A higher performance approach would create all the required widgets,  keep them in a list and reuse them rather than recreating them.

 

Another option would be to look at how to ‘break up’ a long running task into smaller pieces.  For example assuming you have 1000 actions to do in your 10 seconds, insert a Clock.schedule_once() for every few 100 actions.  Have the callback continue the operation, this lets the Kivy event loop run to process events.  You could also take an approach like this and combine it with a progressbar to communicate to the user there is a long running event.

 

If I had to guess – my guess is you are creating lots of widgets.  But don’t guess start by profiling your code.

If you need more help create a minimal, runnable example and share the example with the data that is causing the slow behavior.

 

Here is a small example of using the profiler.  The code creates a large number of spinners and shows the time to create them, and prints the code profile.

 

from kivy.app import App
from kivy.app import Builder
from kivy.uix.spinner import Spinner
from kivy.core.window import Window
from kivy.clock import Clock

from time import perf_counter
import cProfile
import pstats


kv =
"""
<OptionsSpinner>:
    values: [str(x) for x in range(1, 11)]
   
BoxLayout:
    orientation: 'vertical'
    GridLayout:
        id: grid
        rows:20
        cols: 10
    Label:
        id:label
        size_hint_y: None
        height: 24
        text: 'Test in Progress'
   
"""


class OptionsSpinner(Spinner):
   
pass


class
SpeedTestApp(App):
   
def build(self):
        Window.fullscreen =
'auto'
       
return Builder.load_string(kv)

   
def on_start(self):
        Clock.schedule_once(
self.create_widgets_for, 1# wait for fullscreen...

   
def create_widgets_for(self, dt):
       
self.profile = cProfile.Profile()
       
self.profile.enable()
        start_time = perf_counter()
        r =
self.root.ids.grid.rows
        c =
self.root.ids.grid.cols
       
for i in range(r * c):
           
self.root.ids.grid.add_widget(OptionsSpinner(text=f'OS {i}'))
       
self.for_time = perf_counter() - start_time
       
self.profile.disable()
        stats = pstats.Stats(
self.profile)
        stats.sort_stats(
'cumulative')
        stats.print_stats(
25)
       
print(f'For loop execution time: {self.for_time:.2f}')
       
self.root.ids.label.text = f'For loop execution time: {self.for_time:.2f}'


SpeedTestApp().run()

--
You received this message because you are subscribed to the Google Groups "Kivy users support" group.
To unsubscribe from this group and stop receiving emails from it, send an email to kivy-users+...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/kivy-users/0b715e85-d435-466e-8d0d-1fb39a7ac7c8n%40googlegroups.com.

 

Amitha J Gowda

unread,
Feb 28, 2022, 2:20:22 AM2/28/22
to Kivy users support
Below attached is the minimal version of my application.  populate_treeview is the method that is consuming more time and making the UI non-responsive. Another observation is that when I use already existing JSON file the treeview is populated in 4-5secs but when I dynamically construct the JSON file it is taking 9-10secs, even when I try to construct the file in __init__ part and keep it ready before the function call. I'm unable to figure out what is causing the difference in the execution time even though file is ready in both the cases before calling the recursive function.

import os
import sys
import json
from datetime import datetime
from collections import OrderedDict

from kivy.config import Config
Config.set('graphics', 'resizable', True)

from kivy.metrics import dp
from kivy.uix.label import Label
from kivy.uix.boxlayout import BoxLayout

from kivy.clock import Clock
from time import perf_counter
import cProfile
import pstats


from kivy.lang import Builder
from kivy.uix.spinner import Spinner
from kivy.uix.treeview import TreeViewLabel, TreeViewNode

from kivymd.app import MDApp

cdt_folder = os.getcwd()
dmi_wrap_folder = os.path.join(cdt_folder, "DMIWrapper")
lib_folder = os.path.join(cdt_folder, "Lib")
fw_structs_folder = os.path.join(lib_folder, "FWStructs")
sys.path.append(cdt_folder)
sys.path.append(dmi_wrap_folder)
os.add_dll_directory(dmi_wrap_folder)


import CDTLib
from Enum import *
import CDTContainer
import DMIWrapper
import DMIWrapper.main as dmi

global enum_flag
global enum_values
global node_string
node_string = ""
non_editable = ["rfu" , "reserved" ,"validSig" , "validSignature" , "flashValidSignature"]

kv="""
#:import Thread threading.Thread

BoxLayout:
    orientation: 'vertical'
    MDRectangleFlatButton:
        text: "Retrieve Data"
        id: rd
        #disabled: True
        font_size: "18sp"
        font_stlye: "H6"
        #on_release: Thread(target=app.start).start()
        theme_text_color: "Custom"
        text_color: 1, 1, 1, 1
        halign: "center"
        line_width: 0.5
        padding: [dp(20),dp(20),dp(20),dp(20)]
        #size_hint_x: 0.2
        md_bg_color: 0, 0.52, 0.52, 1

    ScrollView:
        do_scroll_x: True
        do_scroll_y: True
        size_hint_y: 0.45
        scroll_type: ['bars','content']

        TreeView:
            id: tv
            size_hint_y: None
            height: self.minimum_height
            padding: [dp(50),dp(10),dp(20),dp(0)]
            root_options: {'text': 'FW Structure', 'font_size': 15, 'color': (1,1,1,1)}
            font_color: 0, 0, 0, 1

       
    Label:
        id:label
        size_hint_y: None
        height: 24
        text: 'Test in Progress'
       
"""

class MyNode(BoxLayout):

    def __init__(self, **kwargs):
        text = kwargs.pop('text', 'None')
        super(MyNode, self).__init__(**kwargs)
        self.orientation = 'horizontal'

        # make height reasonable
        self.size_hint_y = None
        self.height = dp(25)

        # extract data from the string
        event_list = list(text.split())
        data_type = event_list[0]
        param_name = event_list[1]
        bit_size = event_list[2] + " " + event_list[3]
        val = event_list[4]

        # make the parts of the node
        self.data_type = Label(text=data_type, size_hint_x=0.1)#, color=(0,0,0,1))
        self.param_name = Label(text=param_name, size_hint_x=0.4)#, color=(0,0,0,1))
        self.bit_size = Label(text=bit_size, size_hint_x=0.1)#, color=(0,0,0,1))
        self.val = Label(text=val, size_hint_x=0.1)#, color=(0,0,0,1))

        # add the parts to the BoxLayout
        self.add_widget(self.data_type)
        self.add_widget(self.param_name)
        self.add_widget(self.bit_size)
        self.add_widget(self.val)

        if param_name in non_editable:
            val_list = list()
            val_list.append(val)
        else:
            if val == "0x0":
                val_list = list()
                val_list.append('0x0')
                val_list.append('0x1')
            else:
                val_list = list()
                val_list.append('0x1')
                val_list.append('0x0')

        self.value_options = Spinner(text=val, values=val_list, size_hint=(None, None), size=(100,25), pos_hint={'center_x':.5, 'center_y':.5})
        self.value_options.bind(text=self.close_options)
        self.add_widget(self.value_options)

    def open_options(self, instance):
        self.dropdown.open()

    def close_options(self, value_options, txt):

        self.value_options.text = txt


class MyTreeNode(MyNode, TreeViewNode):
    pass


class CdtSetup():
    def __init__(self):

        self.cdt_container = CDTContainer.CDTContainer()
        # self.database = self.cdtContainer.cdt_telemetry
        self.cdt_logger = self.cdt_container.cdt_logger.logger
        self.log_file_name = self.cdt_container.cdt_logger.log_file_name
        self.error_list = self.cdt_container.error_manager.error_list
        self.cdt_logger.info("Entered CDT.py")
        self.time_start = datetime.now()

        self.cdtlib = CDTLib.CDTLib()

        scan_res = dmi.dmi_scan()
        scan_op = json.loads(scan_res)
        for device in scan_op['response']['results']:
            if "nvm_subsystem_uid" in device['data'].keys():
                alias = device['data']['dev_info']['alias']
                break
        self.cdt_container.alias = alias

        info_op = dmi.dmi_get_info(alias)
        device_info = json.loads(info_op)
        self.cdt_container.fwversion = device_info['response']['results'][0]['data']['firmware_version']
        self.cdt_container.program = device_info['response']['results'][0]['data']['model_name']
       

class ExpApp(MDApp):

    def __init__(self, **kwargs):

        super().__init__(**kwargs)
        self.screen = Builder.load_string(kv)
        self.theme_cls.theme_style = "Dark"
        self.device_handle = CdtSetup()        
        self.screen.ids.rd.bind(on_press = self.start)

    def build(self):
        #Window.fullscreen = 'auto'
        return self.screen    

    def start(self, instance):
        Clock.schedule_once(self.create_widgets_for, 1)

    #def on_start(self):
        #Clock.schedule_once(self.create_widgets_for, 1)  # wait for fullscreen...
       
    def upadte_progress_bar(self):
       
        current = self.screen.ids.pb.value
        current += 33
        self.screen.ids.pb.value = current

    def create_widgets_for(self, dt):
    #def create_widgets_for(self):
        tv = self.screen.ids.tv
       
       
        #with open("C:/Users/user\Desktop\Atlas.json", "r") as all_events:
            #self.json_struct = json.load(all_events, object_pairs_hook=OrderedDict)
       
       
        self.config_buffer = self.device_handle.cdtlib.retrieve(ALL_EVENT)
        with open(TEMP_DIR + "\\AllEvents.json", "r") as all_events:
            self.json_struct = json.load(all_events, object_pairs_hook=OrderedDict)

           
        self.profile = cProfile.Profile()
        self.profile.enable()
        start_time = perf_counter()
        for key in self.json_struct.keys():
            next_parent = tv.add_node(TreeViewLabel(text=key), parent=None)
            self.populate_treeview(self.json_struct[key], tv, parent=next_parent)

           
        self.for_time = perf_counter() - start_time
        self.profile.disable()
        stats = pstats.Stats(self.profile)
        stats.sort_stats('cumulative')
        stats.print_stats(25)
        print(f'Recursiva function execution time: {self.for_time:.2f}')

        self.root.ids.label.text = f'For loop execution time: {self.for_time:.2f}'

    def populate_treeview(self, json_struct, tv, parent=None):
        global enum_flag
        global enum_values

        for attr, attr_val in json_struct.items():
            if type(attr_val) is OrderedDict:
                try:
                    if json_struct[attr]["val"]:
                        self.populate_treeview(attr_val, tv, parent)
                except:
                    next_parent = tv.add_node(TreeViewLabel(text=attr), parent)
                    #next_parent = None
                    #print(attr)
                    self.populate_treeview(attr_val, tv, next_parent)

            else:
                global node_string
                if attr != "EnumOptions":
                    node_string = node_string + attr_val + " "
                if attr == "val":
                    if parent:
                        tv.add_node(MyTreeNode(text=node_string), parent)
                    else:
                        tv.add_node(MyTreeNode(text=node_string))
                    enum_flag = 0
                    enum_values = []
                    node_string = ""


ExpApp().run()

Amitha J Gowda

unread,
Feb 28, 2022, 2:23:31 AM2/28/22
to Kivy users support
This is the JSON file I'm using and trying to present it on the UI like this,

ui_issue.PNG
Default_Events_status.json

Elliot Garbus

unread,
Feb 28, 2022, 6:33:35 PM2/28/22
to kivy-...@googlegroups.com

It is the creation of all of the widgets that go into the Treeview that is taking up most of the time.

 

The approach I would suggest is to separate the recursive parsing of the JSON and the widget creation.

Parse the JSON and create a list of parameters to go into the treeview nodes.  Then create a loop, that processes say 50 lines at a time, and updates  a progress bar.

 

Alternatively if you need to view this kind of data frequently, that is you are viewing different datasets, pre-allocate the widgets and add the data to the pre-allocated widgets.  It is the widget creation that is taking most of the time.  If you comment out most of the code in MyNode.__init__(), you will see that is where most of the time is being spent.

Amitha J Gowda

unread,
Mar 3, 2022, 1:52:08 AM3/3/22
to Kivy users support
I use the JSON data very frequently and it is not constant the values and size of JSON data will be different each time. In this case how to have pre-defined widgets,.

Elliot Garbus

unread,
Mar 3, 2022, 8:19:45 AM3/3/22
to kivy-...@googlegroups.com

The widget is not changing in your code, only the data in the widget.

 

Change your __iniit__ so it will work with out getting any data passed in, this will let you separate widget creation and putting data in the widget.   You could then create a list of widgets using a list comprehension.

 

nodes = [MyNode() for _ in range(1000)]

# this would create a list of 1000 MyNode objects.

 

MyNode instances have a text attribute you could later set the text attribute….

nodes[0].text = ‘The text value’

And then add it to the widget tree…

…add_widget(nodes[0])

 

If you you need more nodes add them to the list.  Never delete the widgets in nodes, just clear the data in them, and use the widgets again.  This way you only pay the cost of creating them once. 

Reply all
Reply to author
Forward
0 new messages