Home > OS >  Load Big File into TextInput without blocking UI (python kivy)
Load Big File into TextInput without blocking UI (python kivy)

Time:07-11

I need to load a big file into a screen capture of execution with UI locked for 15 seconds while loading the file

How can I efficiently load the contents of the kivy TextInput with big_file.txt without blocking the UI?

CodePudding user response:

Try using a RecycleView, which is designed for handling large amounts of data. Here is a modified version of your code that uses a RecycleView:

import threading
import time

from kivy.app import App
from kivy.lang import Builder
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.recycleview import RecycleView
from kivy.uix.boxlayout import BoxLayout

class RV(RecycleView):
    pass

Builder.load_string('''
<MyLabel@Label>:
    halign: 'center'
    size_hint: 1, None
    height: dp(25)
    text_size: self.size
<RV>:
    viewclass: 'MyLabel'
    RecycleBoxLayout:
        default_size: None, dp(25)
        default_size_hint: 1, None
        size_hint_y: None
        height: self.minimum_height
        orientation: 'vertical'
''')

class MyApp(App):

    def build(self):
        self.count = 0
        layout = BoxLayout()

        # add the label
        self.label1 = Label(text='0', size_hint_x=0.1)
        layout.add_widget(self.label1)

        # add the button
        layout.add_widget( Button(text='Increment', on_release=self.increment, size_hint_x=0.1))

        # add the RecycleView
        self.rv = RV(size_hint_x=0.7)
        layout.add_widget(self.rv)

        # add the button
        layout.add_widget( Button(text='Load File', on_release=self.load_file, size_hint_x=0.1))

        return layout

    def load_file(self, *args):
        threading.Thread(target=self.actual_load, daemon=True).start()

    def actual_load(self):
        start = time.time()
        print( "loading file" )
        with open( 'big_file.txt' ) as big_file:
            text = big_file.read()
        end = time.time()
        print( "finished loading file in "   str(end-start)   " seconds" )
        lines = []
        start = time.time()
        for line in text.splitlines(keepends=False):
            lines.append({'text': line})
        end = time.time()
        print("finished loading lines in "   str(end-start)   " seconds")
        self.rv.data = lines
        
    def increment(self, *args):
        self.count =1
        self.label1.text = str(self.count)


if __name__ == "__main__":
    MyApp().run()

This uses a Label for each line of text. The RecycleView creates a fixed number of Labels, and just reuses them as you scroll.

CodePudding user response:

The read() isn't actually what's slow. What's slow is the call to update the TextInput.text with a lot of data.

Threaded Solution

You can prevent from locking the UI by creating a background thread that runs asynchronously

import time, threading

from kivy.app import App
from kivy.clock import Clock

from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.textinput import TextInput
from kivy.uix.boxlayout import BoxLayout

class MyApp(App):

    def build(self):
        self.count = 0
        self.update_textinput1_bg = None
        layout = BoxLayout()

        # add the label
        self.label1 = Label(text='0')
        layout.add_widget(self.label1)

        # add the button
        layout.add_widget(
         Button(text='Increment',   on_release=self.increment)
        )

        # add the textinput
        self.textinput1 = TextInput(
         text='Click    button to load file.',
         readonly=True
        )
        layout.add_widget(self.textinput1)

        # add the button
        layout.add_widget(
         Button(text='Load  File', on_release=self.load_file)
        )

        return layout

    # function that's called when the "Load File" button is pressed
    def load_file(self, *args):

        # is there already a thread running attempting to update the textinput?
        if self.update_textinput1_bg != None:
            # a thread is already running; tell the user to be patient

            self.textinput1.text = "Still Loading File. Please be patient."

        else:
            # no background thread is running yet; start one now

            start = time.time()
            print( "loading file" )
            self.textinput1.text = "Loading File. Please wait.."

            with open( 'big_file.txt' ) as big_file:
                file_contents = big_file.read()

            end = time.time()
            print( "finished loading file in "   str(end-start)   " seconds" )

            # we use treading.Thread() instead of multiprocessing.Process
            # because it can update the widget's contents directly without
            # us having to pass data in-memory between the child process.
            # Con: We can't kill threads, so it should only be used for
            # short-running background tasks that won't get stuck
            self.update_textinput1_bg = threading.Thread(
             target = self.update_textinput1,
             args   = (file_contents,)
            )

            self.start = time.time()
            self.update_textinput1_bg.start()

            # Register the update_textinput1_tick() function as a callback to be
            # executed every second, and we'll use that to update the UI with a
            # status message from the update_textinput1() thread and check to see
            # if it finished running
            Clock.schedule_interval(self.update_textinput1_tick, 1)

    # very simple function that updates the contents of the TextInput
    # this is intended to be called in a background Thread so the UI doesn't
    # get locked when "contents" is very large
    def update_textinput1( self, contents, *args ):
        self.textinput1.text = contents

    # callback function that's executed every ~1 second after being secheduled
    # by load_file()
    def update_textinput1_tick( self, dt ):
        print( "called update_textinput1_tick()" )

        # is the background thread still running?
        if not self.update_textinput1_bg.is_alive():
            # the background thread finished running; calculate runtime and
            # unschedule this callback function from being called every second
            end = time.time()
            print( "finished udpdating widget text in "   str(end-self.start)   " seconds" )

            self.update_textinput1_bg = None
            Clock.unschedule( self.update_textinput1_tick )

    # increase the integer displayed in the text of the label widget by one
    def increment(self, *args):
        self.count =1
        self.label1.text = str(self.count)

if  __name__ == "__main__":
    MyApp().run()

Here's an example execution. Note that it takes:

  1. less that 0.2 seconds to read from the file and it takes, and
  2. over 18 seconds to update the TextInput
user@buskill:~/tmp/kivy_file_textinput$ /tmp/kivy_appdir/opt/python3.7/bin/python3.7 main.py 
...
[INFO   ] [Kivy        ] v1.11.1
...
[INFO   ] [Python      ] v3.7.8 (default, Jul  4 2020, 10:00:57) 
[GCC 9.3.1 20200408 (Red Hat 9.3.1-2)]
...
[INFO   ] [Base        ] Start application main loop
loading file
finished loading file in 0.01690673828125 seconds
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
called update_textinput1_tick()
finished udpdating widget text in 18.336608171463013 seconds

Note: Kivy v2.1.0 TextInput Improvements

Note that last year (in 2021), Kivy made some improvements to the slow loading of large data into a TextInput. These were released earlier this year (2022-03-06) in Kivy v2.1.0.

That said, these optimizations appear to be pretty limited. I tested the original code example in both Kivy v1.11.1 (~15 seconds to load the data into the TextInput) and v2.1.0 (~12 seconds)

  • Related