QTimerInfo: use chrono for time intervals

For VeryCoarseTimer:
   - The code used to convert QTimerInfo::interval to seconds in
     registerTimer(), then convert to milliseconds when creating
     QTimerInfo in registeredTimers(); this is a bit confusing as
     "interval" was sometimes milliseconds and sometimes seconds
     depending on the type of the timer; instead "round" to the nearest
     second while always keeping "interval" in milliseconds (relying on
     chrono doing the conversion)..
   - Add roundToSecs() helper; it behaves like the original code (i.e.
     rounding each 500ms to 1 full second):
   const auto list = {300, 499, 500, 600, 1000, 1300, 1499, 1500, 1501,
                      1600, 2000, 2300, 2499, 2500, 2600};
   using namespace std::chrono;
   for (int dur : list) {
       auto i = dur;
       i /= 500; i += 1; i >>= 1; // Original code

       milliseconds msec{dur};
       seconds secs = duration_cast<seconds>(msec);
       milliseconds frac = msec - secs;
       if (frac >= 500ms)
          secs += 1s;

       assert(i == secs.count());
   }
----

- Don't mix signed and unsigned when doing arithmetic

The next "chrono-first" step would be changing
QAbstractEventDispatcher::TimerInfo::interval from int to
chrono::milliseconds, and adding a virtual
QAbstractEventDispatcher::registerTimer() overload that takes
chrono::milliseconds; neither can be done until Qt7 due to binary
compatibility constraints, c.f.:
https://community.kde.org/Policies/Binary_Compatibility_Issues_With_C++

Task-number: QTBUG-110059
Change-Id: I36f9bd8fb29565b1131afb3cdfc313452f625598
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
This commit is contained in:
Ahmad Samir 2023-02-23 13:33:11 +02:00
parent 39a4cd126f
commit 6754795e54
4 changed files with 100 additions and 76 deletions

View File

@ -489,7 +489,8 @@ void QEventDispatcherGlib::registerTimer(int timerId, qint64 interval, Qt::Timer
#endif
Q_D(QEventDispatcherGlib);
d->timerSource->timerList.registerTimer(timerId, interval, timerType, object);
d->timerSource->timerList.registerTimer(timerId, std::chrono::milliseconds{ interval },
timerType, object);
}
bool QEventDispatcherGlib::unregisterTimer(int timerId)

View File

