diff --git a/libs/global/CMakeLists.txt b/libs/global/CMakeLists.txt --- a/libs/global/CMakeLists.txt +++ b/libs/global/CMakeLists.txt @@ -18,6 +18,7 @@ kis_painting_tweaks.cpp KisHandlePainterHelper.cpp KisHandleStyle.cpp + kis_relaxed_timer.cpp kis_signal_compressor.cpp kis_signal_compressor_with_param.cpp kis_acyclic_signal_connector.cpp diff --git a/libs/global/kis_relaxed_timer.h b/libs/global/kis_relaxed_timer.h new file mode 100644 --- /dev/null +++ b/libs/global/kis_relaxed_timer.h @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2017 Bernhard Liebl + * + * 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_RELAXED_TIMER_H +#define __KIS_RELAXED_TIMER_H + +#include +#include +#include + +#include "kritaglobal_export.h" + +/** + * A timer using an interface like QTimer that relaxes the given interval callback + * time guarantees and minimizes internal timer restarts by keeping one long-running + * repeating timer. + * + * Users can use this just like a QTimer. The difference is that KisRelaxedTimer will + * relax the callback guarantee time as follows: timeouts will never happen earlier + * than \p interval ms, but may well happen only after 2 * \p interval ms (whereas + * QTimer guarantees a fixed interval of \p interval ms). + * + * The rationale for using this is that stopping and starting timers can produce a + * measurable performance overhead. KisRelaxedTimer removes that overhead. + */ +class KRITAGLOBAL_EXPORT KisRelaxedTimer : public QObject +{ + Q_OBJECT + +public: + KisRelaxedTimer(QObject *parent = nullptr); + + void start(); + + inline void stop() { + m_emitOnTimeTick = 0; + } + + void setInterval(int interval); + void setSingleShot(bool singleShot); + + inline bool isActive() const { + return m_emitOnTimeTick >= m_nextTimerTickSeqNo; + } + + int remainingTime() const; + +Q_SIGNALS: + void timeout(); + +protected: + void timerEvent(QTimerEvent *event) override; + +private: + int m_interval; + bool m_singleShot; + + QBasicTimer m_timer; + qint64 m_nextTimerTickSeqNo; + qint64 m_emitOnTimeTick; + + QElapsedTimer m_elapsed; + +protected: + class IsEmitting { + public: + IsEmitting(KisRelaxedTimer &timer) : m_timer(timer) { + timer.m_isEmitting = true; + } + + ~IsEmitting() { + m_timer.m_isEmitting = false; + } + + private: + KisRelaxedTimer &m_timer; + }; + + friend class IsEmitting; + + bool m_isEmitting; +}; + +#endif /* __KIS_RELAXED_TIMER_H */ diff --git a/libs/global/kis_relaxed_timer.cpp b/libs/global/kis_relaxed_timer.cpp new file mode 100644 --- /dev/null +++ b/libs/global/kis_relaxed_timer.cpp @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2017 Bernhard Liebl + * + * 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 "kis_relaxed_timer.h" + +KisRelaxedTimer::KisRelaxedTimer(QObject *parent) + : QObject(parent) + , m_interval(0) + , m_singleShot(false) + , m_nextTimerTickSeqNo(1) + , m_emitOnTimeTick(0) + , m_isEmitting(false) +{ +} + +void KisRelaxedTimer::setInterval(int interval) +{ + Q_ASSERT(!isActive()); + m_interval = interval; +} + +void KisRelaxedTimer::setSingleShot(bool singleShot) +{ + m_singleShot = singleShot; +} + +int KisRelaxedTimer::remainingTime() const +{ + // in contrast to normal QTimers, the remaining time is calculated in + // terms of 2 * m_interval as this is the worst case interval. + + if (!isActive()) { + return -1; + } else { + return qMax(qint64(0), 2 * m_interval - qint64(m_elapsed.elapsed())); + } +} + +void KisRelaxedTimer::start() +{ + m_elapsed.start(); + + // cancels any previously scheduled timer and schedules a new timer to be + // triggered as soon as possible, but never sooner than \p m_interval ms. + + if (!m_timer.isActive()) { + // no internal timer is running. start one, and configure it to send + // us a timeout event on the next possible tick which will be exactly + // \p m_interval ms in the future. + + m_emitOnTimeTick = m_nextTimerTickSeqNo; + m_timer.start(m_interval, this); + } else if (m_isEmitting) { + // an internal timer is running and we are actually called from a + // timeout event. so we know the next tick will happen in exactly + // \p m_interval ms. + + m_emitOnTimeTick = m_nextTimerTickSeqNo; + } else { + // an internal timer is already running, but we do not know when + // the next tick will happen. we need to skip next tick as it + // will be sooner than m_delay. the one after that will be good as + // it will be m_interval * (1 + err) in the future. + + m_emitOnTimeTick = m_nextTimerTickSeqNo + 1; + } +} + +void KisRelaxedTimer::timerEvent(QTimerEvent *event) +{ + Q_UNUSED(event); + + const int ticksStopThreshold = 5; + + const qint64 timerTickSeqNo = m_nextTimerTickSeqNo; + + // from this point on, if this is an emit tick, we are no longer active. + m_nextTimerTickSeqNo++; + + if (timerTickSeqNo == m_emitOnTimeTick) { + if (m_singleShot) { + stop(); + } + const IsEmitting emitting(*this); + emit timeout(); + } else if (timerTickSeqNo - m_emitOnTimeTick > ticksStopThreshold) { + m_timer.stop(); + } +} diff --git a/libs/global/kis_signal_compressor.h b/libs/global/kis_signal_compressor.h --- a/libs/global/kis_signal_compressor.h +++ b/libs/global/kis_signal_compressor.h @@ -19,10 +19,10 @@ #ifndef __KIS_SIGNAL_COMPRESSOR_H #define __KIS_SIGNAL_COMPRESSOR_H -#include +#include #include "kritaglobal_export.h" -class QTimer; +class KisRelaxedTimer; /** * Sets a timer to delay or throttle activation of a Qt slot. One example of @@ -51,6 +51,8 @@ * 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 { @@ -83,7 +85,7 @@ void timeout(); private: - QTimer *m_timer; + KisRelaxedTimer *m_timer; Mode m_mode; bool m_gotSignals; }; diff --git a/libs/global/kis_signal_compressor.cpp b/libs/global/kis_signal_compressor.cpp --- a/libs/global/kis_signal_compressor.cpp +++ b/libs/global/kis_signal_compressor.cpp @@ -16,14 +16,36 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_signal_compressor.h" - -#include +/** + * 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. + * FIRST_ACTIVE_POSTPONE_NEXT: + * - first timeout immediately + * - postponed timeout after (1 + err) * \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 + */ +#include "kis_signal_compressor.h" +#include "kis_relaxed_timer.h" KisSignalCompressor::KisSignalCompressor() : QObject(0) - , m_timer(new QTimer(this)) + , m_timer(new KisRelaxedTimer(this)) , m_mode(UNDEFINED) , m_gotSignals(false) { @@ -33,7 +55,7 @@ KisSignalCompressor::KisSignalCompressor(int delay, Mode mode, QObject *parent) : QObject(parent), - m_timer(new QTimer(this)), + m_timer(new KisRelaxedTimer(this)), m_mode(mode), m_gotSignals(false) {