Nyaruko
Nyaruko

Reputation: 4509

QTimer not accurate at all?

Running on Windows7 64bit machine with a very powerful CPU (8 core 16 threads). I used a QTimer to trigger a function call at 50Hz. But I ended up with 30Hz. The function call itself definitely takes less than 10ms to finish. The whole process happens in a separate thread.

What could go wrong in my case? The Qt's doc said it will be accurate within 5%?

Upvotes: 8

Views: 5685

Answers (3)

djvg
djvg

Reputation: 14355

Just to illustrate the issue, I did some quick-and-dirty timing in PyQt5 on a Windows 7 64-bit system, using QTimer both with and without the high-precision setting.

Here's what the result looks like using default precision (as in PyQt4):

requested timer interval vs measured timer interval for default precision

Dots represent the median of 100 samples, vertical lines indicate the range (min to max). Ideally, all dots would be on the dashed line.

Note the obvious discrepancies between approx. 20ms and 100ms. For example, setting QTimer to 20ms (50Hz) in my case results in an actual (measured) interval of approx. 31ms (32Hz). Coarse indeed.

And this is what it looks like when using the PreciseTimer:

requested timer interval vs measured timer interval for high precision

Much better, I would say, especially if you are interested in intervals between 20ms and 100 ms.

For those interested, here's the code (undoubtedly it can be much improved):

import sys
import time
import numpy
from matplotlib import pyplot
try:
    from PyQt5 import QtCore, QtWidgets
    print 'using PyQt5'
except:
    from PyQt4 import QtCore, QtGui
    print 'using PyQt4'


class StampTimer(QtCore.QTimer):
    signal_quit = QtCore.pyqtSignal()

    def __init__(self, number_of_samples, precise=False, *args, **kwargs):
        super(StampTimer, self).__init__(*args, **kwargs)
        if precise:
            try:
                self.setTimerType(QtCore.Qt.PreciseTimer)
                print 'using precise timer (PyQt5)'
            except:
                print 'precise timer not available (PyQt4)'
        self.number_of_samples = number_of_samples
        self.stamps_ms = []
        self.timeout.connect(self.stamp)

    def start(self):
        self.stamps_ms = []
        super(StampTimer, self).start()

    def stamp(self):
        # Save a timestamp (use clock() instead of time())
        self.stamps_ms.append(1e3 * time.clock())
        # Quit when we reach the specified number of samples
        if len(self.stamps_ms) == self.number_of_samples:
            self.stop()
            self.signal_quit.emit()


try:
    app = QtWidgets.QApplication(sys.argv)
except:
    app = QtGui.QApplication(sys.argv)

# Parameters
number_of_intervals = 100
intervals_requested_ms = range(1, 10)
intervals_requested_ms.extend(range(10, 60, 2))
intervals_requested_ms.extend(range(60, 200, 10))
intervals_requested_ms.append(1000)

# Variables
intervals_measured_ms = numpy.zeros(
    (number_of_intervals, len(intervals_requested_ms)), float)

# Run
timer = StampTimer(number_of_samples=number_of_intervals+1, precise=False)
timer.signal_quit.connect(app.quit)
for index, interval_requested_ms in enumerate(intervals_requested_ms):
    # For each interval in the list, we run the timer until we obtain 
    # the specified number of samples
    timer.setInterval(interval_requested_ms)
    timer.start()
    app.exec_()
    # Calculate statistics
    intervals_measured_ms[:, index] = numpy.diff(timer.stamps_ms)
    median_ms = numpy.median(intervals_measured_ms[:, index])
    max_ms = numpy.max(intervals_measured_ms[:, index])
    min_ms = numpy.min(intervals_measured_ms[:, index])
    # Add to plot
    print 'requested: {} ms, ' \
          'measured: median(min/max) {:.1f} ({:.2f}/{:.2f}) ms'.format(
        interval_requested_ms, median_ms, min_ms, max_ms)
    pyplot.plot(interval_requested_ms, median_ms, 'ok')
    pyplot.plot([interval_requested_ms]*2, [min_ms, max_ms], '-k')

# Plot result
pyplot.plot([0, 1000], [0, 1000], 'k--')
pyplot.xscale('log', nonposx='clip')
pyplot.yscale('log', nonposy='clip')
pyplot.xlabel('interval requested [ms]')
pyplot.ylabel(
    'interval measured [ms] (median, n={})'.format(number_of_intervals))
pyplot.show()

Also check this answer for valuable insights.

Upvotes: 8

kefir500
kefir500

Reputation: 4414

You can achieve a better timer precision by setting timer type property to Qt::PreciseTimer (the default type is Qt::CoarseTimer).

From the docs:

Qt::PreciseTimer – Precise timers try to keep millisecond accuracy.
Qt::CoarseTimer – Coarse timers try to keep accuracy within 5% of the desired interval.

However, as pointed out by @Paul and @AlgirdasPreidžius, there is still no guarantee that the precision will be perfectly accurate.

Upvotes: 12

Paul
Paul

Reputation: 13238

Windows 7 is not RTOS, so there is no guarantee that the timer will fire exactly when you expect it to fire. This is not QTimer issue, but the OS.

From QTimer documentaion:

The accuracy of timers depends on the underlying operating system and hardware. Most platforms support a resolution of 1 millisecond, though the accuracy of the timer will not equal this resolution in many real-world situations. The accuracy also depends on the timer type. For Qt::PreciseTimer, QTimer will try to keep the accurance at 1 millisecond. Precise timers will also never time out earlier than expected.

Note, that the documentation states "will try to keep" and not "guarantees to keep".

Upvotes: 8

Related Questions