What's best practice for GUI change notification?


#1

OK, I’ve been collecting notes on this old chestnut and thought I’d post them here to see if others can either answer some of my questions, make recommendations, or point out errors. I’ll try and edit contributions back into this first post for posterity.

[size=30]Possible Solutions for GUI Change Notification[/size]

1. Use a timerCallback in the AudioProcessorEditor to periodically check the AudioProcessor
[list][]Pass a pointer to the AudioProcessor to the constructor of the AudioProcessorEditor (access variables via the pointer, but do this read-only if you want to avoid using locks)
[
]Jules recommends timers as a simple and safe alternative to AysncUpdaters (pages 1, 2 & 4 of this topic)
[]Incurs a little bit of overhead when nothing is happening
[list][
]Jules suggests the timer period could be dynamic (see example in this post)[/list][/list]

2. Use AsyncUpdater to send updates from the AudioProcessor
[list][]Apparently this incurs mallocs and therefore is not good for AudioProcessor (esp. in RTAS)
[
]Vinnie reckons that AsyncUpdater::triggerAsyncUpdate can block the audio thread / calling thread because it internally it calls SendMessage (on Windows at least)[/list]

3. Use ChangeBroadcaster to notify AudioProcessorEditor that a change has occurred
[list][]Can’t indicate what the change actually is, so would need to check everything
[
]ChangeBroadcaster::ChangeBroadcasterCallback inherits from AsyncUpdater so could also suffer from potential malloc problems[/list]

4. AudioProcessListener can be used for communicating parameter changes made by AudioProcessor
[list][*]But apparently not called by host (refer to this and this)[/list]

5. Use MessageThread & Listeners from VFLib
[list][]Order of destruction issues lead to false leak detections firing on “Thread” by JUCE’s LeakedObjectDetector after messages have been queued (work around this by turning it off and using vf::LeakedObjectDetector<> together with _CrtSetDbgFlag instead)
[
]Does malloc get called when a call is queued? Is there any other overhead? See Vinnie’s answer below.[/list]


#2

vf::ConcurrentState wasn’t really meant as a substitute for a CriticalSection.

I wrote ConcurrentState to fulfill the requirement of updating a listener when it is first added. Given:

struct Settings { /*...*/ };

struct State {
  Settings settings;
};
typedef vf::ConcurrentState <State> StateType;

struct Listener {
  // Called when any part of the settings changes.
  virtual void onSettingsChange (Settings settings) = 0;
};

In this example we have a non-atomic structure with some compound settings information, to which observers can listen for changes. Immediately we see a problem: How does the observer receive the initial state? There’s no sane way to do it synchronously so we have to notify the listener using the standard techniques.

To do this, we need to take a read-only lock on the settings data while the listener is being added. This is very important, if we don’t hold the lock during the addition of the listener, we can violate invariant related to the visibility of the order of modifications of mutable data. Holding the lock presents its own unique problem, which is the possibility of deadlock because we are calling into external code. Remember that CallQueue::call will call synchronize if the calling thread is the associated thread (the last thread to call synchronize on the queue).

To prevent deadlock, we use the queue1 function of Listeners which bypasses the synchronization behavior of CallQueue. This guarantees that no external code is called while the lock is held:

struct AudioThread {
  StateType m_state;
  vf::Listeners <Listener> m_listeners;

  void addListener (Listener* listener, CallQueue& thread) {
    StateType::ReadAccess state (m_state); // must happen before add()
    m_listeners->add (listener, thread);
    // Update just the one listener
    m_listeners.queue1 (listener, &Listener::onSettingsChange , state->settings);
  }

  void updateSettings () {
    StateType::WriteAccess state (m_state);
    // modify state
  }
};

This is a very fine detail on the usage of cross-thread observer notifications for shared state and it took a while to get it right and make it easy to use. If you’re using these classes I strongly suggest reading and re-reading this post until it is crystal clear; Failure to have a complete understanding will lead to painful and fruitless debugging sessions.


#3

Thanks Vinnie, I might read that one a good dozen times over the weekend :slight_smile:


