The AudioProcessorGraph is a bit slow nowadays

After I updated to Juce 6 a typical task that previously took virtually no time at all (<0,5s) now takes over a minute to complete.

The cause seems to be an update to the AudioProcessorGraph made some time ago, during Juce 5, don’t know exactly when because I don’t update that often.

Now, in Juce 6, this piece of code in the graph is executed for every change being made (adding a node, making a connection etc)

static void updateOnMessageThread (AsyncUpdater& updater)
{
    if (MessageManager::getInstance()->isThisTheMessageThread())
        updater.handleAsyncUpdate();
    else
        updater.triggerAsyncUpdate();
}

The call to handleAsyncUpdate() is a call to buildRenderingSequence(), which rebuilds the whole graph every time, effectively blocking the message thread.

A typical task in my app is to open a midi file whereupon a few basic plugins are attached to each track, say a score display, a sample player, volume/balance, a VU meter etc.

For a 16 track file this means some 64 plugins are added to the graph and a few hundred connections are being made, connecting inputs to outputs, times two if it’s stereo. All in all, typically a couple of
hundred consecutive calls are being made to graph.addNode() and graph.addConnection().

Each one of these calls triggers a call to buildRenderingSequence() which rebuilds the graph from scratch over and over again.

Previously the code, effectively, was

static void updateOnMessageThread (AsyncUpdater& updater)
{
  updater.triggerAsyncUpdate();
}

which concatenated all subsequent calls to buildRenderingSequence() to just one, giving an effective graph set up time of virtually zero instead of as of now, halfanother minute!

Ok, I admit it’s the debug build I’m talking about, but even in release mode it takes over 10 secs to open a file like anotheronebitesthedust.mid, and virtually all this time is spent in the AudioProcessorGraph doing repeated calls to buildRenderingSequence…

Could we please have an update to the graph that allows a non blocking operation of the graph, even if called from the message thread, like before?

2 Likes

Did anybody get this resolved? I have the exact same problem updating an old program to Juce 6.

There have been lots of improvements to the AudioProcessorGraph in JUCE 6, 7, and 8. Between 6 and 7 we added a change that improved the performance of building large graphs. Between 7 and 8 we added a new feature that allows automatic rebuilding of graphs to be disabled, which means that multiple mutating operations can be combined such that they only require a single graph rebuild, rather than one rebuild per operation.

Thank you reuk for the update and for oxxyyd showing the cause of the delay. I have not upgraded to Juce 8, but I did take a peek and saw that there is an option to update the old way. I commented out the change in Juce 6 and sure enough the graph code is fast again. Hopefully there will not be any issues with this change, until I upgrade to Juce 8.

Also, hopefully the next time a change like this is made the testers will test hostd that have a large number of connections and plugins.

Hi fireplayer.

I was a bit overwhelmed by the level of response to my post (absolutely nil!). And a bit flattered too, while it would mean I was the only hardcore user of the graph :blush:
With you now, we’re two!

Or else nobody tested before happily shipping their products to unsuspecting end users…

I think it took at least half a year before it was possible to rebuild the graph asynchronously again and I could remove my patch.

Anyway that’s better than the bug in the midi handling that inserts new notes per its own discretion. I don’t think that’s resolved even now… :roll_eyes:

Enough ranting now. :slightly_smiling_face:

You typically call the graph nowadays like

graph.addNode(std::move(msProc), {}, AudioProcessorGraph::UpdateKind::async);

instead of previously just

graph.addNode(std::move(msProc))

Have no idea why they choose to make UpdateKind::sync the default, thence breaking old code.

If you need to do even more stuff before rebuilding the graph you can use

UpdateKind::none

and later call

graph.rebuild()

manually (which isn’t neccessary for UpdateKind::async)

Anyway Good luck to you updating the old program. Juce have been a lot better in many places since 6.0

Has that been reported anywhere? Is that a bug in the AudioProcessorGraph specifically, or in other MIDI handling code (MidiFile, MidiInput etc.)? Does it require very specific circumstances to trigger?

Hi reuk, and thanks for your swift reaction!

It’s all in my first reply to cpr:s post from 2018

Either Jules didn’t really understand my point of not inserting note-offs in updateMatchedPairs() or he didn’t care. To his defence (if he ever would need one :slight_smile: ) one might put forth that the initial post was about a slightly different problem of updateMatchedPairs() and from rereading the topic now, I see it took a direction towards efficiency instead of fixing the main issue (improper changing of the the midi score by inserting unneccessary note-offs).

This subject have also been touched upon before like here

How to trigger? Just take a midi file with overlapping note-ons with same note number and channel

By the way, I just stumbled upon this topic in the Vital forum. Vital is build with Juce isn’t it?
Just a reminder that this bug pops up here and there in the real world… :pensive:

