AudioProcessorValueTreeState - Scalability


#1

Hi all! I’m in the process of creating a research project that has a large number of (several thousand) automatable parameters.

This seems to completely thrash performance in the Dec rev (5.4.1) of JUCE - although JUCE 5.3.2 is fine, outside of long load times in the debug builds. I have not compared internal differences - yet. What I do know, is that in v5.4 lots of cycle time is spent in AudioProcessorValueTreeState::flushParameterValuesToValueTree(), which happens each time the timer callback is issued. It appears that this iterates a list of all parameters, maybe not in the most performant way.

I’ve considered duplicating the APVTS class and making some modifications, but before I do, I was wondering if anybody here might have some thoughts about how to approach this. (Or if there’s a way to improve performance without breaking the built-in model) I’ve also been examining to/fromXML to see what gains I might be able to make on the serialization front. Also, with large number of parameters, using state assignment operators is particularly slow. I could optionally revert to the APVTS version in 5.3.2. (which still has slow recall, but scaling doesn’t break it on the timer callbacks)

I understand our use case is atypical - figured somebody here might have ideas. Thanks!

-Matt


#2

Right now I believe the APVTS class checks all the parameters, sees if one has changed, then propagates the change, right?

One way you could do things is to push transactions (Parameter ID + new value) to a double-buffered FIFO and run a separate thread just to track the parameter updates. You’d have to handle the updates to your DSP before you push the transaction to the fifo however.


#3

you may consider not to use APVTS at all, you can simply add Parameters with addParameter()
you only have to serialise/deserialise the current state of all parameter in set/getStateInformation


#4

Thanks, these are both good options.

@Holy_City that could make a lot of sense for APVTS in general, although I’m not sure it’d make a big difference to those with a small set of params. The ValueTree gets copied around like a red-headed stepchild, so I’d probably just create a mechanism to use a reference instead. Actually, it looks like the problem I’m having is here: (juce_AudioProcessorValueTreeState.cpp:436)

		anyUpdated |= p->flushToTree(getChildValueTree(p->getParameter().paramID),
												   valuePropertyID,
												   undoManager);

If I replace it with the following:

anyUpdated |= state.isValid() ? p->flushToTree(state.getChild(p->getParameter().getParameterIndex()),
								     valuePropertyID,
								     undoManager) : false;

The process is no longer hanging. This assumes a 1:1 parity between parameter indices and the index in the tree, but in my case, they always match. Terrible hack, I know.

I assume this also breaks compatibility when expanding the number of parameters, or reordering them. Regardless, this might suggest that perhaps getChildValueTree() is doing some heavy lifting that maybe it should not?

Thoughts?


#5

You don’t need that to do, the ValueTree itself is just a slim wrapper around a ValueTree::SharedObject, so creating a copy is not expensive at all…

These are the only members:


#6

Hey @daniel, thanks for the reply! I understand that, architecturally, it isn’t particularly expensive.

Any idea why getChildValueTree() seems to crawl at a snail’s pace when the tree is large? I assumed it was the ‘light’ object copy happening at large volumes, but maybe that isn’t the case.

Might be that ValueTree::getChildWithProperty() is iterating the tree for each param, which is N^2 performance at worst. If that’s what’s slowing this down, ValueTree::children could possibly be stored by ID as a hashmap instead? Might need a ReferenceCountedMap?

-M


#7

That is already the case:

If you look up adapterTable, that is a std::map<String, std::unique_ptr<ParameterAdapter>> adapterTable; and the ParameterAdapter has stores the tree and a reference to RangedAudioParameter, so there shouldn’t be an iteration happening.

The ParameterAdapter is specifically listener to only one parameter, so there is a 1:1 connection either.

So I can’t see anything suspicious there, I’m afraid. That doesn’t mean, there is nothing to find…


#8

I’m on JUCE 5.3.1 or so looking to update to 5.4 later on… I noticed in my copy of the library that the method for flushing the parameters is:

bool AudioProcessorValueTreeState::flushParameterValuesToValueTree()
{
    ScopedLock lock (valueTreeChanging);

    auto anythingUpdated = false;

    for (auto* ap : processor.getParameters())
    {
        jassert (dynamic_cast<Parameter*> (ap) != nullptr);
        auto* p = static_cast<Parameter*> (ap);

        if (p->needsUpdate.compareAndSetBool (0, 1))
        {
            p->copyValueToValueTree();
            anythingUpdated = true;
        }
    }

    return anythingUpdated;
}

