Message queue problem with repaint throttling


#1

I already postet this in this thread

https://forum.juce.com/t/on-latest-tip-gui-on-osx-is-much-laggier-than-on-windows-w-video/18080/15

but because it think this is an entirely new issue, i created this new thread:

The fix which was introduced to throttling the redraw in Mac OS is causing new issues, for other plugins which are running in the same host at the same time.

My assumption is the triggerAsyncUpdate inside the handleAsyncUpdate can result in a massive message ping pong (this does not happen always, but sometimes), which can block the whole message queue for other plugins in the host
(why - the answer does only know apple)

Alternative Solution:
So why not use a timer which waits until the minimum is reached, instead of massively posting messages trigger/handle/trigger/handle into the queue
(It also looks repaint is smoother with this technique)

And here is the code

  void handleAsyncUpdate() override
    {
       #if JucePlugin_Build_AAX || JucePlugin_Build_RTAS || JucePlugin_Build_AUv3 || JucePlugin_Build_AU || JucePlugin_Build_VST3 || JucePlugin_Build_VST
        const bool shouldThrottle = true;
       #else
        const bool shouldThrottle = areAnyWindowsInLiveResize();
       #endif

        // When windows are being resized, artificially throttling high-frequency repaints helps
        // to stop the event queue getting clogged, and keeps everything working smoothly.
        // For some reason Logic also needs this throttling to recored parameter events correctly.
        

        // already a timer running -> stop
        if (isTimerRunning()) return;
        
        int64 msSinceLastRepaint= Time::getCurrentTime().toMilliseconds() - lastRepaintTime.toMilliseconds();
       
        static int minimumRepaintInterval= 1000 / 30; // 30fps
        
        if (shouldThrottle
            && msSinceLastRepaint < minimumRepaintInterval );
        {
            startTimer(minimumRepaintInterval-msSinceLastRepaint);
            return;
        }

        setNeedsDisplayRectangles();
    }
    
    void timerCallback() override
    {
        setNeedsDisplayRectangles();
        stopTimer();
    };
    
    void setNeedsDisplayRectangles()
    {
        for (const Rectangle<float>* i = deferredRepaints.begin(), *e = deferredRepaints.end(); i != e; ++i)
            [view setNeedsDisplayInRect: makeNSRect (*i)];
        
        lastRepaintTime = Time::getCurrentTime();
        deferredRepaints.clear();
    };

#2

Maybe the AsyncUpdater could be also removed completely


#3

Hi chkn
I’m using this from yesterday, seems to works fine.


#4

Cool, works here great too!

But i want to make sure what the juce team says about this?

Further explanation why the current behavior is bad:
If a second repaint() call comes in a time interval below 33ms, maybe after 1ms, the rest of the time (32ms), the plugin will constantly post triggerAsyncUpdate() messages into the queue

This causes real world issues (bothers other plugins messaging), its a problem!

Any other plugins which build with the current juce, can harm other plugins messaging, its urgent and needs to be fixed soon.


#5

We’re also currently facing big problems with this behaviour in plugins running on macOS, didn’t discover until recently that it was actually related to the message thread event handling and repaint throttling.

I will also do some tests with the above code snippet. Seems like a reasonable change.


#6

bump, this is an acute problem, and should be resolved as soon as possible.


#7

I remember we tried this with a timer before, but there were some race conditions where the last repaint message would sometimes get lost. I’ll check if I can still wrap my head around why we didn’t go for the Timer solution which we originally intended.


#8

Okay thanks for replying to this, the current solution is definitely a big problem. If you have a plugin which redraws faster than the 30fps, other plugins may receive no messages anymore until the redraw is lower than 30fps.

But this happens not all the time, but often enough. It seems that macOs (is this the new spelling) somehow prioritize the message queue differently ( no FIFO behavior )

This must be fixed, also on master, because all vendors who now build plugins with this current behavior will create potential issues on other plugins.


#9

I can’t see any race-condition in my solution where the last repaint message might get lost. If you find some please let me know. Hopefully this will fixed soon (also on master, because of the non-cooperative handling regarding the message thread )


#10

Just for reference - this is why we put in that commit in the first place:


I need to make sure that your fix does not break this again. The problem is, I think this only happened on El Capitan. I can’t reproduce it on Sierra even when i completely remove the async updater thing (and redraw directly).


How to do smooth OpenGL in a Plugin?
#11

Yes, i know, it looks like redraws which will be posted often will somehow prioritized so that other messages will be slowed down, but in the same way you reduce redraws, by posting new messages, these are again become prioritized, which again slows down other message handling. So the key to success is, not posting more messages (or posting permanent messages as a pseudo timer), instead use a real timer thread.

PS: I wouldn’t remove repaint throttling at all, because i think 98% of all audio people still use El Captian), and we have to be sure that the initial issue is really removed with Sierra


