Skip to content

Commit ed1545e

Browse files
committed
fix: ensure execution order of callbacks with zero-delay by handling them independently of delayed callbacks
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.
1 parent a6eb8e5 commit ed1545e

1 file changed

Lines changed: 61 additions & 0 deletions

File tree

qasync/__init__.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import time
2929
from concurrent.futures import Future
3030
from queue import Queue
31+
from collections import deque
3132

3233
logger = logging.getLogger(__name__)
3334

@@ -294,6 +295,58 @@ def __log_debug(self, *args, **kwargs):
294295
self._logger.debug(*args, **kwargs)
295296

296297

298+
@with_logger
299+
class _CallSoonQueue(QtCore.QObject):
300+
def __init__(self):
301+
super().__init__()
302+
# Contains asyncio.Handle objects
303+
# Use a deque instead of Queue, as we don't require
304+
# synchronization between threads here.
305+
self.__callbacks = deque()
306+
# Set a 0-delay timer on itself, this will ensure that
307+
# timerEvent gets fired each time after window events are processed
308+
# See https://doc.qt.io/qt-6/qtimer.html#interval-prop
309+
self.__timerId = self.startTimer(0)
310+
self.__stopped = False
311+
self.__debug_enabled = False
312+
313+
def add_callback(self, handle):
314+
# handle must be an asyncio.Handle
315+
self.__callbacks.append(handle)
316+
self.__log_debug("Registering call_soon handle %s", id(handle))
317+
return handle
318+
319+
def timerEvent(self, event):
320+
timerId = event.timerId()
321+
assert timerId == self.__timerId
322+
323+
# Stop timer if stopped
324+
if self.__stopped:
325+
self.killTimer(timerId)
326+
self.__log_debug("call_soon queue stopped, clearing handles")
327+
# TODO: Do we need to del the handles or somehow invalidate them?
328+
self.__callbacks.clear()
329+
return
330+
331+
# Iterate over pending callbacks
332+
# TODO: Runtime deadline, don't process the entire queue if it takes too long?
333+
while len(self.__callbacks) > 0:
334+
handle = self.__callbacks.popleft()
335+
self.__log_debug("Calling call_soon handle %s", id(handle))
336+
handle._run()
337+
338+
def stop(self):
339+
self.__log_debug("Stopping call_soon queue")
340+
self.__stopped = True
341+
342+
def set_debug(self, enabled):
343+
self.__debug_enabled = enabled
344+
345+
def __log_debug(self, *args, **kwargs):
346+
if self.__debug_enabled:
347+
self._logger.debug(*args, **kwargs)
348+
349+
297350
def _fileno(fd):
298351
if isinstance(fd, int):
299352
return fd
@@ -339,6 +392,7 @@ def __init__(self, app=None, set_running_loop=False, already_running=False):
339392
self._read_notifiers = {}
340393
self._write_notifiers = {}
341394
self._timer = _SimpleTimer()
395+
self._call_soon_queue = _CallSoonQueue()
342396

343397
self.__call_soon_signaller = signaller = _make_signaller(QtCore, object, tuple)
344398
self.__call_soon_signal = signaller.signal
@@ -441,6 +495,7 @@ def close(self):
441495
super().close()
442496

443497
self._timer.stop()
498+
self._call_soon_queue.stop()
444499
self.__app = None
445500

446501
for notifier in itertools.chain(
@@ -474,6 +529,11 @@ def call_later(self, delay, callback, *args, context=None):
474529
return self._add_callback(asyncio.Handle(callback, args, self), delay)
475530

476531
def _add_callback(self, handle, delay=0):
532+
if delay == 0:
533+
# To ensure that we can guarantee the execution order of
534+
# 0-delay callbacks, add them to a special queue, rather than
535+
# assume that Qt will fire the timerEvents in order
536+
return self._call_soon_queue.add_callback(handle)
477537
return self._timer.add_callback(handle, delay)
478538

479539
def call_soon(self, callback, *args, context=None):
@@ -717,6 +777,7 @@ def set_debug(self, enabled):
717777
super().set_debug(enabled)
718778
self.__debug_enabled = enabled
719779
self._timer.set_debug(enabled)
780+
self._call_soon_queue.set_debug(enabled)
720781

721782
def __enter__(self):
722783
return self

0 commit comments

Comments
 (0)