Where the parameter’s copyValueToValueTree() method is simply:

void copyValueToValueTree()
{
        if (state.isValid())
            state.setPropertyExcludingListener (this, owner.valuePropertyID, value, owner.undoManager);
}

Perhaps removing that is the cause of the slow-down? It looks like previously the parameters had a ValueTree state members so when they updated there wasn’t a need traverse the tree, the APVTS would just iterate parameters and tell them to update the ValueTree reference they were already holding. Now it’s iterating the parameters and then iterating over the tree to find the child tree to update


#9

Similarly, setNewValue() sets the parameter’s floating point value without giving the parameter any reference to its relevant ValueTree whereas it used to actually change the ValueTree state member:

// Member function of AudioProcessorValueTreeState::Parameter pre JUCE 5.4
void setNewState (const ValueTree& v)
{
        state = v;
        updateFromValueTree();
}

#10

Thanks for the pointers.
I am looking at 5.4.1 now, and I know this part got a good refactor from Reuben at Roli:

The copyValueToValueTree is gone for good.

The flushParameterValuesToValueTree() looks now like this:

There is also neither a recursion nor any lookup, that would imply O(n^2).

setNewState() doesn’t call anything else now:

I think when finding problems with the current version, a look into the pre-5.4.1 versions won’t tell us much…

(I appreciate your insights though @TonyAtHarrison)


#11

Ah I see, I was looking at the master branch - we follow the tagged releases in our plugs

Since it’s develop I guess this refactor will be a future change (5.4.2)? The release of 5.4.1 will contain the code I linked until 5.4.2 is released, the code gets cherry picked, or if a user chooses to follow the develop branch instead of master


#12

Oh my apologies, I thought that was already out on master as well.

I have no insight, when the next release is going to happen unfortunately…


#13

Ah, no worries! Hopefully soon, I’m trying to clean up our current JUCE copy of patches and cherry-picks and get us to just a stable release copy :wink:


#14

Hey, this is good stuff! flushToTree() in 5.4.1 takes the Tree as an argument, and is provided by getChildValueTree(). That routine iterates the tree children in ValueTree::SharedObject::getChildWithProperty() - juce_ValueTree.cpp:227

ValueTree getChildWithProperty (const Identifier& propertyName, const var& propertyValue) const
{
    for (auto* s : children)  // thousands of these
        if (s->properties[propertyName] == propertyValue)
            return ValueTree (*s);

    return ValueTree();
}

The flush that gets us here:

bool AudioProcessorValueTreeState::flushParameterValuesToValueTree()
{
  ScopedLock lock (valueTreeChanging);

  bool anyUpdated = false;

  for (auto& p : parameters)  // thousands of these
  {
    anyUpdated |= p->flushToTree(getChildValueTree(p->getParameter().paramID),
												   valuePropertyID,
												   undoManager);		
  }

  return anyUpdated;
}

Is it true that this may be fixed in master? I’m working from 5.4.1, and if this stuff has been refactored, it very well could be better now.

-M


#15

It seems it’s changed on develop, but still iterates the tree like you’re seeing in master (5.4.1)


#16

With 10k params, this results in 100m iterations, worst case. :joy:

Using 5.3.1, I was able to make minimal changes and squeak this out, which solves the bulk of the problem for me:

bool AudioProcessorValueTreeState::flushParameterValuesToValueTree()
{
ScopedLock lock (valueTreeChanging);

bool anyUpdated = false;

for (auto& p : parameters)
{
	if (p->needsUpdate)
	{
		anyUpdated |= p->flushToTree(getChildValueTree(p->getParameter().paramID),
			valuePropertyID,
			undoManager);
	}
}

return anyUpdated;
}

AudioProcessorValueTreeState::ParameterAdapter::needsUpdate has to be moved (or an accessor created) for this to work. Now - the first update is slow, but everything is fine after it finishes. Better indexing (juce::HashMap?) of ValueTree children could fix this problem completely, I think.

-M


#17

Did you try the develop branch @Matt_Ryan?


#18

I haven’t yet @daniel - I’m using the 5.4.1 release and try to stick closely to the main release tags, and not touch juce. (When I can help it)

I’ll pull develop this evening and give it a shot, though. From what I can see in the source, I expect it will fix the problem. :slight_smile:

-M


#19

Awesome, thanks for giving it a shot. Good luck!


#20

@reuk any input on this? @daniel mentioned it was your refactoring in the latest version.