#4

Short answer: No.

Long answer: Qualified No.

CallQueue and Listeners use FifoFreeStore as their ABA-immune allocator. The allocator has these properties:

  • Immune to the ABA Problem, without using 64-bit CAS
  • Memory is requested from the system in large blocks
  • Memory is typically recycled and not freed
  • A garbage collector reduces the “working set size” to the minimum number of blocks
  • Most allocations do not call malloc or take a CriticalSection.

When this memory allocator is first used there are several calls to malloc, and once the working set has a good handful of large blocks there are no more calls to malloc and free. Think of it as “priming the pump.”

The overhead for using a Listener or CallQueue is as low as possible, and suitable for direct usage in the audio device I/O callback. It’s been profiled and optimized extensively. If you have access to boost, the version with “Thread Local Storage” is superior. JUCE’s thread local storage is not as robust as the one in boost (which will soon be in all c+11 implementations). The difference is not very large though, you’ll likely not notice for an audio application (I tested the code using 64 threads and 1024 notifiers).

CallQueue::call is wait-free, runs in constant time plus the overhead of signal (a virtual function override), and takes no locks.

GuiCallQueue::signal incurs the overhead of an additional CallQueue::call, which it uses to notify an AsyncUpdater on a separate thread.

A call made on a ThreadWithCallQueue incurs the constant time overhead of CallQueue::call plus the cost of signaling a waitable event object.

Listener::call runs in O(N) where N is the number of different threads which have attached listeners. For example, if you have 10 listeners on 3 threads the runtime is O(3) (and not O(10)). This means that signaling the GUI thread runs in constant time no matter how many listeners are attached. The cost of multiple listeners is paid on the receiving end. In addition, Listener::call pays the signal price for each associated thread (depending on its virtual override).

The ManualCallQueue, which is used to call functions on the audio thread, has an empty body for signal which makes it cost almost nothing.


How to access the processorEditor from the process block
#5

Also note, vf::ReadWriteMutex used in vf::ConcurrentState, is wait-free when there are no writers, which is most of the time. Acquiring the read lock is equivalent to performing an atomic increment. It also allocates no memory. Compare this with the JUCE implementation, which is orders of magnitude slower.

Like all well written synchronization primitives, ReadWriteMutex does not support recursion or does it support “try” functionality.


#7

I despise the timer approach but regardless, the “heavy lifting” is not in the change notification, but in the methods used to pass non-atomic data between threads. For displaying a shared primitive, like an int or a float, almost any technique will work. However as soon as you have something more complex which cannot be natively atomically updated, poor implementations quickly show vulnerabilities.

That is why in VFLib I have integrated the signaling mechanism along with the data transfer mechanism (its done through function parameters stored in the bind). The signaling mechanism is the easy part (you call for a timer to be used). Once you have addressed the difficult part, which is passing the actual data, then it makes sense to use the combined solution as a unified model for dealing with data transfers in a concurrent system.


#9

For the reason that you mentioned, because the update might not be atomic (“graphical glitches”). Like I said, the timer approach is unsuitable for all but the most simplistic of data. And simple data is usually not troublesome when building a concurrent system. So what approach is sufficiently general and robust to work with all types of data, not just the simple kind? Not timers…

Or the third option, which is to build a mathematically correct system end to end with no graphical glitches or audio glitches!

Yes! I agree with everything expressed in the article! And VFLib’s CallQueue and Listeners follows every single piece of advice in there.

Note that there are no critical sections used in CallQueue. I posted the execution time analysis in that earlier thread.


#10

Vinnie - to me this is the crux of the matter when trying to decide between using the timer method or your approach. i.e. where is the boundary between simple and complex? As MarC points out, you can break down non-atomic data into primitives “in a way that nothing goes wrong regardless of their value.” Though I’d qualify that and say, nothing goes terribly wrong.

For example, I manage each of my parameters with a class called Parameter which provides methods getGUIValue(int programIndex) & toStringWithUnits(int programIndex). So in a timerCallback my GUI can do this:

