[graphics/krita] /: Fix frivolous usage of precise timers in Krita

Dmitry Kazakov null at kde.org
Thu Jun 12 12:54:38 BST 2025


Git commit dcbac2e4d957a1ca8a73ebdb9586db3d2d9959eb by Dmitry Kazakov.
Committed on 12/06/2025 at 11:50.
Pushed by dkazakov into branch 'master'.

Fix frivolous usage of precise timers in Krita

Extract from HACKING:

We should be carefult about allocation of timers in Krita. The number of precision
timers is limited on some platforms (e.g. Windows). When Qt runs out of precision
timers it silently degrades all newly created timers into non-precision ones. That
can affect timers that are used for the canvas framerate stabilization and cause
tearing and FPS drops.

Rules of thumb:

1) When using QTimer::singleShot(), either use **null** timeout to just push the
   event through the queue, or pass Qt::CoarseTimer as the second argument:

   QTimer::singleShot(0, ...);

   or

   QTimer::singleShot(<some non-zero value>, Qt::CoarseTimer, ...);

   If you really-really-really need a precise single-shot event, then pass
   Qt::PreciseTimer explicitly.

   The problem is that QTimer::singleShot() creates a precise timer by default,
   which can consume all the available precise timers in the system for the app.

2) When creating timers or using KisSignalCompressor, set timeout to values
   strictly higher than 20 ms (unless you really know what you are doing). Value
   `25 ms` is a good default to compress updates for the lightweight widgets,
   and value `100 ms` is a good default to compress updates for heavy widgets.

   The problem is that Qt internally converts all the timers into precise
   ones, if their timeout is less or equal 20. Which can also consume all
   the available precise timers.

CC:kimageshop at kde.org

M  +34   -0    HACKING
M  +2    -2    libs/flake/KoSelection_p.h
M  +1    -1    libs/ui/KisMainWindow.cpp
M  +1    -1    libs/ui/kis_animation_cache_populator.cpp
M  +1    -1    libs/ui/kis_safe_document_loader.cpp
M  +1    -1    libs/ui/tool/kis_tool_select_base.h
M  +9    -5    libs/widgets/KisHsvColorSlider.cpp
M  +12   -2    libs/widgets/KoColorSlider.cpp
M  +1    -1    libs/widgets/KoDialog.cpp
M  +1    -1    plugins/dockers/advancedcolorselector/kis_color_selector.cpp
M  +1    -1    plugins/dockers/advancedcolorselector/kis_color_selector_base.cpp
M  +1    -1    plugins/dockers/animation/KisAnimTimelineFramesView.cpp
M  +1    -1    plugins/dockers/artisticcolorselector/kis_color_selector.cpp
M  +2    -2    plugins/dockers/smallcolorselector/kis_small_color_widget.cc
M  +1    -1    plugins/dockers/widegamutcolorselector/WGSelectorConfigGrid.cpp
M  +1    -2    plugins/python/comics_project_management_tools/comics_project_manager_docker.py

https://invent.kde.org/graphics/krita/-/commit/dcbac2e4d957a1ca8a73ebdb9586db3d2d9959eb

diff --git a/HACKING b/HACKING
index 379c5c7b4d9..64438cc4708 100644
--- a/HACKING
+++ b/HACKING
@@ -237,6 +237,40 @@ Static initializers
     CMake feature, but it is available only since CMake 3.24, so for now
     just avoid using initializers in plugins.
 
