diff --git a/libs/global/kis_signal_compressor.cpp b/libs/global/kis_signal_compressor.cpp index 9cf7936c35..a87039b09b 100644 --- a/libs/global/kis_signal_compressor.cpp +++ b/libs/global/kis_signal_compressor.cpp @@ -1,142 +1,208 @@ /* * Copyright (c) 2013 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ /** * KisSignalCompressor will never trigger timeout more often than every \p delay ms, * i.e. \p delay ms is a given lower limit defining the highest frequency. * * The current implementation uses a long-running monitor timer to eliminate the * overhead incurred by restarting and stopping timers with each signal. The * consequence of this is that the given \p delay ms is not always exactly followed. * * KisSignalCompressor makes the following callback guarantees (0 < err <= 1, with * err == 0 if this is the first signal after a while): * * POSTPONE: - * - timeout after <= (1 + err) * \p delay ms. + * - timeout after = [0.5 ... 1.0] * \p delay ms. * FIRST_ACTIVE_POSTPONE_NEXT: * - first timeout immediately - * - postponed timeout after (1 + err) * \p delay ms + * - postponed timeout after [0.5 ... 1.0] * \p delay ms * FIRST_ACTIVE: * - first timeout immediately - * - second timeout after (1 + err) * \p delay ms - * - after that: \p delay ms - * FIRST_INACTIVE: - * - timeout after (1 + err) * \p delay ms + * - after that [0.5 ... 1.5] * \p delay ms + * FIRST_INACTIVE: + * - timeout after [0.5 ... 1.5] * \p delay ms */ #include "kis_signal_compressor.h" -#include "kis_relaxed_timer.h" + +#include +#include "kis_assert.h" +#include "kis_debug.h" + KisSignalCompressor::KisSignalCompressor() : QObject(0) - , m_timer(new KisRelaxedTimer(this)) - , m_mode(UNDEFINED) - , m_gotSignals(false) + , m_timer(new QTimer(this)) { - m_timer->setSingleShot(true); + m_timer->setSingleShot(false); connect(m_timer, SIGNAL(timeout()), SLOT(slotTimerExpired())); } KisSignalCompressor::KisSignalCompressor(int delay, Mode mode, QObject *parent) : QObject(parent), - m_timer(new KisRelaxedTimer(this)), - m_mode(mode), - m_gotSignals(false) + m_timer(new QTimer(this)), + m_mode(mode) { - m_timer->setSingleShot(true); + m_timer->setSingleShot(false); m_timer->setInterval(delay); connect(m_timer, SIGNAL(timeout()), SLOT(slotTimerExpired())); } void KisSignalCompressor::setDelay(int delay) { const bool wasActive = m_timer->isActive(); if (wasActive) { m_timer->stop(); } m_timer->setInterval(delay); if (wasActive) { m_timer->start(); } } void KisSignalCompressor::start() { - Q_ASSERT(m_mode != UNDEFINED); + KIS_SAFE_ASSERT_RECOVER_RETURN(m_mode != UNDEFINED); + + const bool isFirstStart = !m_timer->isActive(); + + KIS_SAFE_ASSERT_RECOVER_NOOP(!isFirstStart || !m_signalsPending); switch (m_mode) { case POSTPONE: - m_timer->start(); + if (isFirstStart) { + m_timer->start(); + } + m_lastEmittedTimer.restart(); + m_signalsPending = true; break; case FIRST_ACTIVE_POSTPONE_NEXT: case FIRST_ACTIVE: - if (!m_timer->isActive()) { - m_gotSignals = false; + if (isFirstStart) { m_timer->start(); - emit timeout(); + m_lastEmittedTimer.restart(); + m_signalsPending = false; + if (!tryEmitSignalSafely()) { + m_signalsPending = true; + } } else { - m_gotSignals = true; - if (m_mode == FIRST_ACTIVE_POSTPONE_NEXT) { - m_timer->start(); - } else if (m_mode == FIRST_ACTIVE && m_timer->remainingTime() == 0) { - // overdue, swamped by other events - m_timer->stop(); - slotTimerExpired(); + if (m_mode == FIRST_ACTIVE) { + m_signalsPending = true; + tryEmitOnTick(false); + } else { + m_lastEmittedTimer.restart(); + m_signalsPending = true; } } break; case FIRST_INACTIVE: - if (!m_timer->isActive()) { + if (isFirstStart) { m_timer->start(); + m_lastEmittedTimer.restart(); + m_signalsPending = true; + } else { + m_signalsPending = true; + tryEmitOnTick(false); } case UNDEFINED: ; // Should never happen, but do nothing }; - if (m_mode == POSTPONE || !m_timer->isActive()) { + KIS_SAFE_ASSERT_RECOVER(m_timer->isActive()) { m_timer->start(); } } -void KisSignalCompressor::slotTimerExpired() +bool KisSignalCompressor::tryEmitOnTick(bool isFromTimer) +{ + bool wasEmitted = false; + + // we have different requirements for hi-frequency events (the mean + // of the events rate must be min(compressorRate, eventsRate) + const int realInterval = m_timer->interval(); + const int minInterval = realInterval < 100 ? 0.5 * realInterval : realInterval; + + // Enable for debugging: + // ENTER_FUNCTION() << ppVar(isFromTimer) << ppVar(m_signalsPending) << m_lastEmittedTimer.elapsed(); + + if (m_signalsPending && m_lastEmittedTimer.elapsed() >= minInterval) { + KIS_SAFE_ASSERT_RECOVER_NOOP(!isFromTimer || !m_isEmitting); + + m_lastEmittedTimer.start(); + m_signalsPending = false; + if (!tryEmitSignalSafely()) { + m_signalsPending = true; + } + wasEmitted = true; + } else if (!isFromTimer) { + m_signalsPending = true; + } + + return wasEmitted; +} + +bool KisSignalCompressor::tryEmitSignalSafely() { - Q_ASSERT(m_mode != UNDEFINED); - if ((m_mode != FIRST_ACTIVE && m_mode != FIRST_ACTIVE_POSTPONE_NEXT) || m_gotSignals) { - m_gotSignals = false; + bool wasEmitted = false; + + m_isEmitting++; + + if (m_isEmitting == 1) { emit timeout(); + wasEmitted = true; + } + + m_isEmitting--; + + return wasEmitted; +} + +void KisSignalCompressor::slotTimerExpired() +{ + KIS_ASSERT_RECOVER_NOOP(m_mode != UNDEFINED); + if (!tryEmitOnTick(true)) { + const int calmDownInterval = 5 * m_timer->interval(); + + if (!m_lastEmittedTimer.isValid() || + m_lastEmittedTimer.elapsed() > calmDownInterval) { + + m_timer->stop(); + } } } void KisSignalCompressor::stop() { m_timer->stop(); + m_signalsPending = false; + m_lastEmittedTimer.invalidate(); } bool KisSignalCompressor::isActive() const { - return m_timer->isActive() && (m_mode != FIRST_ACTIVE || m_gotSignals); + return m_signalsPending && m_timer->isActive(); } void KisSignalCompressor::setMode(KisSignalCompressor::Mode mode) { m_mode = mode; } diff --git a/libs/global/kis_signal_compressor.h b/libs/global/kis_signal_compressor.h index 2c03f7fea9..1b6b9db84b 100644 --- a/libs/global/kis_signal_compressor.h +++ b/libs/global/kis_signal_compressor.h @@ -1,94 +1,102 @@ /* * Copyright (c) 2013 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_SIGNAL_COMPRESSOR_H #define __KIS_SIGNAL_COMPRESSOR_H #include #include "kritaglobal_export.h" -class KisRelaxedTimer; +#include + +class QTimer; /** * Sets a timer to delay or throttle activation of a Qt slot. One example of * where this is used is to limit the amount of expensive redraw activity on the * canvas. * * There are three behaviors to choose from. * * POSTPONE resets the timer after each call. Therefore if the calls are made * quickly enough, the timer will never be activated. * * FIRST_ACTIVE_POSTPONE_NEXT emits the first signal and postpones all * the other actions the other action like in POSTPONE. This mode is * used e.g. in move/remove layer functionality. If you remove a * single layer, you'll see the result immediately. But if you want to * remove multiple layers, you should wait until all the actions are * finished. * * FIRST_ACTIVE emits the timeout() event immediately and sets a timer of * duration \p delay. If the compressor is triggered during this time, it will * wait until the end of the delay period to fire the signal. Further events are * ignored until the timer elapses. Think of it as a queue with size 1, and * where the leading element is popped every \p delay ms. * * FIRST_INACTIVE emits the timeout() event at the end of a timer of duration \p * delay ms. The compressor becomes inactive and all events are ignored until * the timer has elapsed. * * The current implementation allows the timeout() to be delayed by up to 2 times * \p delay in certain situations (for details see cpp file). */ class KRITAGLOBAL_EXPORT KisSignalCompressor : public QObject { Q_OBJECT public: enum Mode { POSTPONE, /* Calling start() resets the timer to \p delay ms */ FIRST_ACTIVE_POSTPONE_NEXT, /* emits the first signal and postpones all the next ones */ FIRST_ACTIVE, /* Emit timeout() signal immediately. Throttle further timeout() to rate of one per \p delay ms */ FIRST_INACTIVE, /* Set a timer \p delay ms, emit timeout() when it elapses. Ignore all events meanwhile. */ UNDEFINED /* KisSignalCompressor is created without an explicit mode */ }; public: KisSignalCompressor(); KisSignalCompressor(int delay, Mode mode, QObject *parent = 0); bool isActive() const; void setMode(Mode mode); public Q_SLOTS: void setDelay(int delay); void start(); void stop(); private Q_SLOTS: void slotTimerExpired(); Q_SIGNALS: void timeout(); private: - KisRelaxedTimer *m_timer; - Mode m_mode; - bool m_gotSignals; + bool tryEmitOnTick(bool isFromTimer); + bool tryEmitSignalSafely(); + +private: + QTimer *m_timer = 0; + Mode m_mode = UNDEFINED; + bool m_signalsPending = false; + QElapsedTimer m_lastEmittedTimer; + int m_isEmitting = 0; }; #endif /* __KIS_SIGNAL_COMPRESSOR_H */ diff --git a/libs/global/tests/CMakeLists.txt b/libs/global/tests/CMakeLists.txt index ca8e3eff8f..48093cba83 100644 --- a/libs/global/tests/CMakeLists.txt +++ b/libs/global/tests/CMakeLists.txt @@ -1,9 +1,10 @@ include(ECMAddTests) include(KritaAddBrokenUnitTest) macro_add_unittest_definitions() ecm_add_tests(KisSharedThreadPoolAdapterTest.cpp KisSignalAutoConnectionTest.cpp + KisSignalCompressorTest.cpp NAME_PREFIX libs-global- LINK_LIBRARIES kritaglobal Qt5::Test) diff --git a/libs/global/tests/KisSignalCompressorTest.cpp b/libs/global/tests/KisSignalCompressorTest.cpp new file mode 100644 index 0000000000..b25f8a7351 --- /dev/null +++ b/libs/global/tests/KisSignalCompressorTest.cpp @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2019 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "KisSignalCompressorTest.h" + +#include "QTimer" +#include "kis_signal_compressor.h" + +#include +#include +#include +#include +#include +#include + +#include "kis_debug.h" + +struct CompressorTester : public QObject +{ + Q_OBJECT +public Q_SLOTS: + void start() { + if (!m_timer.isValid()) { + m_timer.start(); + } else { + m_acc(m_timer.restart()); + } + } +public: + void dump(const QString &testName) { + qDebug() << testName + << "cnt:" << boost::accumulators::count(m_acc) + << "min:" << boost::accumulators::min(m_acc) + << "max:" << boost::accumulators::max(m_acc) + << "mean:" << boost::accumulators::mean(m_acc) + << "var:" << std::sqrt(boost::accumulators::variance(m_acc)); + } + +private: + typedef boost::accumulators::stats< + boost::accumulators::tag::min, + boost::accumulators::tag::max, + boost::accumulators::tag::mean, + boost::accumulators::tag::variance> stats; + + boost::accumulators::accumulator_set m_acc; + QElapsedTimer m_timer; +}; + +void testCompression(int timerInterval, int compressorInterval) +{ + CompressorTester tester; + KisSignalCompressor compressor(compressorInterval, KisSignalCompressor::FIRST_ACTIVE); + QTimer timer; + timer.setInterval(timerInterval); + timer.setTimerType(Qt::PreciseTimer); + timer.setSingleShot(false); + + QObject::connect(&timer, SIGNAL(timeout()), &compressor, SLOT(start())); + QObject::connect(&compressor, SIGNAL(timeout()), &tester, SLOT(start())); + + timer.start(); + + QTest::qWait(500); + + + timer.stop(); + QTest::qWait(compressorInterval * 2); + compressor.stop(); + + tester.dump(QString("timer %1 compressor %2").arg(timerInterval).arg(compressorInterval)); + + QTest::qWait(compressorInterval * 10); +} + +void KisSignalCompressorTest::test() +{ + for (int i = 10; i < 50; i++) { + testCompression(i, 25); + } + //testCompression(10, 25); +} + +QTEST_MAIN(KisSignalCompressorTest) + +#include "KisSignalCompressorTest.moc" diff --git a/libs/global/tests/KisSignalCompressorTest.h b/libs/global/tests/KisSignalCompressorTest.h new file mode 100644 index 0000000000..83c5faf9c2 --- /dev/null +++ b/libs/global/tests/KisSignalCompressorTest.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef KISSIGNALCOMPRESSORTEST_H +#define KISSIGNALCOMPRESSORTEST_H + +#include +#include + +class KisSignalCompressorTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void test(); +}; + +#endif // KISSIGNALCOMPRESSORTEST_H