Ensure execution order of callbacks that are expected to be called immediately (such as call_soon) #123
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Summary
Ensure execution order of callbacks with zero delay (such as call_soon) by handling them independently of delayed callbacks.
It's reasonable to assume that custom event loops would adhere to the contract established in the
asyncio
docs. Quoting the asyncio docs:This PR accomplishes it by running a separate timer (with 0 timeout) for servicing immediate callbacks. According to Qt documentation, this timer will run each time the Qt event loop finishes servicing window events (https://doc.qt.io/qt-6/qtimer.html#interval-prop), which should ensure responsive operation.
Issue was observed on Windows.
I hope someone more versed in asyncio and Qt can chime in here. I see this fix more as a best-guess workaround, as I didn't dive into why the 0-delay timer callbacks would get executed out-of-order in the first place. Some quick research didn't give clear clues on whether we can expect any execution order guarantees from the original method.
Some background
Under heavy UI load (such as real-time plotting with pyqtgraph), it was observed that the zero-delay timers scheduled via _SimpleTimer would occasionally run out-of-order on Windows. In these cases multiple immediate callbacks would be serviced within a single event loop iteration.
Unfortunately I haven't been able to get a minimum reproducible example working despite my best efforts to artificially abuse the event loop, but I'll give some overview on how the issue was observed.
We have an application that reads a realtime data stream from a BLE device (using Bleak). New values may be received up to 400 times per second, with each update causing Bleak to use
call_soon
to queue the processing of the value.As observed, each update roughly goes through these stages:
call_soon_threadsafe
, which makes Qt emit the passed callback as a signalcall_soon
(from a different thread than Bleak used)call_soon
tocall_later
withdelay=0
call_later
builds anasyncio.Handle
and passes it to_add_callback
_add_callback
redirects to the_SimpleTimer
, which registers a timer with a timeout of 0timerEvent
methodThe out-of-order issue was observed by adding tracing to each respective stage, where the 6th stage would occasionally call the callbacks out of order, like so: [1, 2, 3, 4, 6, 5, 7, 8, ...]. No callbacks were lost.
This would only happen when the app has heavy load, such as when a graph update took longer than several milliseconds.
After applying the workaround from this PR, the issue no longer happens.