I’m currently evaluating the best architecture for managing high-density, dynamic datasets on the Message Thread in JUCE, and I’d love to hear how others in the community approach this.
The Scenario:
Imagine a continuous curve/spline (e.g., an automation lane or modulation shape) made up of thousands of small nodes. Each node consists purely of primitive types (e.g., two floats for position/value, a uint8 for interpolation type, etc.).
These nodes are frequently edited in the UI:
Modified: Dragging values or curve handles in real time.
Undo/Redo: Full support needed for all user interactions.
Option A: One ValueTree per Node
Pros: Native UndoManager support out of the box, easy binding to individual UI components, granular listeners, fine-grained hierarchy.
Cons: Massive memory overhead and potential performance degradation when batch-modifying thousands of nodes.
Option B: A single ValueTree containing a juce::MemoryBlock / std::vector
Pros: contiguous memory access and minimal memory footprint.
Cons: Native JUCE UndoManager replaces the entire property on change (requiring custom Command/Delta pattern or UndoableActions for micro-edits), and property listeners trigger coarse “all-or-nothing” updates unless wrapped in custom adapters.
Curious to hear your experiences, trade-offs, and benchmarks… or maybe a better solution?
Both would work well enough, with reasonable performance profiles.
I’d lean towards a contiguous memory block. Even thousands of nodes is probably going to be lower digit kilobytes for the lot so undo could just work with snapshots.
You’d get decent L1 cache utilisation too so it’d be super snappy.
Could you do a hybrid approach and have a ValueTree with MemoryBlock properties?
You probably only need to load a chunk of the data at a time, so you could have one property per block for example. You’d get the benefits of the memory alignment and could still undo/repo, just with the caveat that it will undo/redo a whole chunk at a time rather than individual nodes?
A giant single and massive ValueTree state that you synchronise with ValueTreeSynchroniser or whatever is fine. It’s less about the tree and more about what data you’re putting in, and when and how often.
If you’re planning on storing massive files in memory, this probably shouldn’t get mushed into a ValueTree.
Say you’re structuring basic primitives around XML or JSON-like schemas into a ValueTree, the memory and CPU overhead are pretty low. Your code’s usability increases greatly with it.
The value tree itself isn’t the issue when it comes to performance - it’s juce::var which is always (IIRC) 128 bits (64 for the value, and 64 for a pointer). Even if you’re storing 64-bit values, then your memory alignment is broken, and if you’re storing smaller values e.g. float then you’re using 4x as much memory per value.
Being able to use low-level operations like memcpy, memset, etc. make certain operations orders of magnitude faster than with juce::vars. E.g. I did some benchmarks recently and found that (with compiler optimisations) filling a std::vector<double> with 0’s was over 8,000 times faster than filling a std::vector<juce::var>.
Were the benchmarks in a Release build with debug symbols? Just checking, not a criticism.
I’d be curious to know why/how/where the speed is dropping. Some public data would be good, especially since ValueTrees are backbones for many projects out there.
Yes, with -O3. In a debug build there’s little difference.
The benchmarks are easy to reproduce - just use std::fill() with a vector of juce::var and a vector of double and compare the difference.
The 8,000x difference was with filling vectors with zeros where the primitive types likely do no work after it’s determined they’re already zeroed. I just ran them again using juce::Random instead and the vector of doubles is only about 70x faster.
To give a bit more context, I’m currently facing an architectural dilemma involving a project with a high number of small, individual objects, each containing editable properties.
My initial approach of backing each object with its own juce::ValueTree led to significant disk footprint issues when serializing to disk—easily scaling up to gigabytes. Serializing state directly as binary data reduced this footprint down to a few megabytes. While filtering out unused objects improved disk usage considerably, I know there is still significant room for optimization, especially by storing data directly inside binary juce::MemoryBlock properties.
Performance-wise, juce::ValueTree granularities also impact the undo history. Modifying 2,000 separate juce::ValueTree nodes generates 2,000 distinct juce::UndoableAction instances. Consolidating that state into a single MemoryBlock property reduces that overhead to a single UndoableAction.
However, encapsulating batch state within a single MemoryBlock introduces new challenges regarding identification and targeted edits:
ID Mapping: Because objects cannot be uniquely identified by their raw content, each requires a unique identifier (such as a uint32_t).
Batch & Single-Item Mutations: I tested passing an std::unordered_set<uint32_t> of selected IDs along with a processing lambda, iterating through the entire collection and checking which element is inside the std::unordered_set<uint32_t> to apply mutations before writing the updated MemoryBlock back to the ValueTree. While functional, it feels suboptimal: modifying a single element forces a full payload copy, incurring the cost of duplicating the entire buffer in the UndoableAction state.
External Undo Management: Attempting to manage custom UndoableAction classes outside of the ValueTree hierarchy quickly becomes unmaintainable.
Has anyone encountered a similar trade-off between ValueTree flexibility, binary serialization footprint, and undo manager efficiency? I would appreciate any insights on recommended patterns for managing large-scale granular states in JUCE.
@juan1979 I think it’s worth getting specific about what you’re doing. I feel like this is all AI and glossy, and just too high level to be practical - maybe even avoidant.
What’s the actual problem?
Tracktion uses ValueTree and whatnot just fine. And it’s a DAW.
It sounds to me like neither ValueTree nor MemoryBlock are right for this use case, and that you’ll need to create your own container for it which:
Stores the data in contiguous and cache-friendly way
Serializes into a ValueTree somehow
Is quickly undo-able without saving the entire state for every snapshot.
3 is probably the hardest requirement, and you should first consider if it is really necessary (if the requirement could be removed, the problem becomes easy).
Assuming that the undo action is required, I would consider trying to find or create a data structure which has one baseline vector for the original data, and then doesn’t mutate the data with user changes, but instead stores them on top of it in a series of layers.
When you write data, it creates or re-uses a layer, and updates a pointer somewhere to indicate where the latest layer is.
When you read the data, you scan through the original layer and substitute whatever changes have been made through the pointed-to layers.
When you create an undo snapshot, you save 1: a pointer to the original data, and 2: a set of pointers to the current layers (which should be a much smaller footprint than re-saving the whole baseline layer).
This would not be easy to build, but if you got it right, it should satisfy all of your requirements. A few things to consider though:
Is your data being accessed on the audio thread? If so then you have a much bigger problem to solve and all bets are off (and that goes for your ValueTree or MemoryBlock solutions as well).
It would be good if you could “flatten” the change layers down into the base layer once in a while. This would prevent the data from getting too fragmented, and would also help when serializing the whole thing and having compatibility with ValueTree or MemoryBlock for serialization. If you’re clever, you could tie this in with UndoableAction::createCoalescedAction()as well.
Take a look at the Immer library: GitHub - arximboldi/immer: Postmodern immutable and persistent data structures for C++ — value semantics at scale · GitHub. It does something similar, storing data as an immutable baseline vector and then changing them by adding layers on top. Unfortunately I don’t think it will work in your case as I don’t think it give you access to the intermediate layers. But it might provide some inspiration, or there might be another library out there that does what you want.
Thanks for your replies, and my apologies for the late response, I’ve been really busy.
What I was trying to do was precisely that: a spline, but for entire music sessions (about an hour long).
Years ago, when I tried building a step sequencer, having everything heavily structured in ValueTrees resulted in a complete nightmare with file save sizes in the gigabyte range. I just wanted to prevent that issue upfront and ask if any of you had a better idea.
But I think I’ll go ahead with that approach, having each node as a ValueTree.