QEventDispatcherWin32: properly support 64-bit timeouts

The Windows timer APIs take intervals as uint milliseconds.
As a result, the code was unconditionally truncating an qint64
millisecond value to uint, leading to incorrect timeouts.

Detect such cases, and implement a logic to adjust the interval passed
to the Windows APIs and send a timer event at every second timeout.
This will give a 1ms or 2ms error in the timeout calculation, but that
should be an acceptable tolerance when we talk about intervals around
50 days or longer.

Adjust the tst_QChronoTimer::remainingTimeInitial() test to check that
the remaining time is not less than "expected interval - 1 minute".
This should allow to catch cases when internal calculations are
terribly broken.

It's not really possible to provide even a manual test for timeouts
that are larger than std::numeric_limits<uint>::max(), so just rely on
the existing tests to make sure that the updated implementation does
not break regular cases with smaller timeouts.

Amends 0d0b346322f6b078e6fe60cd3612e8d08a0d5f00.

Task-number: QTBUG-129170
Change-Id: Ib9885dcb7aca18e9fb193fad14bace0efeb437da
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
This commit is contained in:
Ivan Solovev 2025-01-14 16:58:44 +01:00
parent 34f1b6b6d1
commit bb716c0111
3 changed files with 29 additions and 2 deletions

View File

@ -15,6 +15,8 @@
#include "qcoreapplication_p.h" #include "qcoreapplication_p.h"
#include <private/qthread_p.h> #include <private/qthread_p.h>
#include "q26numeric.h"
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
#ifndef TIME_KILL_SYNCHRONOUS #ifndef TIME_KILL_SYNCHRONOUS
@ -298,7 +300,7 @@ static HWND qt_create_internal_window(const QEventDispatcherWin32 *eventDispatch
static ULONG calculateNextTimeout(WinTimerInfo *t, quint64 currentTime) static ULONG calculateNextTimeout(WinTimerInfo *t, quint64 currentTime)
{ {
uint interval = t->interval; qint64 interval = t->interval;
ULONG tolerance = TIMERV_DEFAULT_COALESCING; ULONG tolerance = TIMERV_DEFAULT_COALESCING;
switch (t->timerType) { switch (t->timerType) {
case Qt::PreciseTimer: case Qt::PreciseTimer:
@ -347,7 +349,13 @@ void QEventDispatcherWin32Private::registerTimer(WinTimerInfo *t)
bool ok = false; bool ok = false;
ULONG tolerance = calculateNextTimeout(t, qt_msectime()); ULONG tolerance = calculateNextTimeout(t, qt_msectime());
uint interval = t->interval; uint interval = q26::saturate_cast<uint>(t->interval);
if (interval != t->interval) {
// Now we'll need to post the timer event at each second timeout.
interval = q26::saturate_cast<uint>(t->interval / 2 + 1);
t->usesExtendedInterval = true;
t->isSecondTimeout = false;
}
if (interval == 0u) { if (interval == 0u) {
// optimization for single-shot-zero-timer // optimization for single-shot-zero-timer
QCoreApplication::postEvent(q, new QZeroTimerEvent(t->timerId)); QCoreApplication::postEvent(q, new QZeroTimerEvent(t->timerId));
@ -390,6 +398,12 @@ void QEventDispatcherWin32Private::sendTimerEvent(int timerId)
{ {
WinTimerInfo *t = timerDict.value(timerId); WinTimerInfo *t = timerDict.value(timerId);
if (t && !t->inTimerEvent) { if (t && !t->inTimerEvent) {
if (t->usesExtendedInterval && !t->isSecondTimeout) {
// That's the case when the interval is larger than uint
t->isSecondTimeout = true;
return;
}
// send event, but don't allow it to recurse // send event, but don't allow it to recurse
t->inTimerEvent = true; t->inTimerEvent = true;
@ -403,6 +417,7 @@ void QEventDispatcherWin32Private::sendTimerEvent(int timerId)
if (t->timerId == -1) { if (t->timerId == -1) {
delete t; delete t;
} else { } else {
t->isSecondTimeout = false;
t->inTimerEvent = false; t->inTimerEvent = false;
} }
} }
@ -702,6 +717,8 @@ void QEventDispatcherWin32::registerTimer(Qt::TimerId timerId, Duration interval
t->obj = object; t->obj = object;
t->inTimerEvent = false; t->inTimerEvent = false;
t->fastTimerId = 0; t->fastTimerId = 0;
t->usesExtendedInterval = false;
t->isSecondTimeout = false;
d->registerTimer(t); d->registerTimer(t);

View File

@ -93,6 +93,8 @@ struct WinTimerInfo { // internal timer info
Qt::TimerType timerType; Qt::TimerType timerType;
UINT fastTimerId; UINT fastTimerId;
bool inTimerEvent; bool inTimerEvent;
bool usesExtendedInterval;
bool isSecondTimeout;
}; };
class QZeroTimerEvent : public QTimerEvent class QZeroTimerEvent : public QTimerEvent

View File

@ -284,6 +284,14 @@ void tst_QChronoTimer::remainingTimeInitial()
} else { } else {
QCOMPARE_LE(rt, startTimeNs); QCOMPARE_LE(rt, startTimeNs);
} }
if (startTimeNs > 1min) {
// The test will surely take less than 1 minute
auto startMinusOneMinute = std::chrono::nanoseconds{startTimeNs - 1min};
// If the remaining time is less than startMinusOneMinute, then
// something is terribly wrong with the internal interval calculations
QCOMPARE_GE(rt, startMinusOneMinute);
}
} }
void tst_QChronoTimer::remainingTimeDuringActivation_data() void tst_QChronoTimer::remainingTimeDuringActivation_data()