[Development] QTimer question

Jaroslaw Kobus Jaroslaw.Kobus at qt.io
Wed May 31 00:52:48 CEST 2023


> On Tuesday, 30 May 2023 12:25:10 PDT Jaroslaw Kobus wrote:
> > Hi Thiago,
> >
> > thank you for your answer. There are 3 main points:
> >
> > 1. Coarsing is OK and I'm totally fine with having my timers called a bit
> > earlier or later (within 5% accuracy).
> >
> > However, this doesn't explain why I can't rely on the proper order in the
> > given example.
>
> You can today, with the current implementation, but it's not a promise we've
> made in the documentation. Today, it always rounds down to the target
> boundary, never up, and the target boundary only depends on the requested
> timeout. Therefore, two timers with the same timeout will trigger always in
> the order they were started.
>

Yeah, not promising in docs is fine. However, (form the 3rd point)
documenting explicitly, than you can't rely on an intuitive behavior, may help some users.

> > Recently I was rewriting the autotests for the TaskTree
> > (https://doc-snapshots.qt.io/qtcreator-extending/tasking-tasktree.html),
> > and wanted to use the simplest possible asynchronous task, instead of a
> > concurrent call. I've concluded it must be a singleShot(). Inside the tests
> > I need to rely on the proper order of timers' timeouts. It would be cool to
> > have it in Qt. Anyway, it looks like currently I need to implement the
> > above solution on my own (at least until it's done in Qt, if decided so).
>
> It's only a matter of documentation, not implementation: do we want to
> establish on stone that the order is and will forever be maintained, per
> timeout value?

I don't know whether setting it in stone is the best possible choice,
since the extra implementation may insignificantly slow down the runtime.
Could it be maybe optional, configurable? I don't know...

> This doesn't apply if you start a timer with 1000 ms and then the next 20
> milliseconds later with 980ms.

It does. It really doesn't matter, in fact.

If you start the first one, say at 12:00:00 [000] ms, and the second one at
12:00:00[020], the crucial thing is what target timestamp both calls register
inside a call to singleShot(). To make it really explicit, the singleShot()
could return the registered expectedTimeoutTimestamp value. In this way
the user may rely on the returned value and may expect the handlers
are executed exactly in the returned timestamp order.

> > 2. Regarding:
> > > It will depend on the timeout too. For single-shot timers, timers below 2
> > > seconds are Precise, which means only those above that threshold will
> > > suffer reordering.
> >
> > It doesn't look so, unfortunately. I've written the following test:
> >
> >         QTimer::singleShot(1, this, [&] { log.append(1); });
> >         QTimer::singleShot(1, this, [&] { log.append(2); });
> >
> > Both timeouts are 1ms, and I'm observing the opposite order on CI machine:
> > https://codereview.qt-project.org/c/qt-creator/qt-creator/+/480663/1
>
> That shouldn't be happening:
> void QTimerInfoList::timerInsert(QTimerInfo *ti)
> {
>     int index = size();
>     while (index--) {
>         const QTimerInfo * const t = at(index);
>         if (!(ti->timeout < t->timeout))
>             break;
>     }
>     insert(index+1, ti);
> }
>
> This should cause the timer to be inserted after the last timer for which its
> timeout wasn't smaller. QTimerInfoList::activateTimers() respects that order,
> so the second should trigger after the first.

I don't know QTimer's internals, so I can't comment on this. However, please
refer to what the CI machine reports for:
https://codereview.qt-project.org/c/qt-creator/qt-creator/+/480663/1

> > Moreover, if I change it to:
> >
> >         QTimer::singleShot(1, this, [&] { log.append(1); });
> >         QTimer::singleShot(2, this, [&] { log.append(2); });
> >
> > I still observe the opposite order, even when the second shot, executed
> > later, with 100% longer interval, may still come before the first:
> > https://codereview.qt-project.org/c/qt-creator/qt-creator/+/480663/2
> > (the same change, different patchset). So, in this case it looks like the 5%
> > tolerance is beaten considerably up to 100%.
>
> That's not about the 5% tolerance, because 5% of less than 20 rounds down to
> 0. Any timer of less than 20 ms is Precise, and therefore wakes up exactly
> when you asked it to. However, the sleep may have been for more than 1 ms so
> the second timer was expired by the time we woke up.

The basic question is:

If we define a tolerance of 5% for a Course timer, could we also define what 100% is?

I don't know what "sleep" you are referring to. Please refer to the tests I've provided links for.

> Any timer of less than 20 ms is Precise, and therefore wakes up exactly
> when you asked it to.

I really don't how the Precise mode works internally. Should I?

Also, the 20ms info doesn't come from docs, does it?

When the target thread is busy, I can't expect the exact accuracy for the elapsed timeouts,
and therefore I don't know how to interpret the "wakes up exactly when you asked it to"?

> Either way, I can't match what you're seeing from the code in QTimer.

Maybe the provided tesst on different CI machines may help?
It looks like the CI machine for Qt likes the
https://codereview.qt-project.org/c/qt/qtbase/+/480703,
while (not sure, maybe a different CI machine) doesn't like the
https://codereview.qt-project.org/c/qt-creator/qt-creator/+/480663

> > 3. Despite of how 1. and 2. will be proceeded, I think we should clearly
> > document it. It's really not obvious that Qt users can't rely on the proper
> > order of timeouts.
>
> From what you've shown, it may be that you can rely on the *reverse* order.

I can't, since I'm receiving the opposite results on my local machine.
My local machine accepts the intuitive order of timeouts (quite fast one),
while the CI doesn't.

Best regards

Jarek

________________________________________
From: Development <development-bounces at qt-project.org> on behalf of Jaroslaw Kobus via Development <development at qt-project.org>
Sent: Tuesday, May 30, 2023 11:03 PM
To: Development at qt-project.org
Subject: Re: [Development] QTimer question

[Re-sending my reply to Thiago to the mailing list, as I think it may be possibly interesting for more people.]
[In meantime I've created https://codereview.qt-project.org/c/qt/qtbase/+/480703 and waiting for CI results.]

> On Monday, 29 May 2023 11:40:14 PDT Jaroslaw Kobus via Development wrote:
> > Hi All,
> >
> > when I start 2 single shot timers synchronously in a row, with exactly the
> > same interval, from the same thread, can I rely on having their handlers
> > called in the same order in which they were started?
> >
> > I.e.:
> >
> > QTimer::singleShot(1000, [] { qDebug() << "1st timer elapsed"; });
> > QTimer::singleShot(1000, [] { qDebug() << "2nd timer elapsed"; });
> >
> > Should I always see the:
> >
> > 1st timer elapsed
> > 2nd timer elapsed
> >
> > on the output, or should I expect the random order?
>
> Strictly speaking, no, you can't. Because timers aren't precise, timers can be
> delivered early or late by 5% if they are Coarse. Therefore, the order will
> depend on which direction the rounding went, which depends on how long it took
> you between scheduling those two timers.
>
> It will depend on the timeout too. For single-shot timers, timers below 2
> seconds are Precise, which means only those above that threshold will suffer
> reordering.
>
> For non-QTimer::singleShot, all timers are Coarse by default.

Hi Thiago,

thank you for your answer. There are 3 main points:

1. Coarsing is OK and I'm totally fine with having my timers called a bit
earlier or later (within 5% accuracy).

However, this doesn't explain why I can't rely on the proper order in the given example.
I can imagine that the expected order is preserved even when having coarse timers.
The simple implementation could look like:

Keeping a global hash (per thread?) of started timers together
with their expected timeout timestamp, and the opposite map:

QHash<timerId, expectedTimeoutTimestamp> timerIdToDeadline;
QMap<expectedTimeoutTimestamp, timerId> deadlineToTimerId;

Whenever calling the singleShot(), the implementation should add and item to the map and hash.
The expectedTimeoutTimestamp would be set:
expectedTimeoutTimestamp = currentTime + timerInterval;

Whenever any timer (timer X) timeouts, and before it emits a timeout signal publicly,
it could lookup the hash for its expectedTimeoutTimestamp.
Before emitting the timer X's timeout, it should emit timeouts for timers that have their
expectedTimeoutTimestamp lower than the timer X's expectedTimeoutTimestamp
(i.e. those, that are before the timer X's expectedTimeoutTimestamp inside the deadlineToTimerId map).
In this case, all the extra timers that emitted their timeout should be removed
from map and hash, and later, when theirs internal activation signal comes - the public emission
should be omitted.

When adding a mutex to the map and hash, it could probably also work for all threads.

Recently I was rewriting the autotests for the TaskTree
(https://doc-snapshots.qt.io/qtcreator-extending/tasking-tasktree.html),
and wanted to use the simplest possible asynchronous task, instead of a concurrent call.
I've concluded it must be a singleShot(). Inside the tests I need to rely on the proper order
of timers' timeouts. It would be cool to have it in Qt. Anyway, it looks like currently I need
to implement the above solution on my own (at least until it's done in Qt, if decided so).

I may create a task request ticket for it, if you want me to.

2. Regarding:

> It will depend on the timeout too. For single-shot timers, timers below 2
> seconds are Precise, which means only those above that threshold will suffer
> reordering.

It doesn't look so, unfortunately. I've written the following test:

        QTimer::singleShot(1, this, [&] { log.append(1); });
        QTimer::singleShot(1, this, [&] { log.append(2); });

Both timeouts are 1ms, and I'm observing the opposite order on CI machine:
https://codereview.qt-project.org/c/qt-creator/qt-creator/+/480663/1

Moreover, if I change it to:

        QTimer::singleShot(1, this, [&] { log.append(1); });
        QTimer::singleShot(2, this, [&] { log.append(2); });

I still observe the opposite order, even when the second shot, executed later,
with 100% longer interval, may still come before the first:
https://codereview.qt-project.org/c/qt-creator/qt-creator/+/480663/2
(the same change, different patchset). So, in this case it looks like the 5%
tolerance is beaten considerably up to 100%.

I may create a bugreport for it, if you want me to.

3. Despite of how 1. and 2. will be proceeded, I think we should clearly document it.
It's really not obvious that Qt users can't rely on the proper order of timeouts.

I may create a bugreport for doc team, if you want me to.

Best regards

Jarek
--
Development mailing list
Development at qt-project.org
https://lists.qt-project.org/listinfo/development


More information about the Development mailing list