Commit bbc0f807 authored by Dmitry Kazakov's avatar Dmitry Kazakov

Refactor signal compressor to have better timing properties

* before: emits signals with time range [1.0...2.0] * interval
* after: emits signals with time range [0.5...1.5] * interval

Bascially, now it handles it much better when interval is around
10-20 ms. With the old version it caused KisCanvas2 to drop frames
and look ugly when the user pans the canvas.

CCBUG:409460
parent bf4e20d4
......@@ -28,38 +28,38 @@
* 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 <QTimer>
#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()));
}
......@@ -81,59 +81,125 @@ void KisSignalCompressor::setDelay(int delay)
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)
......
......@@ -22,7 +22,9 @@
#include <QObject>
#include "kritaglobal_export.h"
class KisRelaxedTimer;
#include <QElapsedTimer>
class QTimer;
/**
* Sets a timer to delay or throttle activation of a Qt slot. One example of
......@@ -86,9 +88,15 @@ 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 */
......@@ -5,5 +5,6 @@ macro_add_unittest_definitions()
ecm_add_tests(KisSharedThreadPoolAdapterTest.cpp
KisSignalAutoConnectionTest.cpp
KisSignalCompressorTest.cpp
NAME_PREFIX libs-global-
LINK_LIBRARIES kritaglobal Qt5::Test)
/*
* Copyright (c) 2019 Dmitry Kazakov <dimula73@gmail.com>
*
* 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 <boost/accumulators/accumulators.hpp>
#include <boost/accumulators/statistics/stats.hpp>
#include <boost/accumulators/statistics/min.hpp>
#include <boost/accumulators/statistics/max.hpp>
#include <boost/accumulators/statistics/mean.hpp>
#include <boost/accumulators/statistics/variance.hpp>
#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<qreal, stats> 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"
/*
* Copyright (c) 2019 Dmitry Kazakov <dimula73@gmail.com>
*
* 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 <QtTest>
#include <QObject>
class KisSignalCompressorTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void test();
};
#endif // KISSIGNALCOMPRESSORTEST_H
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment