I need to load a big file into a
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:
- less that 0.2 seconds to read from the file and it takes, and
- 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)