Update Notifications

Careful design and implementation of update notification mechanisms is critical to the performance of interactive systems. There are three main approaches to update notification:

  • Low-Frequency App-wide
  • Polled
  • High-Frequency Direct

Low-Frequency App-wide

User interaction with a system tends to be low-frequency and sporadic. Users expect immediate response to changes that they make. In these low-frequency cases, it is acceptable to simply redraw the entire UI in response to changes made by the user. This reduces the UI update code to a simple “Update()” function that redraws everything.

Since the user can only interact with one part of the system at a time, a very large system with many windows can have a single update notification mechanism shared by all. This might end up being a bit of a global nightmare, so having update notifications in each window is OK too, so long as they don't need to update other windows in the system. Then a single global notification is a good idea.

Rosegarden Specific

The RosegardenDocument::documentModified() signal is the global app-wide notification mechanism that should be used throughout Rosegarden to trigger an update in response to low-frequency user input. As of 11/2017, this is not the case. Other notification mechanisms sprinkled throughout Rosegarden (e.g. CompositionObserver) should be switched to using RosegardenDocument::documentModified().

Most edits in any of the editors are typical low-frequency use-cases. E.g. deleting a note, changing a note's duration, deleting a segment, pasting from the clipboard, etc… For these cases, it is acceptable to refresh the entire app UI.

Implementation Details

  • The notification will have *no parameters* since it always causes a complete refresh.
  • *Do not* send a change notification from a setter.
  • Notifiers should call this as *infrequently* as possible.
  • Only use for *low-frequency* user interactions.
  • Observers (the UI) should respond by doing a *complete refresh*.
  • The complete refresh can be optimized, but *don't optimize prematurely*. Try without optimization and see how it looks and feels.
  • A single app-wide mechanism would be best, but *slightly* finer grain (subsystem-wide or window-wide) is OK.

The first three work together to simplify the code and significantly improve performance. The opposite end of the spectrum would be to include every possible detail related to the change in the change notification. This is a “fine-grained” approach. E.g.

// Don't do this!
void HasChanged(operation, segment ID, event ID, value);

This would allow the observer to make very fine grained changes to the UI. E.g. changes just to specific fields. The problem is that this now requires anyone making changes to send an update for every single tiny change. That pushes the HasChanged() calls into the setters. And this means that we need optimized handling in the UI for every single possible change that might come in. It ends up being a great deal of complicated code. And it isn't reusable in case we also want to use a polled approach (which is very common).

Performance-wise, this fine-grained approach should be better for very small changes, but it is terrible for large changes like deleting 10 notes. In the end, it is premature optimization of a simpler approach. The complete refresh approach is highly optimized for large changes and good enough for low-frequency small changes (especially if some detection of actual changes is easy to implement). The code is a lot simpler. Notifiers no longer need to send notifications for every change. They can make a whole slew of changes to the underlying data and then fire off a single notification which will update the entire display.

Examples

AudioMixerWindow2 is the most recent as of November 2017 to have been rewritten to use RosegardenDocument::documentModified() as its primary update notification mechanism. AudioMixerWindow2::slotDocumentModified() handles the signal and calls updateWidgets() which updates the entire UI.

Polled

There are two main cases where polling is ideal:

  • Slow polling: when there are high-frequency changes in a system and it is not imperative that the user see them immediately.
  • Fast polling: when generating real-time audio or video.

The slow polling case results in a significant reduction in CPU usage. The UI is only updated maybe once a second while changes may be coming in at a very high rate, e.g. thousands of updates a second. Slow polling combines nicely with the “Low-Frequency App-Wide” approach in the previous section since it can use the same UI update code.

The fast polling case is only needed in special circumstances. Since the display is polling the underlying data quickly, there is no need for any other update mechanism. All changes to the system are immediately reflected in the rapidly updating UI.

Rosegarden Specific

When recording, a massive amount of MIDI data is coming in and being added to the document. We don't want to refresh the UI for every single note-on or note-off message. That could result in thousands of updates to the UI per second. Instead, we want to update the segment as data comes in, but only refresh the UI maybe 5 to 10 times per second to show that MIDI is indeed being recorded. The slow polling approach is appropriate here.

During playback, the playback position pointer and the clock on the transport window need to be updated constantly to show the current position in the document. A fast polling approach works best here.

Sending out MIDI and Audio would probably also fit the fast polling approach. Although those might be driven by external (ALSA/JACK) timers, it's the same idea.

Implementation Details

  • Use a timer.
  • The timer handler should simply call an update routine that does a complete refresh.
  • Optimization on the receiving (polling) side is important in the fast polling case.
  • Optimization on the sending (data update) side may be important in the slow polling case if the incoming updates are frequent. Try to use as little CPU as possible when handling updates that come in rapidly.

Examples

As of November 2017, there are examples of the two polling approaches in Rosegarden, although I've not been in those areas in a while and I would need to track them down. I do remember that fixing some recording bugs in Rosegarden required moving the code toward a slow polling approach. Also, the playback position pointer was moved toward a fast polling approach. This was older work, though, so these might not be the best examples to build from.

High-Frequency Direct

Immediate response to a high-frequency user update. E.g.: dragging things with the mouse, an indicator that changes with mouse position, fast typing (in an editor).

This technique should be avoided if possible. It uses the most CPU and should be reserved only for the very specific situations where it is needed.

Rosegarden Specific

This approach is usually used for dragging things on the UI. E.g. dragging segments on the Segment Canvas (CompositionView) or notes on the notation view.

Implementation Details

  • Make as *direct* of a connection as possible.
  • Details can be included in the notification if that helps optimize things.
  • The update routine should be *highly optimized*.
  • The update routine should update *as little* of the UI as possible.
  • The update routine should *never* update the entire UI.

Examples

As of November 2017, InstrumentStaticSignals::controlChange() is the latest example of a high-frequency direct update mechanism. AudioStrip::slotFaderLevelChanged() emits controlChange() when the user changes the fader level. AudioMixerWindow2::slotControlChange() handles it and passes it on to AudioStrip::controlChange() which updates the appropriate widget.

Since this is typically needed when dragging things on the UI, any mouseMoveEvent() in the system should be an example of this technique.

 
 
dev/update_notifications.txt · Last modified: 2022/05/06 16:07 (external edit)
Recent changes RSS feed Creative Commons License Valid XHTML 1.0 Valid CSS Driven by DokuWiki