Lower GUI repaint priority during CPU-heavy calculations

My Audio Plugin has an “analysis mode”, in which the audio fed into it is analysed in different ways. This is very CPU-intensive.

When profiling, I’ve noticed that the larger the GUI window is, the more CPU is used by the frequent repaints. I’ve optimized the GUI best I could, but the Plugin is still only able to operate in real-time with a small GUI window size.

Is there a way to limit the frequency of actual repaints during analysis mode (i.e. capping the GUI at 10fps), or at least lower the Graphics thread’s priority so much that the Audio IO thread is able to keep up with its computations?

Note: I’m testing my code in a Standalone App with a DocumentWindow before building it into a Plugin, if that makes any difference.

Thanks in advance,

CrushedPixel

How exactly is your code implemented? It sounds like you are making the audio thread wait for your GUI thread to do its work? That’s not going to work. At most the audio thread should just copy the audio data into the GUI objects, the audio thread shouldn’t be waiting or be blocked on things like drawing and GUI update operations.

My GUI code is non-blocking. The issue is that the GUI thread takes as much CPU time as the IO thread.

Here’s the relative CPU usage of each thread (IO Thread, GUI Thread, Message Thread)

Why do you think you are not yourself in control of the graphics update speed? Is OpenGL or something like that involved? With normal Components, they are drawn some time after repaint() has been called. (Which you would be calling from a timer callback or when something has changed so that a repaint is needed.)

Is OpenGL or something like that involved?

I’m rendering my GUI in an OpenGLContext, if that matters.

With normal Components, they are drawn some time after repaint() has been called […] which would be called when something has changed so that a repaint is needed

I do indeed use juce::ChangeBroadcasters inside my Audio Thread, and my GUI components implement juce::ChangeListener and repaint whenever they receive a change message. This happens very frequently during analysis (easily 100+ times a second).

But did I understand correctly your audio processing gets slower/non-working too when the GUI size is increased? Then something in the audio processing has to be doing more work or waiting longer for something when the GUI size is increased…Or is this only about the GUI not being able to keep up with the updates when the GUI size is large?

The audio processing does not get slower, as far as I understand it, but it gets less time to do its processing, because the GUI repaints use up more CPU time.

Audio threads should be automatically running on a higher priority than the GUI. What system is this about? Is it a single or multicore CPU system?

I’m on a MacBook Pro from Late 2013, which has a Dual-Core i7 processor, running macOS 10.14 (Mojave).

Perhaps one approach to limit GUI repaints would be setting a dirty flag to true in the ChangeListener callback instead of calling repaint(), and having an internal timer with a limited frequency, which calls repaint() if dirty?

Is it actually causing glitches in your audio? If not, you shouldn’t need to worry about the GUI thread blocking the audio thread unless you’ve got a lot of communication between the two.

Yes it does. As I said, the IO thread is unable to perform my DSP in real-time with the large GUI size.

Maybe the ChangeBroadcaster should not be used from the audio thread? Maybe sending a notification from that gets stuck if the GUI thread is doing a lot of work. To cause GUI updates to happen, a simple (atomic) boolean flag variable should be enough anyway. (The GUI would have a timer that checks in its callback if the flag is dirty etc…)

Maybe the ChangeBroadcaster should not be used from the audio thread?

I didn’t see any hints about that in the docs. I really liked the idea of only repainting when the ChangeBroadcaster emitted a change event instead of having a timer for the GUI. Perhaps one of the devs (@jules?) can comment on the usage of ChangeBroadcaster inside the audio thread?

The implementation using atomic boolean flags has the downside that there can only be a single “listener” reading from the dirty flag. Nevertheless, it’s probably suitable for my code design, and I’ll have a look at it, unless there’s another way to limit GUI repaints.

Yes, the documentation for void ChangeBroadcaster::sendChangeMessage() implies the method will always return immediately but maybe that’s just for a best case scenario, when there isn’t a lot of stuff already happening in the GUI thread…? Anyway, you should really profile the code to see where the time is actually spent.

I think the fear with ChangeBroadcaster::sendChangeMessage() isn’t that it will block, but that it may allocate a message. Whether or not it allocates probably depends on the platform and how full the queue is etc.

I think AsyncUpdater is the thing you want to use. A ChangeListener will react to every broadcast event, even piling up repaint() calls.
The AsyncUpdater has this kind of dirty flag and handles once, until a new triggerAsyncUpdate() was called.

Neither ChangeBroadcaster or AsyncUpdater are safe to use from the audio thread, they both involve posting a message, which isn’t realtime-safe.

For analysis tasks like this, you need to be feeding data into some kind of lock-free fifo and emptying it on another thread or your gui thread, depending on exactly what processing you need to do.

3 Likes

Thanks for clarifying this, I wasn’t aware that the ChangeBroadcaster isn’t suitable for real-time processing.

Thank you everyone for their input, especially @Xenakios - I will probably do as you suggested, and implement a dirty flag as an atomic boolean.

@jules If ChangeBroadcaster and AsyncUpdater aren’t realtime-safe, is it realtime-safe to update a juce::Value from the audio thread, which calls juce::Value::Listeners? Does that work without posting a message?

No, a quote from the Value documentation:

Important note! The Value class is not thread-safe! If you’re accessing one from multiple threads, then you’ll need to use your own synchronisation around any code that accesses it.

I think something like this should be added to Changebroadcasters documentation. This comes up regularly.

2 Likes

I’ve also noticed on mac that the DSP usage meaurement goes up when continually rendering OpenGL. And if the drawing is more intense (like the window being larger) the problem is worse. Even though I’m not locking the audio thread it can even create audio dropouts.
This issue goes away if I setComponentPaintingEnabled to false.

There is no issue on Windows or Linux.

2 Likes