How many emitted signals is too many?
-
Well I made one change: although conceptually each processor flag update should cause a signal, so that the UI can show it, most instructions set multiple flags. Moved to one signal at end of instruction for all flags instead and that took tens of thousands of signals out. Improved some of the "merging" of multiple signals to make that quicker. All good, clean fun :)
-
Hi,
I think you might be looking for "signal compression".
This StackOverflow answer does a good job explaining it.
-
Hi,
I think you might be looking for "signal compression".
This StackOverflow answer does a good job explaining it.
@SGaist
On first reading at least:The event contains the sender object, the signal id, the slot index, and packaged call parameters.
The objective is to compress such calls so that only one unique call exists in the event queue for a given tuple of (sender object, sender signal, receiver object, receiver slot).
I don't see how the second quotation deals with the first's "and packaged call parameters"? My signals have parameters. And sometimes they matter, sometimes they don't: some of my signals a later one completely overwrites an earlier one, like setting the value of a register, while others do not, like you can't reduce multiple
dataChanged()signals to one. If the proposed code when "pruning" either ignores parameters or only matches on identical ones I don't see how that suffices. -
Here's a demonstration of the mailbox idea I alluded to, using an atomic pointer to post the latest state. At most, there are 3 live versions at a time: 1 in progress, 1 in the mailbox, and 1 being processed in the main thread.
Edit: I should have referred to this pattern as triple buffering
#include <QCoreApplication> #include <QThread> #include <QDebug> #include <QTimer> struct data { int count; }; struct Sender : public QThread { QAtomicPointer<data> & m_mailbox; Sender(QAtomicPointer<data> & mailbox, QObject *parent=nullptr) : QThread(parent), m_mailbox(mailbox) {} void run() override { data *next = nullptr; int counter = 0; while (!isInterruptionRequested()) { if (!next) { next = new data; } next->count = counter++; next = m_mailbox.fetchAndStoreOrdered(next); } } }; int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); QAtomicPointer<data> mailbox; Sender s(mailbox); s.start(); QTimer t; t.setInterval(1000); QObject::connect(&t, &QTimer::timeout, [&]() { data *next = mailbox.fetchAndStoreOrdered(nullptr); if (!next) return; qDebug() << "acquired new value" << next->count; delete next; }); t.start(); a.exec(); } -
This problem reminds me of progress dialogs: If you have a fast loop and update the progress bar/progress dialog in each iteration the GUI will hang. The same thing will happen in your case and for a good user experience you should absolutely do something about that. Here are a few ideas (some derived from earlier posts):
-
You mentioned the "signal queuer-reducer". Instead of putting it into the sender or receiver thread, you could have a third thread running. This third thread could be tasked with just recuding the number of actual signals send to the intended receiver. This would leave enough time for both the emulator thread and the GUI thread.
-
I'm not sure how the
compressEvent()is supposed to work. I do know thatupdate()first collects several calls. I'm not sure how to google for this (and I believe it has gotten harder to find this), but there is an established approach using two timers: The first timer is to delay firing the actual signal just in case the same signal comes in again. Everytime the same signal comes in, the timer is reset. This just adds a tiny delay to the update after the last signal was emitted. Maybe you could choose something like 5 or 10ms for this first timer. The problem, however, is that in your specific case the signal might never be emitted because the first timer is always reset. That's where the second timer comes in: It has a longer timeout and is triggered with the first signal. I can be something like 50ms. On timeout it emits the last signal. This makes sure that occasionally something happens. Here are some of the important functions for a class that I have:
void ConsolidatedSlotCall::timeout() { minTimer.stop(); maxTimer.stop(); if(function && parameterPack) { function->callWithParameters(parameterPack); emit slotTriggered(); } } void ConsolidatedSlotCall::delayCall() { updateTimers(); } void ConsolidatedSlotCall::initTimers() { minTimer.setInterval(5); maxTimer.setInterval(25); QObject::connect(&minTimer, &QTimer::timeout, this, &ConsolidatedSlotCall::timeout); QObject::connect(&maxTimer, &QTimer::timeout, this, &ConsolidatedSlotCall::timeout); } void ConsolidatedSlotCall::updateTimers() { QMetaObject::invokeMethod(&minTimer,[this]() // make sure timers are started/stopped inside the correct thread { if (!maxTimer.isActive()) { maxTimer.start(); } minTimer.stop(); minTimer.start(); }); }The full implementation is a lot more involved. There is some type erasure going on and the parameters have to be saved and finally applied when the signal gets emitted. (You see
functionandparameterPackin the first method I posted.) If you want I can post some more code for this solution.- This is the approach I use most often for progress updates: I'm using
QTimeand check if 50ms have elapsed and only after this I actually update the progress. This would be done on the senders side and I don't think it takes a lot of time. It used to be quite nice because there were member functionselapsed()andrestart(). These are not available in Qt 6 anymore. You have to usemsecsTo()andQTime::currentTime()instead. I guess that in your case you would have a separate QTime for each kind of signal you can emit. This would throttle each signal individually. This approach is actually the easiest to implement and plenty fast.
-
-
Here's a demonstration of the mailbox idea I alluded to, using an atomic pointer to post the latest state. At most, there are 3 live versions at a time: 1 in progress, 1 in the mailbox, and 1 being processed in the main thread.
Edit: I should have referred to this pattern as triple buffering
#include <QCoreApplication> #include <QThread> #include <QDebug> #include <QTimer> struct data { int count; }; struct Sender : public QThread { QAtomicPointer<data> & m_mailbox; Sender(QAtomicPointer<data> & mailbox, QObject *parent=nullptr) : QThread(parent), m_mailbox(mailbox) {} void run() override { data *next = nullptr; int counter = 0; while (!isInterruptionRequested()) { if (!next) { next = new data; } next->count = counter++; next = m_mailbox.fetchAndStoreOrdered(next); } } }; int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); QAtomicPointer<data> mailbox; Sender s(mailbox); s.start(); QTimer t; t.setInterval(1000); QObject::connect(&t, &QTimer::timeout, [&]() { data *next = mailbox.fetchAndStoreOrdered(nullptr); if (!next) return; qDebug() << "acquired new value" << next->count; delete next; }); t.start(); a.exec(); }@jeremy_k
Hi Jeremy, thanks for this. So that I understand; this essentially allows a single updated data-message to be passed from producer to consumer. The atomic stuff allows for safe read/write across thread boundaries. The consumer uses sleep + poll to read.And the point, compared to sending signals, is that we don't end up with a stream of unprocessed signals sitting around taking up memory until they are pulled at consumer side.
I am thinking aloud about this. I am not keen on the consumer side doing sleep & poll. I would like consumer to see message/data as soon as it is available and consumer thread is running (unknown time after producer thread was running and posted data) while at same time not sleeping or polling. A signal does that for me. I don't fancy a
QTimer(0)for checking as that is too busy, as is QTimer(1), while QTimer(1000) is not frequent enough. But the problem with signals alone is that there can be too many of them in the event queue.I think I could adapt your idea of "sharing" data between producer and consumer without making consumer issue a cross-thread request for latest data along the lines of:
- Producer uses your code to store data first time in mailbox.
- At this point it sends a signal "data ready". And notes that it has sent signal.
- Producer produces new data.
- It replaces state in mailbox, but seeing that signal has been sent it does not send another signal.
- Consumer at some point runs and receives "data ready" signal.
- At that point it reads mailbox and lets consumer know it has done so. In your code by storing
nullptrback to mailbox data. - Next time producer has data to send it sees that consumer has read previous data and so additionally sends new "data ready" signal, and we are back at step #1.
The essential of what we are doing here is changing producer Qt signals which currently contain data over to signals with no data parameters and storing the data in the shared mailbox instead. Which allows us to reduce to just a single signal from producer per consumer read of that signal, with data elsewhere but immediately accessible to consumer without having to query producer. That is interesting. I will think about this :)
-
This problem reminds me of progress dialogs: If you have a fast loop and update the progress bar/progress dialog in each iteration the GUI will hang. The same thing will happen in your case and for a good user experience you should absolutely do something about that. Here are a few ideas (some derived from earlier posts):
-
You mentioned the "signal queuer-reducer". Instead of putting it into the sender or receiver thread, you could have a third thread running. This third thread could be tasked with just recuding the number of actual signals send to the intended receiver. This would leave enough time for both the emulator thread and the GUI thread.
-
I'm not sure how the
compressEvent()is supposed to work. I do know thatupdate()first collects several calls. I'm not sure how to google for this (and I believe it has gotten harder to find this), but there is an established approach using two timers: The first timer is to delay firing the actual signal just in case the same signal comes in again. Everytime the same signal comes in, the timer is reset. This just adds a tiny delay to the update after the last signal was emitted. Maybe you could choose something like 5 or 10ms for this first timer. The problem, however, is that in your specific case the signal might never be emitted because the first timer is always reset. That's where the second timer comes in: It has a longer timeout and is triggered with the first signal. I can be something like 50ms. On timeout it emits the last signal. This makes sure that occasionally something happens. Here are some of the important functions for a class that I have:
void ConsolidatedSlotCall::timeout() { minTimer.stop(); maxTimer.stop(); if(function && parameterPack) { function->callWithParameters(parameterPack); emit slotTriggered(); } } void ConsolidatedSlotCall::delayCall() { updateTimers(); } void ConsolidatedSlotCall::initTimers() { minTimer.setInterval(5); maxTimer.setInterval(25); QObject::connect(&minTimer, &QTimer::timeout, this, &ConsolidatedSlotCall::timeout); QObject::connect(&maxTimer, &QTimer::timeout, this, &ConsolidatedSlotCall::timeout); } void ConsolidatedSlotCall::updateTimers() { QMetaObject::invokeMethod(&minTimer,[this]() // make sure timers are started/stopped inside the correct thread { if (!maxTimer.isActive()) { maxTimer.start(); } minTimer.stop(); minTimer.start(); }); }The full implementation is a lot more involved. There is some type erasure going on and the parameters have to be saved and finally applied when the signal gets emitted. (You see
functionandparameterPackin the first method I posted.) If you want I can post some more code for this solution.- This is the approach I use most often for progress updates: I'm using
QTimeand check if 50ms have elapsed and only after this I actually update the progress. This would be done on the senders side and I don't think it takes a lot of time. It used to be quite nice because there were member functionselapsed()andrestart(). These are not available in Qt 6 anymore. You have to usemsecsTo()andQTime::currentTime()instead. I guess that in your case you would have a separate QTime for each kind of signal you can emit. This would throttle each signal individually. This approach is actually the easiest to implement and plenty fast.
@SimonSchroeder said in How many emitted signals is too many?:
You mentioned the "signal queuer-reducer". Instead of putting it into the sender or receiver thread, you could have a third thread running. This third thread could be tasked with just recuding the number of actual signals send to the intended receiver. This would leave enough time for both the emulator thread and the GUI thread.
Hi, yes, I had considered this! Apart from the extra code, I "worry" theoretically about timings here. Once we have repeated signals emitted by one thread to be consumed or thinned in any other thread, be that the Ui or a tertiary one, we could have: producer bangs out 100,000 signals before other/tertiary thread happens to be scheduled to pull them and that uses a lot of Qt's event loop memory to store the queue. I don't like that from a "purist" point of view.
Have a look at @jeremy_k's suggestion above and my reply. Looks like separating data content (to be single, replaceable, atomically) from signal allows for single signal emission per many changing, overwritable data, so low memory (and processing) overhead.
-
-
I have run into similar situations where a value could update multiple times in short intervals and resulted in a flood of GUI updates.
What I ended up doing was the following:
Each value became its own full fledged property/class: getter, setter, signal and a QTimer object.
When the setter is called and the timer is not running emit the signal directly, and start the timer.
When the setter is called and the timer is running, set a flag to schedule a signal on timer timeout.
In the timeout slot, stop the timer, if the signal is scheduled emit the signal with the latest value as argument and restart timer. If ne emit is scheduled we end up in original state againSomething like this, quick moc up not actual production ;)
#include <QObject> #include <QVariant> #include <QTimer> class BaseProperty final : public QObject { Q_OBJECT Q_PROPERTY(QVariant value READ value WRITE setValue NOTIFY valueChanged) Q_PROPERTY(int intervalMs READ intervalMs WRITE setIntervalMs NOTIFY intervalMsChanged) public: explicit BaseProperty(QObject* parent = nullptr) : QObject(parent) { m_timer.setSingleShot(true); QObject::connect(&m_timer, &QTimer::timeout, this, &BaseProperty::onTimeout); } QVariant value() const { return m_value; } int intervalMs() const { return m_intervalMs; } public slots: void setIntervalMs(int ms) { if (ms < 0) ms = 0; if (m_intervalMs == ms) return; m_intervalMs = ms; emit intervalMsChanged(m_intervalMs); } void setValue(const QVariant& value) { if(value == m_value) { return; } m_value = value; if (!m_timer.isActive()) { emit valueChanged(m_value); m_timer.start(m_intervalMs); m_pendingEmit = false; return; } m_pendingEmit = true; } signals: void valueChanged(const QVariant& value); void intervalMsChanged(int intervalMs); private slots: void onTimeout() { if (!m_pendingEmit) { return; } m_pendingEmit = false; emit valueChanged(m_value); m_timer.start(m_intervalMs); } private: QVariant m_value; QTimer m_timer; int m_intervalMs = 50; bool m_pendingEmit = false; }; -
I have run into similar situations where a value could update multiple times in short intervals and resulted in a flood of GUI updates.
What I ended up doing was the following:
Each value became its own full fledged property/class: getter, setter, signal and a QTimer object.
When the setter is called and the timer is not running emit the signal directly, and start the timer.
When the setter is called and the timer is running, set a flag to schedule a signal on timer timeout.
In the timeout slot, stop the timer, if the signal is scheduled emit the signal with the latest value as argument and restart timer. If ne emit is scheduled we end up in original state againSomething like this, quick moc up not actual production ;)
#include <QObject> #include <QVariant> #include <QTimer> class BaseProperty final : public QObject { Q_OBJECT Q_PROPERTY(QVariant value READ value WRITE setValue NOTIFY valueChanged) Q_PROPERTY(int intervalMs READ intervalMs WRITE setIntervalMs NOTIFY intervalMsChanged) public: explicit BaseProperty(QObject* parent = nullptr) : QObject(parent) { m_timer.setSingleShot(true); QObject::connect(&m_timer, &QTimer::timeout, this, &BaseProperty::onTimeout); } QVariant value() const { return m_value; } int intervalMs() const { return m_intervalMs; } public slots: void setIntervalMs(int ms) { if (ms < 0) ms = 0; if (m_intervalMs == ms) return; m_intervalMs = ms; emit intervalMsChanged(m_intervalMs); } void setValue(const QVariant& value) { if(value == m_value) { return; } m_value = value; if (!m_timer.isActive()) { emit valueChanged(m_value); m_timer.start(m_intervalMs); m_pendingEmit = false; return; } m_pendingEmit = true; } signals: void valueChanged(const QVariant& value); void intervalMsChanged(int intervalMs); private slots: void onTimeout() { if (!m_pendingEmit) { return; } m_pendingEmit = false; emit valueChanged(m_value); m_timer.start(m_intervalMs); } private: QVariant m_value; QTimer m_timer; int m_intervalMs = 50; bool m_pendingEmit = false; };@J.Hilk said in How many emitted signals is too many?:
When the setter is called and the timer is not running emit the signal directly, and start the timer.
When the setter is called and the timer is running, set a flag to schedule a signal on timer timeout.This means that if the UI gets to run after step 2 it will only see the update from step 1 and not yet from step 2 since that is in a "pending timeout". It will only see the first update when many further ones could be "pending". From my "purist" pov I don't like this, the UI update is not as up-to-date as it can/should be.
-
with the approach of passing the value as argument through the queue, yes, the ui may be a few milliseconds behind. You could forgo the value in argument and simply use the signal to tell the gui to call the getter and make that mutex locked. That way it would get the latest value, and the getter call could also stop the timer, so no unnecessary operations happen
-
with the approach of passing the value as argument through the queue, yes, the ui may be a few milliseconds behind. You could forgo the value in argument and simply use the signal to tell the gui to call the getter and make that mutex locked. That way it would get the latest value, and the getter call could also stop the timer, so no unnecessary operations happen
@J.Hilk said in How many emitted signals is too many?:
use the signal to tell the gui to call the getter and make that mutex locked
Yes! But I'd like to avoid the sync point for this. I liked @jeremy_k's suggestion above. When he uses producer:
QAtomicPointer<data> & m_mailbox; m_mailbox.fetchAndStoreOrdered(next);and consumer:
data *next = mailbox.fetchAndStoreOrdered(nullptr);is that doing just the same behind the scenes as your explicit "call the getter and make that mutex locked"? Or is it a bit subtler than that?