Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups
  • Search
  • Get Qt Extensions
  • Unsolved
Collapse
Brand Logo
  1. Home
  2. General talk
  3. Brainstorm
  4. How many emitted signals is too many?
Forum Updated to NodeBB v4.3 + New Features

How many emitted signals is too many?

Scheduled Pinned Locked Moved Unsolved Brainstorm
20 Posts 5 Posters 336 Views 4 Watching
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • JonBJ Online
    JonBJ Online
    JonB
    wrote last edited by
    #10

    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 :)

    1 Reply Last reply
    0
    • SGaistS Offline
      SGaistS Offline
      SGaist
      Lifetime Qt Champion
      wrote last edited by
      #11

      Hi,

      I think you might be looking for "signal compression".

      This StackOverflow answer does a good job explaining it.

      Interested in AI ? www.idiap.ch
      Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

      JonBJ 1 Reply Last reply
      2
      • SGaistS SGaist

        Hi,

        I think you might be looking for "signal compression".

        This StackOverflow answer does a good job explaining it.

        JonBJ Online
        JonBJ Online
        JonB
        wrote last edited by
        #12

        @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.

        1 Reply Last reply
        0
        • jeremy_kJ Offline
          jeremy_kJ Offline
          jeremy_k
          wrote last edited by jeremy_k
          #13

          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();
          }
          

          Asking a question about code? http://eel.is/iso-c++/testcase/

          JonBJ 1 Reply Last reply
          1
          • S Offline
            S Offline
            SimonSchroeder
            wrote last edited by
            #14

            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):

            1. 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.

            2. I'm not sure how the compressEvent() is supposed to work. I do know that update() 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 function and parameterPack in the first method I posted.) If you want I can post some more code for this solution.

            1. This is the approach I use most often for progress updates: I'm using QTime and 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 functions elapsed() and restart(). These are not available in Qt 6 anymore. You have to use msecsTo() and QTime::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.
            JonBJ 1 Reply Last reply
            1
            • jeremy_kJ jeremy_k

              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();
              }
              
              JonBJ Online
              JonBJ Online
              JonB
              wrote last edited by JonB
              #15

              @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:

              1. Producer uses your code to store data first time in mailbox.
              2. At this point it sends a signal "data ready". And notes that it has sent signal.
              3. Producer produces new data.
              4. It replaces state in mailbox, but seeing that signal has been sent it does not send another signal.
              5. Consumer at some point runs and receives "data ready" signal.
              6. At that point it reads mailbox and lets consumer know it has done so. In your code by storing nullptr back to mailbox data.
              7. 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 :)

              1 Reply Last reply
              0
              • S SimonSchroeder

                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):

                1. 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.

                2. I'm not sure how the compressEvent() is supposed to work. I do know that update() 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 function and parameterPack in the first method I posted.) If you want I can post some more code for this solution.

                1. This is the approach I use most often for progress updates: I'm using QTime and 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 functions elapsed() and restart(). These are not available in Qt 6 anymore. You have to use msecsTo() and QTime::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.
                JonBJ Online
                JonBJ Online
                JonB
                wrote last edited by
                #16

                @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.

                1 Reply Last reply
                0
                • J.HilkJ Offline
                  J.HilkJ Offline
                  J.Hilk
                  Moderators
                  wrote last edited by
                  #17

                  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 again

                  Something 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;
                  };
                  
                  

                  Be aware of the Qt Code of Conduct, when posting : https://forum.qt.io/topic/113070/qt-code-of-conduct


                  Q: What's that?
                  A: It's blue light.
                  Q: What does it do?
                  A: It turns blue.

                  JonBJ 1 Reply Last reply
                  1
                  • J.HilkJ J.Hilk

                    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 again

                    Something 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;
                    };
                    
                    
                    JonBJ Online
                    JonBJ Online
                    JonB
                    wrote last edited by
                    #18

                    @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.

                    1 Reply Last reply
                    0
                    • J.HilkJ Offline
                      J.HilkJ Offline
                      J.Hilk
                      Moderators
                      wrote last edited by
                      #19

                      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


                      Be aware of the Qt Code of Conduct, when posting : https://forum.qt.io/topic/113070/qt-code-of-conduct


                      Q: What's that?
                      A: It's blue light.
                      Q: What does it do?
                      A: It turns blue.

                      JonBJ 1 Reply Last reply
                      0
                      • J.HilkJ J.Hilk

                        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

                        JonBJ Online
                        JonBJ Online
                        JonB
                        wrote last edited by
                        #20

                        @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?

                        1 Reply Last reply
                        0

                        • Login

                        • Login or register to search.
                        • First post
                          Last post
                        0
                        • Categories
                        • Recent
                        • Tags
                        • Popular
                        • Users
                        • Groups
                        • Search
                        • Get Qt Extensions
                        • Unsolved