GUI very slow when multithreading PyQt
-
This post is deleted!
-
This post is deleted!
@mayaduchia
Hello and welcome.While waiting for someone to tell you how best to write this from Python:
-
Calling
QApplication.processEvents()in a tight loop is "costly". And you never get out of theonControllerOn()slot, your whole UI now runs on theprocessEvents()instead of allowing it back to the normal, top-level Qt event loop. -
What does your
#get input from controllerin a tight loop do? Yes it is in a secondary thread, but if that thread is constantly busy it will impact time for the UI thread.
-
-
@JonB thank you for your comment. I added the QApplication.processEvents() because otherwise the while loop would have a delay when updating the reportProgressxyz() function. I don't have any other solution for it (as far as I know). Is there another option that replaces the QApplication.processEvents()?
And regarding the #get input from controller, it is where I update my GUI (I am changing position in a plot) using the input of the controller at every iteration of the while loop. I don't know if the explanation is clear, if not please do tell me. -
@JonB thank you for your comment. I added the QApplication.processEvents() because otherwise the while loop would have a delay when updating the reportProgressxyz() function. I don't have any other solution for it (as far as I know). Is there another option that replaces the QApplication.processEvents()?
And regarding the #get input from controller, it is where I update my GUI (I am changing position in a plot) using the input of the controller at every iteration of the while loop. I don't know if the explanation is clear, if not please do tell me.@mayaduchia
Usually we basically never callprocessEvents()for choice, the top-level event loop does the event processing and we leave it to it.processEvents()can be used in situations where code would block/take a long time, but there are issues and it is not desirable.And regarding the #get input from controller, it is where I update my GUI (I am changing position in a plot) using the input of the controller at every iteration of the while loop.
Nope! This loop is running in your non-UI, secondary worker thread. Qt has an absolute rule that UI operations can only be performed in the main UI thread, else havoc/crash. You must access the UI directly in any other thread, at most such a thread can emit signals which the main UI thread has a slot on, so that the main thread only performs UI operations in response.
When I integrated the controller module, I created a while loop to handle all controller inputs, but the application crashed. So to prevent the application from crashing, I used the multithreading solution
Truly out of the frying pan but into the fire! ;-)
Basically it is a "shame" that you are having to use threads at all. Half of beginners rush in where angels fear to tread, threading in Qt (and elsewhere!) is about the hardest thing to get right and to be avoided if at all possible, certainly if you are a beginner.
You are going to want to start by understanding how your "controller/joystick", and the "pygame library" work and interact. How are joystick events delivered? If the pygame stuff is blocking/synchronous, you might run that alone in a thread and have it just emit signals which the main UI thread receives and deals with.
-
@mayaduchia
Usually we basically never callprocessEvents()for choice, the top-level event loop does the event processing and we leave it to it.processEvents()can be used in situations where code would block/take a long time, but there are issues and it is not desirable.And regarding the #get input from controller, it is where I update my GUI (I am changing position in a plot) using the input of the controller at every iteration of the while loop.
Nope! This loop is running in your non-UI, secondary worker thread. Qt has an absolute rule that UI operations can only be performed in the main UI thread, else havoc/crash. You must access the UI directly in any other thread, at most such a thread can emit signals which the main UI thread has a slot on, so that the main thread only performs UI operations in response.
When I integrated the controller module, I created a while loop to handle all controller inputs, but the application crashed. So to prevent the application from crashing, I used the multithreading solution
Truly out of the frying pan but into the fire! ;-)
Basically it is a "shame" that you are having to use threads at all. Half of beginners rush in where angels fear to tread, threading in Qt (and elsewhere!) is about the hardest thing to get right and to be avoided if at all possible, certainly if you are a beginner.
You are going to want to start by understanding how your "controller/joystick", and the "pygame library" work and interact. How are joystick events delivered? If the pygame stuff is blocking/synchronous, you might run that alone in a thread and have it just emit signals which the main UI thread receives and deals with.
@JonB Thank you so much for your answer. I will look into it.
-
Hi,
I don't remember whether the module is available with Python but Qt 5 at least had the Qt Gamepad.
-
I don't know Python, but only C++. Maybe someone can help to translate, if necessary.
Your problem is your control loop running processEvents(). This has terrible performance (I used to do it in the same way, but notice something like a 100x performance hit). You are most likely correct that
reportProgressxyzneeds to be called inside the GUI thread. The easiest solution is for your controller thread to emit a signal (from your separate thread) which is connected to a slot in the GUI thread. This just works.You should also make sure that reportProgressxyz is not called to often. This will also slow down the GUI significantly. I personally have the problem with a regular progress bar. What I started adapting is the use of QTime (C++):
QTime time; time.start(); while(...) { if(time.elapsed() >= 50) // use whatever time fits your case best instead of 50ms { //reportProgressxyz time.restart(); } }50ms is good for my progress bar, but not suitable in your case. 16ms is the time for a single frame at 60Hz. So, maybe do something like 8ms or 4ms.
There is another way to execute reportProgressxyz inside the GUI thread from another thread (again C++):
QMetaObject::invokeMethod(qApp, [&](){ this->reportProgressxyz(worker->x, worker->y, worker->z); });For performance: never block the GUI thread. Even not when calling processEvents().
-
Hi,
I don't remember whether the module is available with Python but Qt 5 at least had the Qt Gamepad.
@SGaist There is no Qt Gamepad support on PyQt5
-
I don't know Python, but only C++. Maybe someone can help to translate, if necessary.
Your problem is your control loop running processEvents(). This has terrible performance (I used to do it in the same way, but notice something like a 100x performance hit). You are most likely correct that
reportProgressxyzneeds to be called inside the GUI thread. The easiest solution is for your controller thread to emit a signal (from your separate thread) which is connected to a slot in the GUI thread. This just works.You should also make sure that reportProgressxyz is not called to often. This will also slow down the GUI significantly. I personally have the problem with a regular progress bar. What I started adapting is the use of QTime (C++):
QTime time; time.start(); while(...) { if(time.elapsed() >= 50) // use whatever time fits your case best instead of 50ms { //reportProgressxyz time.restart(); } }50ms is good for my progress bar, but not suitable in your case. 16ms is the time for a single frame at 60Hz. So, maybe do something like 8ms or 4ms.
There is another way to execute reportProgressxyz inside the GUI thread from another thread (again C++):
QMetaObject::invokeMethod(qApp, [&](){ this->reportProgressxyz(worker->x, worker->y, worker->z); });For performance: never block the GUI thread. Even not when calling processEvents().
@SimonSchroeder Thank you for your comment. I tried to use the QTime solution and removed QApplication.processEvents() from the while loop, it didn't work, the application crashed right away. Also when I used a signal and consider reportProgressxyz as a slot, for some reason the UI don't get updated.
-
@SimonSchroeder Thank you for your comment. I tried to use the QTime solution and removed QApplication.processEvents() from the while loop, it didn't work, the application crashed right away. Also when I used a signal and consider reportProgressxyz as a slot, for some reason the UI don't get updated.
@mayaduchia said in GUI very slow when multithreading PyQt:
the application crashed right away
What does the debugger tell you?
Please post your current code... -
@mayaduchia said in GUI very slow when multithreading PyQt:
the application crashed right away
What does the debugger tell you?
Please post your current code...@jsulm here's my code:
class Window(QMainWindow): def init(self): #create button for controller controllerOn = QPushButton("Controller On") controllerOn.clicked.connect(self.onControllerOn) #create frame to display plot self.frame = QtWidgets.QFrame() vbox = QVBoxLayout() self.plotter = QtInteractor(self.frame) vbox.addWidget(self.plotter.interactor) def onControllerOn(self): if(self.controllerFirstConnexion == True): #create thread to handle controller inputs self.thread = QThread() self.worker = Worker() self.worker.moveToThread(self.thread) #do all the connections self.thread.started.connect(self.worker.run) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.thread.deleteLater) #start thread self.thread.start() while(self.worker.controllerConnected == True): QApplication.processEvents() self.reportProgressxyz(self.worker.x, self.worker.y, self.worker.z) def reportProgressxyz(self, x, y, z): #update point in plot points = np.array([x,y,z]) self.plotter.add_points(points, render_points_as_spheres=True, point_size=8.0, color ='r') class Worker(QObject): def run(self): pygame.init() joysticks = [] #connect controller for i in range(0, pygame.joystick.get_count()): joysticks.append(pygame.joystick.Joystick(i)) joysticks[-1].init() self.controllerConnected = True if pygame.joystick.get_count() == 1: while (self.controllerConnected == True) : for event in pygame.event.get(): if event.type == pygame.JOYAXISMOTION: if(event.axis == 5): if (event.value>0.9): self.rightTrigger = 1 if(event.value>-0.5 and event.value<0.9): self.rightTrigger = 0 if(event.axis == 4): if (event.value>0.9): self.leftTrigger = 1 if(event.value>-0.5 and event.value<0.9): self.leftTrigger = 0 while self.rightTrigger == 1 and self.leftTrigger == 0: self.x+=1 while self.rightTrigger == 0 and self.leftTrigger == 1: self.y+=1 while self.rightTrigger == 1 and self.leftTrigger == 1: self.x+=1 self.y+=1 -
@mayaduchia said in GUI very slow when multithreading PyQt:
the application crashed right away
What does the debugger tell you?
Please post your current code... -
@jsulm here's my code:
class Window(QMainWindow): def init(self): #create button for controller controllerOn = QPushButton("Controller On") controllerOn.clicked.connect(self.onControllerOn) #create frame to display plot self.frame = QtWidgets.QFrame() vbox = QVBoxLayout() self.plotter = QtInteractor(self.frame) vbox.addWidget(self.plotter.interactor) def onControllerOn(self): if(self.controllerFirstConnexion == True): #create thread to handle controller inputs self.thread = QThread() self.worker = Worker() self.worker.moveToThread(self.thread) #do all the connections self.thread.started.connect(self.worker.run) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.thread.deleteLater) #start thread self.thread.start() while(self.worker.controllerConnected == True): QApplication.processEvents() self.reportProgressxyz(self.worker.x, self.worker.y, self.worker.z) def reportProgressxyz(self, x, y, z): #update point in plot points = np.array([x,y,z]) self.plotter.add_points(points, render_points_as_spheres=True, point_size=8.0, color ='r') class Worker(QObject): def run(self): pygame.init() joysticks = [] #connect controller for i in range(0, pygame.joystick.get_count()): joysticks.append(pygame.joystick.Joystick(i)) joysticks[-1].init() self.controllerConnected = True if pygame.joystick.get_count() == 1: while (self.controllerConnected == True) : for event in pygame.event.get(): if event.type == pygame.JOYAXISMOTION: if(event.axis == 5): if (event.value>0.9): self.rightTrigger = 1 if(event.value>-0.5 and event.value<0.9): self.rightTrigger = 0 if(event.axis == 4): if (event.value>0.9): self.leftTrigger = 1 if(event.value>-0.5 and event.value<0.9): self.leftTrigger = 0 while self.rightTrigger == 1 and self.leftTrigger == 0: self.x+=1 while self.rightTrigger == 0 and self.leftTrigger == 1: self.y+=1 while self.rightTrigger == 1 and self.leftTrigger == 1: self.x+=1 self.y+=1@mayaduchia I don't see you doing this: "Also when I used a signal and consider reportProgressxyz as a slot".
And you also ignored my question regarding debugger...