#12

Thanks chkn. I’ve pushed your fix to develop. Please test!


#13

Thanks!

Yes, i will do that


#14

I just wondering, if its better to measure the time with Time::getMillisecondCounter(), because its the same how the Timer-Class it does, to make it more exact?

Here is the diff

diff --git a/modules/juce_gui_basics/native/juce_mac_NSViewComponentPeer.mm b/modules/juce_gui_basics/native/juce_mac_NSViewComponentPeer.mm
index 8281315..04eee0d 100644
--- a/modules/juce_gui_basics/native/juce_mac_NSViewComponentPeer.mm
+++ b/modules/juce_gui_basics/native/juce_mac_NSViewComponentPeer.mm
@@ -83,6 +83,7 @@ static NSRect flippedScreenRect (NSRect r) noexcept
           textWasInserted (false),
           isStretchingTop (false), isStretchingLeft (false),
           isStretchingBottom (false), isStretchingRight (false),
+          lastRepaintTime (Time::getMillisecondCounter()),^M
           notificationCenter (nil)
     {
         appFocusChangeCallback = appFocusChanged;
@@ -907,8 +908,11 @@ void repaint (const Rectangle<int>& area) override
 
         // already a timer running -> stop
         if (isTimerRunning()) return;
-
-        int64 msSinceLastRepaint = Time::getCurrentTime().toMilliseconds() - lastRepaintTime.toMilliseconds();
+        ^M
+        const uint32 now = Time::getMillisecondCounter();^M
+        const int msSinceLastRepaint = (int) (now >= lastRepaintTime ? (now - lastRepaintTime)^M
+                                   : (std::numeric_limits<uint32>::max() - (lastRepaintTime - now)));^M
+        ^M
         static int minimumRepaintInterval = 1000 / 30; // 30fps
 
         // When windows are being resized, artificially throttling high-frequency repaints helps
@@ -917,9 +921,9 @@ void repaint (const Rectangle<int>& area) override
         if (shouldThrottle
             && msSinceLastRepaint < minimumRepaintInterval)
         {
-            startTimer (static_cast<int> (static_cast<int64> (minimumRepaintInterval) - msSinceLastRepaint));
+            startTimer (minimumRepaintInterval - msSinceLastRepaint);^M
             return;
-        }
+        };^M
 
         setNeedsDisplayRectangles();
     }
@@ -935,7 +939,8 @@ void setNeedsDisplayRectangles()
         for (const Rectangle<float>* i = deferredRepaints.begin(), *e = deferredRepaints.end(); i != e; ++i)
             [view setNeedsDisplayInRect: makeNSRect (*i)];
 
-        lastRepaintTime = Time::getCurrentTime();
+        lastRepaintTime = Time::getMillisecondCounter();^M
+       ^M
         deferredRepaints.clear();
     };
 
@@ -1364,7 +1369,7 @@ void textInputRequired (Point<int>, TextInputTarget&) override {}
     NSNotificationCenter* notificationCenter;
 
     RectangleList<float> deferredRepaints;
-    Time lastRepaintTime;
+    uint32  lastRepaintTime;^M
 
     static ModifierKeys currentModifiers;
     static ComponentPeer* currentlyFocusedPeer;

#15

hi chkn,
Today I had issues with your first mod (not the last patch). repaint() calls from my component stopped working. I do not know why, since it was working perfectly.
For a moment I thought it was something I changed in my project, then I have returned to an old commit where everything worked perfect, but the problem persisted. It has been solved by returning to the original version of NSViewComponentPeer.

It is very weird, since all these days it was working perfectly.

EDIT: Now I’ve applied your last patch and it works again.


#16

@chkn: OK I’ve changed the workaround to use Time::getMillisecondCounter.

@xeneize: Does this also happen on the latest develop branch? I hope not!


#17

Thanks fabian!

My first version, still uses the AsyncUpdater (which was always triggered by any repaint), while the patch in the develop-branch (and the latest version) directly posts the rectangles when needed (setNeedsDisplayInRect) or uses the timer, maybe thats the difference, why the current version works better.

I recommend to use the latest patch from the develop branch, and if still something going on, to debug.

BTW: The reason why i preferred Time::getMillisecondCounter was, I also measure repaints which are still faster than 30fps, i guess this is something how the Timer works (i double-checked the whole timer code, maybe Time::getMillisecondCounter isn’t just precise enough), but otherwise it effectively reduces the amount of repaints.


#18

@fabian
is this right? in your patch

(std::numeric_limits<uint32>::max() - lastRepaintTime) + now);

should it be

(std::numeric_limits<uint32>::max() - (lastRepaintTime - now)));


#19

I think that’s the same right? I always like to write it the first way: A + B where A is the amount of elements until we wrap around and B is anything left over.


#20

Ah yes, you are right! Thats more clear!