Vital won’t play overlapping MIDI notes, if the notes are the same - Bugs - Vital

Can someone explain why you would EVER want to use UpdateKind::sync? Why not always use async?

Is there some way to set it to use UpdateKind::async by default, other than modifying the JUCE code?

I have a plugin host that I’ve been working on for years, and the AudioProcessorGraph stuff hasn’t been touched in awhile. I just noticed this thread, I have many different calls to addNode, add/removeConnection() etc. that I think should all be changed to use UpdateKind:async for better performance, but there are many of them…

You actually want to use AudioProcessorGraph::UpdateKind::none until all your connections are made and then do a rebuild or make a final connection asynchronously.

Something like:

Declare the method:

void    updateSends (AudioProcessorGraph::UpdateKind kind = AudioProcessorGraph::UpdateKind::sync);

And in the implementation:

void AuxInputButton::updateSends (AudioProcessorGraph::UpdateKind kind)
{
     :
    
    const AudioProcessorGraph::UpdateKind localKind = AudioProcessorGraph::UpdateKind::none;

    for (auto* pTrack : m_TrackArray)
        {
        if (pTrack->isAuxTrack())
            pTrack->updateSends (localKind);
        }
    
    if (localKind != kind)
        m_pProcessor->getAudioProcessorGraph()->rebuild();
}

Rail

1 Like

I think this is for backwards compatibility. The original behaviour was to make all changes synchronously, and changing this behaviour might break user projects that expect the graph to function in a certain way.

Not currently, no. I’d be a bit wary of just switching all sync operations to async, because this will break any code that expects the modifications to be made immediately. As Rail mentioned, you can call rebuild() in these situations to force any pending operations to complete synchronously.

1 Like

Nope, as can be read in my initial post (scroll upwards) the original behaviour was asynchronical.
Consecutive updates was coalesced into a single rebuild of the graph.

This was changed around 2020 so every update (adding or changing a node etc) was followed by a complete rebuild.

Probably a lot of application silently got slower then when updating to a new juce version.
Which probably went unnoticed unless you were a hard core user and found the app or plugin to slow and possibly looked for another maker…

The new behaviour should of course have been made optional, which it wasn’t.

Now, the damaged is done and just as you say it would be unwise to swith back to the original asynchronous behaviour.

But I think i would be beneficial for both old and new code to make the UpdateKind parameter mandatory again (instead of defaulting to sync).

Then updaters of old code would be forced to make choice if they wanted a slow (sync) or fast (async) behaviour of graph updates.

And of course it would be the same case for any new code, you had to decide if you really needed a costly rebuild after every update.

1 Like

So I updated all of my calls to addNode, removeNode, addConnection, removeConnection etc. to pass UpdateKind:none, and then follow with a single call to rebuildGraph() at the appropriate time - significant update in performance and loading of large graphs. Thanks!

I guess I missed this when it was first introduced, but I’m glad to have that optimization now.

I just have my own set of Helper Functions - if you make a similar set you can make the default UpdateKind your own preference.

Over the years I’d made mods to improve performance of the graph… and this was the biggest issue - so I was glad to see it added to the JUCE code officially.

// -------------------------------------------------------------

bool canMakeMonoConnection (AudioProcessorGraph* graph, AudioProcessorGraph::Node::Ptr from, AudioProcessorGraph::Node::Ptr to);

bool addMonoConnection (AudioProcessorGraph* graph,
                        AudioProcessorGraph::Node::Ptr from,
                        AudioProcessorGraph::Node::Ptr to,
                        AudioProcessorGraph::UpdateKind kind = AudioProcessorGraph::UpdateKind::sync);

bool addMonoConnection (AudioProcessorGraph* graph,
                        AudioProcessorGraph::Node::Ptr from,
                        AudioProcessorGraph::Node::Ptr to,
                        int iFromChanIndex,
                        int iToChanIndex,
                        AudioProcessorGraph::UpdateKind kind = AudioProcessorGraph::UpdateKind::sync);

bool removeMonoConnection (AudioProcessorGraph* graph,
                           AudioProcessorGraph::Node::Ptr from,
                           AudioProcessorGraph::Node::Ptr to,
                           int iFromChanIndex,
                           int iToChanIndex,
                           AudioProcessorGraph::UpdateKind kind = AudioProcessorGraph::UpdateKind::sync);

bool canMakeStereoConnection (AudioProcessorGraph* graph, AudioProcessorGraph::Node::Ptr from, AudioProcessorGraph::Node::Ptr to);

bool addStereoConnection (AudioProcessorGraph* graph,
                          AudioProcessorGraph::Node::Ptr from,
                          AudioProcessorGraph::Node::Ptr to,
                          int iChanIndex1 = 0,
                          int iChanIndex2 = 1,
                          AudioProcessorGraph::UpdateKind kind = AudioProcessorGraph::UpdateKind::sync);

