On latest tip, GUI on OSX is much Laggier than on Windows (w. Video)


#1

Hi!

After seeing This Discussion, I hoped updating to the latest tip would solve the issue also for me, but unfortunately it didn’t.

On windows my program runs great: https://www.youtube.com/watch?v=7w1aWXwkGEw
On OSX it is very sluggish: https://www.youtube.com/watch?v=cfF1fpoh0ZM

The Win machine runs Win 10.
The OSX machine is a recent mac mini of the better ones, (late 2014, 2.6GHz intel Core i5, 8GB ram), running El Capitan, 10.11.4.

As you can see in the videos the difference is huge - any tips on what I should look for?

Thank you!


#2

Maybe cache the drawing of the more complex components?

complexComponent.setBufferedToImage(true);

Not every component, but the topmost of a unit.

I’m not convinced it will solve your issues, but it’s worth a try.


The Event Queue: what do I need to know to manage it efficienly?
#3

Hi!

I’d be surprised if it changes anything, but it is worth a try regardless, as it is likely to speed up drawing also on Windows.

I am convinced it has to do with the platform because even on very low-end hardware, the software runs just fine on windows, but on a relatively powerful Mac Mini it is very laggy. Also, note I do not use OpenGL-drawing for the GUI. I tried it, but the issues were far too many for me to even begin trying to fix them…

Another test I could do is install windows on the Mac and run it there, to confirm my assumption that it is the platform and not the hardware spec that causes the lag.


#4

This has nothing to do with graphics performance!

What you’re seeing is some kind of event-queue bottleneck. Paint messages, update messages and timers all happen on the same queue, but will be prioritised slightly differently on different OSes. Looks like you’re just hammering the event queue with events to the point where some repaint messages are getting discarded - you need to look at the overall behaviour of your app, not how it’s rendering!


#5

Before I begin I just want to say that your machine is more than powerful enough to do what you need it to do. I develop far more power-hungry stuff on older hardware with less ram on OSX.

And, as a disclaimer, I’m not tracing through the source.

I spent the morning researching this as best I could. Specifically, I found this in the documentation:

Apple Docs: Cocoa: Optimizing Performance: Avoiding Synchronous Updates

If you are creating animated content, you should also be careful not to trigger visual updates more frequently than the screen refresh rate allows. Updating faster than the refresh rate results in your code drawing frames that are never seen by the user. In addition, updating faster than the refresh rate is not allowed in OS X v10.4 and later. If you try to update the screen faster than the refresh rate, the window server may block the offending thread until the next update cycle.

Now, I don’t think that this is specifically the issue. But it does demonstrate that the Windowing Server has the ability to block repaint messages that it deems to cause performance issues.

The windowing server in question is the Quartz Compositor which also handles the event queue (mouse, timers, etc…). When I started developing on OSX years ago, I noticed that I get FAR MORE mouse events than on Windows. It was enough of a problem that I started throwing events away. IIRC, dragging the mouse was the primary culprit (it’s been quite a while ago).

This video demonstrates the difference in Windowing performance on Mavericks and El Capitan: https://www.youtube.com/watch?v=LlOdeSeNLcs

I have fixed a couple bugs related to ‘upgrades’ in the Windowing system to make me think there might have been significant rewrites. During the rewrites they could have certainly prioritized user responsiveness over redraw.

Long story short: one day OSX will do all drawing/composition on GPU in a separate thread (probably on a Metal renderer). It was probably meant that El Capitan be that release but slipped.

What does this mean for you?
Well, I suspect that the Quartz upgrades includes the ability to throw away repaint requests. At least delay them for longer intervals while the mouse is dragging. It might also delay requests based on frequency instead of draw time. Julian has addressed this issue in the past with aggregation, but with high precision mouse drag events, it might still be affecting the frequency of repaints.

Or they might just throw away repaints altogether while mouse dragging. And force update at lower intervals. There’s nothing you can do if Quartz is straight-up throwing away repaints during drag operation.

Where to Go?
Use the Quartz Debugger and enable QuartzGL drawing. If this fixes your problem, it might tell you something. Remember to disable again to do any profiling.

Use Instruments to profile your App EXACTLY when you are scrolling. Pause the profiling, clear the data, restart profiling… Then scroll your app a bunch and pause the profiling again. See if there are any sections that seem to spend a lot of time in.

It’s harder to diagnose if OSX is choking your redraw. In fact, we can simply assume that it is and see if it’s

  1. Time based (too much drawing)
  2. Frequency Based (too many update requests)
  3. Fixed Function (OSX simply throwing away repaints during mouse drag)

If #1 is the problem, you should catch it in Instruments as a lot of time spent in drawing code. Also, caching should have some significant improvements. The Quartz Debugger speedometer would peak and then drop as Quartz starts throwing away repaints.