@ -299,7 +299,7 @@ void QEventDispatcherUNIX::registerTimer(int timerId, qint64 interval, Qt::Timer
#endif
Q_D(QEventDispatcherUNIX);
d->timerList.registerTimer(timerId, interval, timerType, obj);
d->timerList.registerTimer(timerId, std::chrono::milliseconds{ interval }, timerType, obj);
}
/*!

View File

@ -9,7 +9,6 @@
#include "private/qtimerinfo_unix_p.h"
#include "private/qobject_p.h"
#include "private/qabstracteventdispatcher_p.h"
#include <QtCore/private/qtools_p.h>
#ifdef QTIMERINFO_DEBUG
# include <QDebug>
@ -18,6 +17,8 @@
#include <sys/times.h>
using namespace std::chrono;
QT_BEGIN_NAMESPACE
Q_CORE_EXPORT bool qt_disable_lowpriority_timers=false;
@ -153,6 +154,25 @@ static_assert(roundToMillisecond({0, 1'000'000}) == timespec{0, 1'000'000});
static_assert(roundToMillisecond({0, 999'999'999}) == timespec{1, 0});
static_assert(roundToMillisecond({1, 0}) == timespec{1, 0});
static constexpr seconds roundToSecs(milliseconds msecs)
{
// The very coarse timer is based on full second precision, so we want to
// round the interval to the closest second, rounding 500ms up to 1s.
//
// std::chrono::round() wouldn't work with all multiples of 500 because for the
// middle point it would round to even:
// value round() wanted
// 500 0 1
// 1500 2 2
// 2500 2 3
auto secs = duration_cast<seconds>(msecs);
const milliseconds frac = msecs - secs;
if (frac >= 500ms)
++secs;
return secs;
}
#ifdef QTIMERINFO_DEBUG
QDebug operator<<(QDebug s, timeval tv)
{
@ -187,97 +207,96 @@ static void calculateCoarseTimerTimeout(QTimerInfo *t, timespec currentTime)
//
// The objective is to make most timers wake up at the same time, thereby reducing CPU wakeups.
uint interval = uint(t->interval);
Q_ASSERT(interval >= 20);
Q_ASSERT(t->interval >= 20ms);
// Calculate how much we can round and still keep within 5% error
uint absMaxRounding = interval / 20;
const milliseconds absMaxRounding = t->interval / 20;
using namespace std::chrono;
uint msec = duration_cast<milliseconds>(nanoseconds{t->timeout.tv_nsec}).count();
auto fracMsec = duration_cast<milliseconds>(nanoseconds{t->timeout.tv_nsec});
if (interval < 100 && interval != 25 && interval != 50 && interval != 75) {
if (t->interval < 100ms && t->interval != 25ms && t->interval != 50ms && t->interval != 75ms) {
auto fracCount = fracMsec.count();
// special mode for timers of less than 100 ms
if (interval < 50) {
if (t->interval < 50ms) {
// round to even
// round towards multiples of 50 ms
bool roundUp = (msec % 50) >= 25;
msec >>= 1;
msec |= uint(roundUp);
msec <<= 1;
bool roundUp = (fracCount % 50) >= 25;
fracCount >>= 1;
fracCount |= roundUp;
fracCount <<= 1;
} else {
// round to multiple of 4
// round towards multiples of 100 ms
bool roundUp = (msec % 100) >= 50;
msec >>= 2;
msec |= uint(roundUp);
msec <<= 2;
bool roundUp = (fracCount % 100) >= 50;
fracCount >>= 2;
fracCount |= roundUp;
fracCount <<= 2;
}
fracMsec = milliseconds{fracCount};
} else {
uint min = qMax<int>(0, msec - absMaxRounding);
uint max = qMin(1000u, msec + absMaxRounding);
milliseconds min = std::max(0ms, fracMsec - absMaxRounding);
milliseconds max = std::min(1000ms, fracMsec + absMaxRounding);
// find the boundary that we want, according to the rules above
// extra rules:
// 1) whatever the interval, we'll take any round-to-the-second timeout
if (min == 0) {
msec = 0;
if (min == 0ms) {
fracMsec = 0ms;
goto recalculate;
} else if (max == 1000) {
msec = 1000;
} else if (max == 1000ms) {
fracMsec = 1000ms;
goto recalculate;
}
uint wantedBoundaryMultiple;
milliseconds wantedBoundaryMultiple{25};
// 2) if the interval is a multiple of 500 ms and > 5000 ms, we'll always round
// towards a round-to-the-second
// 3) if the interval is a multiple of 500 ms, we'll round towards the nearest
// multiple of 500 ms
if ((interval % 500) == 0) {
if (interval >= 5000) {
msec = msec >= 500 ? max : min;
if ((t->interval % 500) == 0ms) {
if (t->interval >= 5s) {
fracMsec = fracMsec >= 500ms ? max : min;
goto recalculate;
} else {
wantedBoundaryMultiple = 500;
wantedBoundaryMultiple = 500ms;
}
} else if ((interval % 50) == 0) {
} else if ((t->interval % 50) == 0ms) {
// 4) same for multiples of 250, 200, 100, 50
uint mult50 = interval / 50;
if ((mult50 % 4) == 0) {
milliseconds mult50 = t->interval / 50;
if ((mult50 % 4) == 0ms) {
// multiple of 200
wantedBoundaryMultiple = 200;
} else if ((mult50 % 2) == 0) {
wantedBoundaryMultiple = 200ms;
} else if ((mult50 % 2) == 0ms) {
// multiple of 100
wantedBoundaryMultiple = 100;
} else if ((mult50 % 5) == 0) {
wantedBoundaryMultiple = 100ms;
} else if ((mult50 % 5) == 0ms) {
// multiple of 250
wantedBoundaryMultiple = 250;
wantedBoundaryMultiple = 250ms;
} else {
// multiple of 50
wantedBoundaryMultiple = 50;
wantedBoundaryMultiple = 50ms;
}
} else {
wantedBoundaryMultiple = 25;
}
uint base = msec / wantedBoundaryMultiple * wantedBoundaryMultiple;
uint middlepoint = base + wantedBoundaryMultiple / 2;
if (msec < middlepoint)
msec = qMax(base, min);
milliseconds base = (fracMsec / wantedBoundaryMultiple) * wantedBoundaryMultiple;
milliseconds middlepoint = base + wantedBoundaryMultiple / 2;
if (fracMsec < middlepoint)
fracMsec = qMax(base, min);
else
msec = qMin(base + wantedBoundaryMultiple, max);
fracMsec = qMin(base + wantedBoundaryMultiple, max);
}
recalculate:
if (msec == 1000u) {
if (fracMsec == 1000ms) {
++t->timeout.tv_sec;
t->timeout.tv_nsec = 0;
} else {
t->timeout.tv_nsec = nanoseconds{milliseconds{msec}}.count();
t->timeout.tv_nsec = nanoseconds{fracMsec}.count();
}
if (t->timeout < currentTime)
t->timeout += interval;
t->timeout += t->interval;
}
static void calculateNextTimeout(QTimerInfo *t, timespec currentTime)
@ -302,10 +321,11 @@ static void calculateNextTimeout(QTimerInfo *t, timespec currentTime)
return;
case Qt::VeryCoarseTimer:
// we don't need to take care of the microsecond component of t->interval
t->timeout.tv_sec += t->interval;
// t->interval already rounded to full seconds in registerTimer()
const auto secs = duration_cast<seconds>(t->interval).count();
t->timeout.tv_sec += secs;
if (t->timeout.tv_sec <= currentTime.tv_sec)
t->timeout.tv_sec = currentTime.tv_sec + t->interval;
t->timeout.tv_sec = currentTime.tv_sec + secs;
#ifdef QTIMERINFO_DEBUG
t->expected.tv_sec += t->interval;
if (t->expected.tv_sec <= currentTime.tv_sec)
@ -361,6 +381,11 @@ bool QTimerInfoList::timerWait(timespec &tm)
If the timer is overdue, the returned value will be 0.
*/
qint64 QTimerInfoList::timerRemainingTime(int timerId)
{
return remainingDuration(timerId).count();
}
milliseconds QTimerInfoList::remainingDuration(int timerId)
{
timespec currentTime = updateCurrentTime();
repairTimersIfNeeded();
@ -371,10 +396,9 @@ qint64 QTimerInfoList::timerRemainingTime(int timerId)
if (currentTime < t->timeout) {
// time to wait
tm = roundToMillisecond(t->timeout - currentTime);
const std::chrono::milliseconds dur = QtMiscUtils::timespecToChronoMs(&tm);
return dur.count();
return QtMiscUtils::timespecToChronoMs(&tm);
} else {
return 0;
return milliseconds{0};
}
}
}
@ -383,10 +407,16 @@ qint64 QTimerInfoList::timerRemainingTime(int timerId)
qWarning("QTimerInfoList::timerRemainingTime: timer id %i not found", timerId);
#endif
return -1;
return milliseconds{-1};
}
void QTimerInfoList::registerTimer(int timerId, qint64 interval, Qt::TimerType timerType, QObject *object)
{
registerTimer(timerId, milliseconds{interval}, timerType, object);
}
void QTimerInfoList::registerTimer(int timerId, milliseconds interval,
Qt::TimerType timerType, QObject *object)
{
QTimerInfo *t = new QTimerInfo;
t->id = timerId;
@ -409,30 +439,26 @@ void QTimerInfoList::registerTimer(int timerId, qint64 interval, Qt::TimerType t
// so our boundaries are 20 ms and 20 s
// below 20 ms, 5% inaccuracy is below 1 ms, so we convert to high precision
// above 20 s, 5% inaccuracy is above 1 s, so we convert to VeryCoarseTimer
if (interval >= 20000) {
if (interval >= 20s) {
t->timerType = Qt::VeryCoarseTimer;
} else {
t->timeout = expected;
if (interval <= 20) {
if (interval <= 20ms) {
t->timerType = Qt::PreciseTimer;
// no adjustment is necessary
} else if (interval <= 20000) {
} else if (interval <= 20s) {
calculateCoarseTimerTimeout(t, currentTime);
}
break;
}
Q_FALLTHROUGH();
case Qt::VeryCoarseTimer:
// the very coarse timer is based on full second precision,
// so we keep the interval in seconds (round to closest second)
t->interval /= 500;
t->interval += 1;
t->interval >>= 1;
t->timeout.tv_sec = currentTime.tv_sec + t->interval;
const seconds secs = roundToSecs(t->interval);
t->interval = secs;
t->timeout.tv_sec = currentTime.tv_sec + secs.count();
t->timeout.tv_nsec = 0;
// if we're past the half-second mark, increase the timeout again
using namespace std::chrono;
if (currentTime.tv_nsec > nanoseconds{500ms}.count())
++t->timeout.tv_sec;
}
@ -493,15 +519,9 @@ bool QTimerInfoList::unregisterTimers(QObject *object)
QList<QAbstractEventDispatcher::TimerInfo> QTimerInfoList::registeredTimers(QObject *object) const
{
QList<QAbstractEventDispatcher::TimerInfo> list;
for (int i = 0; i < size(); ++i) {
const QTimerInfo * const t = at(i);
if (t->obj == object) {
list << QAbstractEventDispatcher::TimerInfo(t->id,
(t->timerType == Qt::VeryCoarseTimer
? t->interval * 1000
: t->interval),
t->timerType);
}
for (const QTimerInfo *const t : std::as_const(*this)) {
if (t->obj == object)
list.emplaceBack(t->id, t->interval.count(), t->timerType);
}
return list;
}
@ -576,7 +596,7 @@ int QTimerInfoList::activateTimers()
// reinsert timer
timerInsert(currentTimerInfo);
if (currentTimerInfo->interval > 0)
if (currentTimerInfo->interval > 0ms)
n_act++;
// Send event, but don't allow it to recurse:

View File

@ -31,7 +31,7 @@ QT_BEGIN_NAMESPACE
struct QTimerInfo {
int id; // - timer identifier
Qt::TimerType timerType; // - timer type
qint64 interval; // - timer interval in milliseconds
std::chrono::milliseconds interval; // - timer interval
timespec timeout; // - when to actually fire
QObject *obj; // - object to receive event
QTimerInfo **activateRef; // - ref from activateTimers
@ -71,8 +71,11 @@ public:
void timerInsert(QTimerInfo *);
qint64 timerRemainingTime(int timerId);
std::chrono::milliseconds remainingDuration(int timerId);
void registerTimer(int timerId, qint64 interval, Qt::TimerType timerType, QObject *object);
void registerTimer(int timerId, std::chrono::milliseconds interval, Qt::TimerType timerType,
QObject *object);
bool unregisterTimer(int timerId);
bool unregisterTimers(QObject *object);
QList<QAbstractEventDispatcher::TimerInfo> registeredTimers(QObject *object) const;