bool removeStereoConnection (AudioProcessorGraph* graph,
                             AudioProcessorGraph::Node::Ptr from,
                             AudioProcessorGraph::Node::Ptr to,
                             int iChanIndex1 = 0,
                             int iChanIndex2 = 1,
                             AudioProcessorGraph::UpdateKind kind = AudioProcessorGraph::UpdateKind::sync);

bool addMultiConnection (AudioProcessorGraph* graph,
                         AudioProcessorGraph::Node::Ptr from,
                         AudioProcessorGraph::Node::Ptr to,
                         int iNumberOfConnections,
                         AudioProcessorGraph::UpdateKind kind = AudioProcessorGraph::UpdateKind::sync);

bool removeMultiConnection (AudioProcessorGraph* graph,
                            AudioProcessorGraph::Node::Ptr from,
                            AudioProcessorGraph::Node::Ptr to,
                            int iNumberOfConnections,
                            AudioProcessorGraph::UpdateKind kind = AudioProcessorGraph::UpdateKind::sync);

bool addStereoConnectionToSingleChannel (AudioProcessorGraph* graph,
                                         AudioProcessorGraph::Node::Ptr from,
                                         AudioProcessorGraph::Node::Ptr to,
                                         int iChannel,
                                         AudioProcessorGraph::UpdateKind kind = AudioProcessorGraph::UpdateKind::sync);

bool addStereoConnectionToChannelPair (AudioProcessorGraph* graph,
                                       AudioProcessorGraph::Node::Ptr from,
                                       AudioProcessorGraph::Node::Ptr to,
                                       int iChannel,
                                       AudioProcessorGraph::UpdateKind kind = AudioProcessorGraph::UpdateKind::sync);

bool addMonoInputToStereoOutput (AudioProcessorGraph* graph,
                                 AudioProcessorGraph::Node::Ptr from,
                                 AudioProcessorGraph::Node::Ptr to,
                                 AudioProcessorGraph::UpdateKind kind = AudioProcessorGraph::UpdateKind::sync);

bool removeMonoInputToStereoOutput (AudioProcessorGraph* graph,
                                    AudioProcessorGraph::Node::Ptr from,
                                    AudioProcessorGraph::Node::Ptr to,
                                    AudioProcessorGraph::UpdateKind kind = AudioProcessorGraph::UpdateKind::sync);

bool removeStereoConnectionToSingleChannel (AudioProcessorGraph* graph,
                                            AudioProcessorGraph::Node::Ptr from,
                                            AudioProcessorGraph::Node::Ptr to,
                                            int iChannel,
                                            AudioProcessorGraph::UpdateKind kind = AudioProcessorGraph::UpdateKind::sync);

bool removeStereoConnectionToChannelPair (AudioProcessorGraph* graph,
                                          AudioProcessorGraph::Node::Ptr from,
                                          AudioProcessorGraph::Node::Ptr to,
                                          int iChannel,
                                          AudioProcessorGraph::UpdateKind kind = AudioProcessorGraph::UpdateKind::sync);

bool addOutputStereoConnectionToSingleChannel (AudioProcessorGraph* graph,
                                               AudioProcessorGraph::Node::Ptr from,
                                               AudioProcessorGraph::Node::Ptr to,
                                               int iChanIndex,
                                               AudioProcessorGraph::UpdateKind kind = AudioProcessorGraph::UpdateKind::sync);

bool removeOutputStereoConnectionToSingleChannel (AudioProcessorGraph* graph,
                                                  AudioProcessorGraph::Node::Ptr from,
                                                  AudioProcessorGraph::Node::Ptr to,
                                                  int iChanIndex,
                                                  AudioProcessorGraph::UpdateKind kind = AudioProcessorGraph::UpdateKind::sync);

//--------------------------------------------------------------------------

Rail

Of course I can, as I showed above. This post was about

  1. The graph was suddenly made slow around 2020 in JUCE 5.x, when the behaviour was changed from asynchronous to synchronous. Something that couldn’t be remedied then without patching the code.

  2. Some time thereafter it was again possible to use the graph as it was originally intended to function - without rebuilding the graph after every change in node topology.

  3. I acknowledged that fireplayer four years later faced the same problem as I had.

  4. I concluded that anyone who updates legacy code without take any closer look at the changed behaviour of the graph, will end up with a graph that have the potential to be very slow when changing the topology.

  5. I’m glad it suits your needs nowadays. Personally I can’t find a reason why anyone would want to rebuild the graph after each and every change in the topology, but obviously other can.

  6. So let’s conclude the graph is very functional and refined piece of code these days.