-
-
Notifications
You must be signed in to change notification settings - Fork 480
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Replace repeated polling in Outbox.loop() with an asyncio event (see #2482) #2867
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for the pull request, @afullerx!
I like this approach very much. There are just a few things I don't quite understand or would implement differently. Can you check my comments, please? 🙂
I just noticed that I completely forgot to read your initial post and immediately jumped into your code instead - probably out of excitement and curiosity to see how you solved this problem. Therefore I missed your valuable explanations and asked kind of redundant questions. Shame on me. 🫤 Especially considering this being your first pull request ever, I really appreciate the effort you've put into the implementation, testing and documentation! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just reviewed and improved the code a bit:
- With
await asyncio.wait_for(self._enqueue_event.wait(), timeout=1.0)
we can await an asyncio event with a timeout. If the timeout occurs, we can simplycontinue
. The while loop will simply continue iterating if_should_stop
is not set. - If there is no client connection, we continue after a short delay. This way we check for a client connection in 0.1-second intervals.
- The 0.1 second delay is a bit more defensive. I guess this is why I had to adjust two pytests.
Regarding the batching of multiple messages: If multiple updates happened within 0.01 seconds, the old implementation waited and submitted multiple updates at once. The new implementation will submit a single update, but only once the currently running task yields and the outbox loop is called for another cycle. So if you synchronously update multiple UI elements, their updates will be batched.
Anyway, @rodja and I will thouroughly review this pull request once again to make sure we don't break anything.
@falkoschindler I like the improvements. I saw just one issue: Prior to Python 3.11 Good point about the natural batching that occurs with synchronous updates. I should have thought of that. It makes me even more curious why I saw an increase in test failures and why adding a sleep fixed it. Hopefully just an anomaly with my low-performance test setup and small changes in timing. |
@falkoschindler Since the addition of |
Fixed in the latest commit. The fix is pretty simple, but figuring out what was going on definitely wasn't. I thought it was going to be an event-related deadlock. It turns out it was some kind of obscure issue preventing task termination when The hangs occurred when This change also provides a useful optimization, since every call to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you so much for your thorough investigation and beautiful solution, @afullerx! It's a nice improvement to avoid creating tasks just to exit immediately because the event is already set. And if it resolves the hanging tests - the better! 😀
This change replaces the continuous polling implementation of the
Outbox.loop()
method with one based on asyncio events. It, along with #2827, resolves issue #2482, while lowering CPU usage at idle by over 80%. (To just 0.02% on my system).I was unable to use the most straightforward approach due to the fact that in versions of Python prior to 3.10, asyncio synchronization primitives attach themselves to the current event loop during initialization. In this case, the event loop that
Outbox
is created on is different from the one executing its loop method. Therefore, I had to go with a slightly more complex lazy initialization approach in order to maintain compatibility with Python 3.8.At first, I removed the
asyncio.sleep()
, reasoning that lower latency in the delivery of messages and updates could result in better performance. However, during automated testing, this led to an increase in the number of random timing related errors. As a result, I concluded that some batching was actually beneficial for system performance. When adding it back, 5ms was chosen as the batch duration because this is the average delay in processing with the current implementation and didn't result in any increase in sporadic errors. This value could likely be optimized further.The automated tests were run using Python 3.8 in the Docker dev container. Ad hoc testing was done natively on Windows 10 using Python 3.12.