+The usage of timers, compressors and QTimer::singleShot()
+
+    We should be carefult about allocation of timers in Krita. The number of precision
+    timers is limited on some platforms (e.g. Windows). When Qt runs out of precision
+    timers it silently degrades all newly created timers into non-precision ones. That
+    can affect timers that are used for the canvas framerate stabilization and cause
+    tearing and FPS drops.
+
+    Rules of thumb:
+
+    1) When using QTimer::singleShot(), either use **null** timeout to just push the
+       event through the queue, or pass Qt::CoarseTimer as the second argument:
+
+       QTimer::singleShot(0, ...);
+
+       or
+
+       QTimer::singleShot(<some non-zero value>, Qt::CoarseTimer, ...);
+
+       If you really-really-really need a precise single-shot event, then pass
+       Qt::PreciseTimer explicitly.
+
+       The problem is that QTimer::singleShot() creates a precise timer by default,
+       which can consume all the available precise timers in the system for the app.
+
+    2) When creating timers or using KisSignalCompressor, set timeout to values
+       strictly higher than 20 ms (unless you really know what you are doing). Value
+       `25 ms` is a good default to compress updates for the lightweight widgets,
+        and value `100 ms` is a good default to compress updates for heavy widgets.
+
+       The problem is that Qt internally converts all the timers into precise
+       ones, if their timeout is less or equal 20. Which can also consume all
+       the available precise timers.
+
 With Krita now supporting Python scripting, we need guidelines for these as well.
 These guidelines are preliminary and may be further refined in the future.
 
diff --git a/libs/flake/KoSelection_p.h b/libs/flake/KoSelection_p.h
index 4c04d9163ed..9f120a47c54 100644
--- a/libs/flake/KoSelection_p.h
+++ b/libs/flake/KoSelection_p.h
@@ -18,12 +18,12 @@ public:
     explicit Private()
         : QSharedData()
         , activeLayer(0)
-        , selectionChangedCompressor(new KisThreadSafeSignalCompressor(1, KisSignalCompressor::FIRST_INACTIVE))
+        , selectionChangedCompressor(new KisThreadSafeSignalCompressor(100, KisSignalCompressor::FIRST_INACTIVE))
     {}
     explicit Private(const Private &)
         : QSharedData()
         , activeLayer(0)
-        , selectionChangedCompressor(new KisThreadSafeSignalCompressor(1, KisSignalCompressor::FIRST_INACTIVE))
+        , selectionChangedCompressor(new KisThreadSafeSignalCompressor(100, KisSignalCompressor::FIRST_INACTIVE))
     {
     }
 
diff --git a/libs/ui/KisMainWindow.cpp b/libs/ui/KisMainWindow.cpp
index 3ed8dd66da5..a1479cb132a 100644
--- a/libs/ui/KisMainWindow.cpp
+++ b/libs/ui/KisMainWindow.cpp
@@ -602,7 +602,7 @@ KisMainWindow::KisMainWindow(QUuid uuid)
     d->viewManager->updateGUI();
     d->viewManager->updateIcons();
 