If it’s #2 or #3, then the Quartz Debugger speedometer should unexpectedly drop when scrolling. Like as soon as you start scrolling. There might be a small upward hitch if it’s frequency based, but if it’s fixed function then it would drop straight away.

If it’s #1, then you should be able to cache some of the more complex components. If it’s #2 or #3 then you might need to switch to an OpenGL renderer (JUCE OpenGL runs on a separate thread).

If the theory is completely wrong and Quartz isn’t throwing away repaints then the speedometer will peg out while scrolling your app. Be sure to run your tests with QuartzGL turned off.


#6

Hi, wow, I couldn’t possible have hoped for a more extensive post than this, you’ve given me a lot to work on!

You confirm what I have felt since I (recently) started developing for OSX: all the user friendlines of mac’s seems to be a the expense of developers :smiley:

Every PC I’ve run my program on, it’s worked great - on the mac, obviously not…

Anyway, thanks again, hopefully with your instructions I’ll figure it out!


#7

Without wanting to downplay jpoag’s helpful post, I do think my diagnosis above is a better place to start, and really don’t think this is a graphics performance issue…

I’m basing that mainly on the fact that your scrollbar repaints more often than the other content, which is something I’ve seen before in situations where there’s some event queue overload.


#8

I will look into that too, thank you!

It was a while since I looked at the messaging architecture, it is likely it contains misunderstandings of how I should use JUCE, which I didn’t go back to optimize because, well, it worked fine on PC’s… Now is a good chance to revisit and optimize, definitely.


#9

I don’t full agree with you here Jules, i mean if he is able to run his software smoothly on windows with older hardware it does seem he’s been doings things right isn’t he. The fact that his OSX version does not render as smoothly as expected can’t be fully blamed on him. The task of a cross platform library like Juce is to handle these kind of things for you. His Windows build did not give him any indication he was doing anything wrong.
The fact that OSX handles painting differently shouldn’t be of concern to us. It’s of course fair to except that the behaviour is slightly different but in this case (and i have seen similar behaviour with our own software) the difference is quite huge.


#10

There are a few things that we can’t magically get to behave exactly the same on all platforms!

One example of that is the algorithm that the OS uses to balance and prioritise its repaint events on the message thread - Windows and OSX do have slightly different characteristics in the way they do that. Usually that’ll make no difference whatsoever to almost all apps, but occasionally in an app that makes heavy use of events, you’ll find that it can push the event queue in a way that causes unexpected interactions with one platform or another. That’s what I’m guessing is happening here based on things I’ve seen before, but I could also be wrong.


#11

Hi!

Finally I’m able to dedicate some time to fix this. But I’m a bit lost as to where to start!

How can I check the size of the event-queue at any one moment, to see what it is that makes it too large?

Is it the MessageManager class I should be using to figure this out?

Thank you!


#12

I just updated to the JUCE latest tip, and the issue is gone!


#13

Has anyone investigated if the issue is the same in macOS Sierra? I.e. do we have to accept 30FPS-limited plugins also here?


#14

@jules
I just found another big issue and i suspect that the repaint throttling is the culprit (but Mac OS X also)
Sometimes, when a plugin (Juce 4.3) use heavy repainting above >30fps (and the repaint throttling is active) in other plugins (Juce 4.0 in this case) somehow messages are heavily delayed or will wait to arrive until the heavy repaint interval has stopped.
(This is a big problem, it looks like the new JUCE plugin blocks the whole host)

Maybe because the throttling itself post messages? Funnily it looks like that the repaint messages in the JUCE 4.0 still come through, but other kind of messages not (which makes sense, because in older Juce versions, the repaint is handled directly without using triggerAsyncUpdate() )

So long story short, the whole think should be rewritten to use less(!!) message posts
(Yes, i know the whole the triggerAsync mechanism was implemented because the lost rectangles)

Maybe it would be better if the repaint happens like the old way, directly in repaint() to call the OS, and a low frequency job checks if all dirty rectangles have been repainted properly


#15

@fabian @jules

It looks the triggerAsyncUpdate inside the handleAsyncUpdate can result in a massive message ping pong (this does not happen always, but sometimes)

This is a problem, it causes problems in other plugins in the same host

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.
        
        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();
    };

#16

this is a real world issue, and a problem for all newly developed juce plugins, so it should be fixed quickly


#17

@jules
Can you changes the current code of juce_mac_NSViewComponentPeer.mm which currently use
ifdef
in order to activate the throtling to the use of isStandaloneApp() function
It would allows people building juce in on single lib for both standalone and plugin
to use this tricks

Thanks !


#18

Yep, sounds like a good idea, will do that.