int i = pluginProcessor->getCurrentProgram(); paramSlider->setValue (pluginProcessor->param.getGUIValue (i), dontSendNotification ); paramLabel->setText (pluginProcessor->param.toStringWithUnits (i), false);
The worst thing that can happen here is for the program to be changed during the timerCallback so we get an inconsistency in what’s displayed. I don’t like that, but pragmatically speaking it doesn’t matter because it’s going to be fixed at the next refresh (40 msec later in my case). The other case of interest for GUIs is metering - again, I don’t have many issues with this if I use the timer approach.

I should say that I feel uncomfortable with the timer approach because it lacks finesse, but my imagination is failing to come up with compelling issues that would encourage me to use your approach. Maybe that’s just because I’m not doing anything complicated enough!


#11

Well, look at it this way. Would you use a timer to pass data to the audio device I/O callback? Would you update multiple atomic values in the GUI thread on a structure that was looked at by the audio thread (where the individual values have collective meaning)? Of course not. What if you had a third thread, not the audio thread or the GUI thread, and you needed to communicate information. For example, a background thread that processed external audio files and produced a thumbnail. Would you use timers to pick up its results? What if you “miss” a notification? Who deallocates the memory in that case (hint: the worker thread).

A musical envelope consists only of primitives (int/float for attack, decay, sustain, release). But would you update these non-atomically? Of course not, the results would be disastrous. That’s a perfect example of non-atomic data which is broken down into primitives, but updating values non-atomically will cause things to go wrong.

Perhaps your existing system will remain simple enough for the timer approach to work but since the demands placed on software only increase (never decrease) eventually you will reach a point where you need a new paradigm for managing concurrent data. Enter… VFLib! Conveniently licensed (MIT), plenty of documentation and example code, and a robust proven framework. Now there is a unified system for handling all types of thread communication within a JUCE application or plugin.


#12

This is the most persuasive argument for me as I am wishing to implement a future-proof framework for my plugins.

Nice promotion :slight_smile:


#13

On the other hand, most of the complex use-cases contemplated are applicable mostly for host applications and not necessarily plugins.

For simple to moderately complex effects and instruments you could probably get away with timers almost indefinitely. But if you’re building one of those crazy plugins which is like its own little application in the interface (for example one with a full blown sequencer and digital instrument designer) then timers might not cut it.


#14

Geez, don’t go all wishy washy now!


#15

Heh well I think its better to be “fair and balanced” than overzealous in promoting the “one right way to do things.”


#17

SimpleDJ in my signature


#19

vinnie - I just downloaded SimpleDJ and compiled the app on OSX, but it seems to be a bit buggy. Other sliders move very erratically when I try to change the pitch slider for one deck. Is it working for you?

see this vid:

http://dl.dropbox.com/u/33721778/iShowU-Capture.mov

I am really interested in this topic too, so I am investigating your work, which looks great.

oli


#20

Yep, I updated to the latest JUCE and the slider changes goofed everything up. I fixed it so go ahead and pull the branch again.


#21

SimpleDJ uses vf::MessageThread for updating the levels so doesn’t that mean if you’re running at 64 samples @ 44100 then the message queue gets posted to ~670 times per second? I prefer the idea of using VFlib’s mechanisms so I’m not trying to make an argument for timers, but a timer would typically be processed 30-60 times a second which seems a lot less taxing on the message queue. I’m not an expert on the message queue so is the difference negligible or am I misunderstanding how it’s working?


#22

vf::Listeners<>::update() will replace an existing notification if it hasn’t been processed yet, rather than continually appending to the queue.

However, even if you use vf::Listeners<>::call(), only one message ever gets posted to the platform-specific message queue at a time no matter how many notifications you send or how quickly/slowly the GUI processes them. This is true even when you have multiple threads all posting to the GUI thread at the same time.


#23

Did this get answered to your satisfaction? I saw your question in the #JUCE IRC channel.


#24

Sorry Vinnie, yes it did thanks! I’ve had to pick up a contract so I was at work yesterday when I saw your response but didn’t have the opportunity to reply at the time.