-    QTimer::singleShot(1000, this, SLOT(checkSanity()));
+    QTimer::singleShot(1000, Qt::CoarseTimer, this, SLOT(checkSanity()));
 
     {
         using namespace std::placeholders; // For _1 placeholder
diff --git a/libs/ui/kis_animation_cache_populator.cpp b/libs/ui/kis_animation_cache_populator.cpp
index e8546dd42dc..8ada3865f3a 100644
--- a/libs/ui/kis_animation_cache_populator.cpp
+++ b/libs/ui/kis_animation_cache_populator.cpp
@@ -396,5 +396,5 @@ void KisAnimationCachePopulator::slotConfigChanged()
 {
     KisConfig cfg(true);
     m_d->calculateAnimationCacheInBackground = cfg.calculateAnimationCacheInBackground();
-    QTimer::singleShot(1000, this, SLOT(slotRequestRegeneration()));
+    QTimer::singleShot(1000, Qt::CoarseTimer, this, SLOT(slotRequestRegeneration()));
 }
diff --git a/libs/ui/kis_safe_document_loader.cpp b/libs/ui/kis_safe_document_loader.cpp
index b437894792f..88088f8af58 100644
--- a/libs/ui/kis_safe_document_loader.cpp
+++ b/libs/ui/kis_safe_document_loader.cpp
@@ -307,7 +307,7 @@ void KisSafeDocumentLoader::fileChangedCompressed(bool sync)
 
 
     if (!sync) {
-        QTimer::singleShot(100, this, SLOT(delayedLoadStart()));
+        QTimer::singleShot(100, Qt::CoarseTimer, this, SLOT(delayedLoadStart()));
     } else {
         QApplication::processEvents();
         delayedLoadStart();
diff --git a/libs/ui/tool/kis_tool_select_base.h b/libs/ui/tool/kis_tool_select_base.h
index 70d33d5db96..b21733a124c 100644
--- a/libs/ui/tool/kis_tool_select_base.h
+++ b/libs/ui/tool/kis_tool_select_base.h
@@ -516,7 +516,7 @@ public:
 
     void updateCursorDelayed() {
         setAlternateSelectionAction(KisSelectionModifierMapper::map(m_currentModifiers));
-        QTimer::singleShot(100,
+        QTimer::singleShot(100, Qt::CoarseTimer,
             this,
             [this]()
             {
diff --git a/libs/widgets/KisHsvColorSlider.cpp b/libs/widgets/KisHsvColorSlider.cpp
index b1fe683183f..2ff10567d96 100644
--- a/libs/widgets/KisHsvColorSlider.cpp
+++ b/libs/widgets/KisHsvColorSlider.cpp
@@ -18,6 +18,7 @@
 #include <memory>
 
 #include "KoColorDisplayRendererInterface.h"
+#include <kis_signal_compressor.h>
 
 namespace {
 
@@ -192,6 +193,7 @@ struct Q_DECL_HIDDEN KisHsvColorSlider::Private
         , pixmap()
         , upToDate(false)
         , displayRenderer(nullptr)
+        , updateCompressor(100, KisSignalCompressor::FIRST_ACTIVE)
         , circularHue(false)
         , mixMode(KisHsvColorSlider::MIX_MODE::HSV)
     {
@@ -205,6 +207,7 @@ struct Q_DECL_HIDDEN KisHsvColorSlider::Private
     QPixmap pixmap;
     bool upToDate;
     QPointer<KoColorDisplayRendererInterface> displayRenderer;
+    KisSignalCompressor updateCompressor;
 
     // If the min and max color has the same hue, should the widget display the entire gamut of hues.
     bool circularHue;
@@ -223,6 +226,7 @@ KisHsvColorSlider::KisHsvColorSlider(Qt::Orientation orientation, QWidget *paren
     setMaximum(255);
     d->displayRenderer = displayRenderer;
     connect(d->displayRenderer, SIGNAL(displayConfigurationChanged()), SLOT(update()), Qt::UniqueConnection);
+    connect(&d->updateCompressor, SIGNAL(timeout()), SLOT(update()), Qt::UniqueConnection);
 }
 
 KisHsvColorSlider::~KisHsvColorSlider()
@@ -236,7 +240,7 @@ void KisHsvColorSlider::setColors(const KoColor min, const KoColor max)
     d->minKoColor = min;
     d->maxKoColor = max;
     d->upToDate = false;
-    QTimer::singleShot(1, this, SLOT(update()));
+    d->updateCompressor.start();
 }
 
 void KisHsvColorSlider::setColors(const QColor min, const QColor max)
@@ -245,7 +249,7 @@ void KisHsvColorSlider::setColors(const QColor min, const QColor max)
     d->minKoColor.fromQColor(min);
     d->maxKoColor.fromQColor(max);
     d->upToDate = false;
-    QTimer::singleShot(1, this, SLOT(update()));
+    d->updateCompressor.start();
 }
 
 void KisHsvColorSlider::setColors(qreal minH, qreal minS, qreal minV, qreal maxH, qreal maxS, qreal maxV)
@@ -260,19 +264,19 @@ void KisHsvColorSlider::setColors(qreal minH, qreal minS, qreal minV, qreal maxH
     d->maxKoColor.fromQColor(maxQ);
     d->upToDate = false;
 
-    QTimer::singleShot(1, this, SLOT(update()));
+    d->updateCompressor.start();
 }
 
 void KisHsvColorSlider::setCircularHue(bool value) {
     d->circularHue = value;
     d->upToDate = false;
-    QTimer::singleShot(1, this, SLOT(update()));
+    d->updateCompressor.start();
 }
 
 void KisHsvColorSlider::setMixMode(MIX_MODE mode) {
     d->mixMode = mode;
     d->upToDate = false;
-    QTimer::singleShot(1, this, SLOT(update()));
+    d->updateCompressor.start();
 }
 
 void KisHsvColorSlider::drawContents(QPainter *painter)
diff --git a/libs/widgets/KoColorSlider.cpp b/libs/widgets/KoColorSlider.cpp
index bbd97677d19..fea52cebc21 100644
--- a/libs/widgets/KoColorSlider.cpp
+++ b/libs/widgets/KoColorSlider.cpp
@@ -14,17 +14,25 @@
 #include <QTimer>
 #include <QStyleOption>
 #include <QPointer>
+#include <kis_signal_compressor.h>
 
 #define ARROWSIZE 8
 
 struct Q_DECL_HIDDEN KoColorSlider::Private
 {
-    Private() : upToDate(false), displayRenderer(0) {}
+    Private()
+        : upToDate(false)
+        , displayRenderer(0)
+        , updateCompressor(100, KisSignalCompressor::FIRST_ACTIVE)
+    {
+    }
+
     KoColor minColor;
     KoColor maxColor;
     QPixmap pixmap;
     bool upToDate;
     QPointer<KoColorDisplayRendererInterface> displayRenderer;
+    KisSignalCompressor updateCompressor;
 };
 
 KoColorSlider::KoColorSlider(QWidget* parent, KoColorDisplayRendererInterface *displayRenderer)
@@ -34,6 +42,7 @@ KoColorSlider::KoColorSlider(QWidget* parent, KoColorDisplayRendererInterface *d
     setMaximum(255);
     d->displayRenderer = displayRenderer;
     connect(d->displayRenderer, SIGNAL(displayConfigurationChanged()), SLOT(update()), Qt::UniqueConnection);
+    connect(&d->updateCompressor, SIGNAL(timeout()), SLOT(update()), Qt::UniqueConnection);
 }
 
 KoColorSlider::KoColorSlider(Qt::Orientation o, QWidget *parent, KoColorDisplayRendererInterface *displayRenderer)
@@ -42,6 +51,7 @@ KoColorSlider::KoColorSlider(Qt::Orientation o, QWidget *parent, KoColorDisplayR
     setMaximum(255);
     d->displayRenderer = displayRenderer;
     connect(d->displayRenderer, SIGNAL(displayConfigurationChanged()), SLOT(update()), Qt::UniqueConnection);
+    connect(&d->updateCompressor, SIGNAL(timeout()), SLOT(update()), Qt::UniqueConnection);
 }
 
 KoColorSlider::~KoColorSlider()
@@ -54,7 +64,7 @@ void KoColorSlider::setColors(const KoColor& mincolor, const KoColor& maxcolor)
     d->minColor = mincolor;
     d->maxColor = maxcolor;
     d->upToDate = false;
-    QTimer::singleShot(1, this, SLOT(update()));
+    d->updateCompressor.start();
 }
 
 void KoColorSlider::drawContents( QPainter *painter )
diff --git a/libs/widgets/KoDialog.cpp b/libs/widgets/KoDialog.cpp
index ad0ba1c46c7..85eb0cd499b 100644
--- a/libs/widgets/KoDialog.cpp
+++ b/libs/widgets/KoDialog.cpp
@@ -447,7 +447,7 @@ void KoDialog::keyPressEvent(QKeyEvent *event)
 void KoDialog::showEvent(QShowEvent *e)
 {
     QDialog::showEvent(e);
-    QTimer::singleShot(5, [&]() {
+    QTimer::singleShot(5, Qt::CoarseTimer, [&]() {
         adjustPosition(parentWidget());
     });
 }
diff --git a/plugins/dockers/advancedcolorselector/kis_color_selector.cpp b/plugins/dockers/advancedcolorselector/kis_color_selector.cpp
index 6fe20e018ec..38f6ebdb21f 100644
--- a/plugins/dockers/advancedcolorselector/kis_color_selector.cpp
+++ b/plugins/dockers/advancedcolorselector/kis_color_selector.cpp
@@ -407,7 +407,7 @@ void KisColorSelector::init()
 
     // a tablet can send many more signals, than a mouse
     // this causes many repaints, if updating after every signal.
-    m_signalCompressor = new KisSignalCompressor(20, KisSignalCompressor::FIRST_INACTIVE, this);
+    m_signalCompressor = new KisSignalCompressor(25, KisSignalCompressor::FIRST_INACTIVE, this);
     connect(m_signalCompressor, SIGNAL(timeout()), SLOT(update()));
 
     setMinimumSize(40, 40);
diff --git a/plugins/dockers/advancedcolorselector/kis_color_selector_base.cpp b/plugins/dockers/advancedcolorselector/kis_color_selector_base.cpp
index 9f0433c0117..f8090fae168 100644
--- a/plugins/dockers/advancedcolorselector/kis_color_selector_base.cpp
+++ b/plugins/dockers/advancedcolorselector/kis_color_selector_base.cpp
@@ -145,7 +145,7 @@ KisColorSelectorBase::KisColorSelectorBase(QWidget *parent) :
 
     using namespace std::placeholders; // For _1 placeholder
     auto function = std::bind(&KisColorSelectorBase::slotUpdateColorAndPreview, this, _1);
-    m_updateColorCompressor.reset(new ColorCompressorType(20 /* ms */, function));
+    m_updateColorCompressor.reset(new ColorCompressorType(25 /* ms */, function));
 }
 
 KisColorSelectorBase::~KisColorSelectorBase()
diff --git a/plugins/dockers/animation/KisAnimTimelineFramesView.cpp b/plugins/dockers/animation/KisAnimTimelineFramesView.cpp
index be3f113507f..1c2c11e93d5 100644
--- a/plugins/dockers/animation/KisAnimTimelineFramesView.cpp
+++ b/plugins/dockers/animation/KisAnimTimelineFramesView.cpp
@@ -837,7 +837,7 @@ void KisAnimTimelineFramesView::slotEnsureRowVisible(int row)
     // Delay's UI scrolling by 1/60 of a second to compensate for
     // inconsistent dummy indexing caused by a brief period where
     // two unpinned dummies exist on the timeline simultaneously.
-    QTimer::singleShot(16, this, [this, index](){
+    QTimer::singleShot(16, Qt::PreciseTimer, this, [this, index](){
         scrollTo(index);
     });
 }
diff --git a/plugins/dockers/artisticcolorselector/kis_color_selector.cpp b/plugins/dockers/artisticcolorselector/kis_color_selector.cpp
index 2f220b92ef4..216787ada1a 100644
--- a/plugins/dockers/artisticcolorselector/kis_color_selector.cpp
+++ b/plugins/dockers/artisticcolorselector/kis_color_selector.cpp
@@ -54,7 +54,7 @@ KisColorSelector::KisColorSelector(QWidget* parent, KisColor::Type type)
 
     using namespace std::placeholders; // For _1 placeholder
     auto function = std::bind(&KisColorSelector::slotUpdateColorAndPreview, this, _1);
-    m_updateColorCompressor.reset(new ColorCompressorType(20 /* ms */, function));
+    m_updateColorCompressor.reset(new ColorCompressorType(25 /* ms */, function));
 }
 
 void KisColorSelector::setColorSpace(KisColor::Type type)
diff --git a/plugins/dockers/smallcolorselector/kis_small_color_widget.cc b/plugins/dockers/smallcolorselector/kis_small_color_widget.cc
index f6e48a06497..f30f32d36b0 100644
--- a/plugins/dockers/smallcolorselector/kis_small_color_widget.cc
+++ b/plugins/dockers/smallcolorselector/kis_small_color_widget.cc
@@ -96,7 +96,7 @@ KisSmallColorWidget::KisSmallColorWidget(QWidget* parent)
     d->saturation = 0;
     d->updateAllowed = true;
 
-    d->repaintCompressor = new KisSignalCompressor(20, KisSignalCompressor::FIRST_ACTIVE, this);
+    d->repaintCompressor = new KisSignalCompressor(25, KisSignalCompressor::FIRST_ACTIVE, this);
     connect(d->repaintCompressor, SIGNAL(timeout()), SLOT(update()));
 
     d->resizeUpdateCompressor = new KisSignalCompressor(200, KisSignalCompressor::FIRST_ACTIVE, this);
@@ -105,7 +105,7 @@ KisSmallColorWidget::KisSmallColorWidget(QWidget* parent)
     d->valueSliderUpdateCompressor = new KisSignalCompressor(100, KisSignalCompressor::FIRST_ACTIVE, this);
     connect(d->valueSliderUpdateCompressor, SIGNAL(timeout()), SLOT(updateSVPalette()));
 
-    d->colorChangedSignalCompressor = new KisSignalCompressor(20, KisSignalCompressor::FIRST_ACTIVE, this);
+    d->colorChangedSignalCompressor = new KisSignalCompressor(25, KisSignalCompressor::FIRST_ACTIVE, this);
     connect(d->colorChangedSignalCompressor, SIGNAL(timeout()), SLOT(slotTellColorChanged()));
 
     {
diff --git a/plugins/dockers/widegamutcolorselector/WGSelectorConfigGrid.cpp b/plugins/dockers/widegamutcolorselector/WGSelectorConfigGrid.cpp
index 9a108e8e7a3..9a380dbdfc4 100644
--- a/plugins/dockers/widegamutcolorselector/WGSelectorConfigGrid.cpp
+++ b/plugins/dockers/widegamutcolorselector/WGSelectorConfigGrid.cpp
@@ -183,7 +183,7 @@ bool WGSelectorConfigGrid::event(QEvent *event)
     if (event->type() == QEvent::PaletteChange) {
         // For some reason, Qt doesn't like if we recreate the icons from this
         // even handler and randomly crashes somewhere after this function returns
-        QTimer::singleShot(10, this, &WGSelectorConfigGrid::updateIcons);
+        QTimer::singleShot(0, this, &WGSelectorConfigGrid::updateIcons);
         event->accept();
         handled = true;
     }
diff --git a/plugins/python/comics_project_management_tools/comics_project_manager_docker.py b/plugins/python/comics_project_management_tools/comics_project_manager_docker.py
index 970eaf0f1cc..990b400e80b 100644
--- a/plugins/python/comics_project_management_tools/comics_project_manager_docker.py
+++ b/plugins/python/comics_project_management_tools/comics_project_manager_docker.py
@@ -855,8 +855,7 @@ class comics_project_manager_docker(DockWidget):
         #   of `slot_check_for_page_update` would not know which files to update now.
         # https://bugs.kde.org/show_bug.cgi?id=426701
         self.updateurls.append(url)
-        QTimer.singleShot(200, Qt.TimerType.PreciseTimer, self.slot_check_for_page_update)
-         
+        QTimer.singleShot(200, Qt.TimerType.CoarseTimer, self.slot_check_for_page_update)
 
     def slot_check_for_page_update(self):
         url = self.updateurls.pop(0)



More information about the kimageshop mailing list