Java + Swing: writing code to coalesce change events
Asked Answered
B

5

6

I have this data flow, roughly:

DataGenerator -> DataFormatter -> UI

DataGenerator is something that generates data rapidly; DataFormatter is something that formats it for display purposes; and the UI is just a bunch of Swing elements.

I'd like to make my DataGenerator something like this:

class DataGenerator
{
   final private PropertyChangeSupport pcs;
   ...
   public void addPropertyChangeListener(PropertyChangeListener pcl) {
     this.pcs.addPropertyChangeListener(pcl); 
   }
   public void removePropertyChangeListener(PropertyChangeListener pcl) {
     this.pcs.removePropertyChangeListener(pcl);
   }
}

and just call this.pcs.firePropertyChange(...) whenever my data generator has new data; then I can just do dataGenerator.addPropertyListener(listener) where listener is responsible for pushing the change forward to the DataFormatter and then to the UI.

The problem with this approach, is that there are thousands of dataGenerator changes per second (between 10,000 and 60,000 per second depending on my situation), and the computational cost of formatting it for the UI is high enough that it puts an unnecessary load on my CPU; really all I care about visually is at most 10-20 changes per second.

Is there any way to use a similar approach, but coalesce the change events before they get to the DataFormatter? If I receive multiple update events on a single topic, I just care about displaying the latest one, and can skip all the previous ones.

Barnet answered 8/2, 2012 at 21:52 Comment(2)
How about maintaining a long value with the System.nanoTime of the last UI-update, and ignore property-change events if they occure N ns after an update?Chlordane
I thought of that (heck, System.currentTimeMillis() would work -- I just care if things are quick relative to human perception), but there's a problem. Let's say you have a change event 100 microseconds after a visual update, so the DataFormatter / UI ignore the change. Now for some reason the DataGenerator stops producing updates (it's not necessarily 10-60K events per second continuously). Whoops, you've missed the most recent change. That's bad.Barnet
S
5

Two ideas:

  • Aggregate PropertyChangeEvents. Extend PropertyChangeSupport, overwrite public void firePropertyChange(PropertyChangeEvent evt), fire only if last event was fired more than 50ms (or whatever time seems appropriate) ago. (In fact you should overwrite every fire* method or at least the one you use in your scenario to prevent the creation of the PropertyChangeEvent.)
  • Drop the whole event based approached. 60.000 events per second is a quite high number. In this situation I would poll. It is a conceptual change to MVP where the presenter knows if it is in active state and should poll. With this approach you don't generate thousands of useless events; you can even present the highest possible frames per second, no matter how much data there is. Or you can set the presenter to a fixed rate, let it sleep between refreshes for a certain time, or let it adapt to other circumstances (like CPU load).

I would tend to the second approach.

Stovepipe answered 8/2, 2012 at 22:17 Comment(3)
OK. For your second approach, is there a way to use polling (I can easily keep a "changed" flag of some sort using AtomicBoolean) but still keep a decoupling between the two parties? I don't want my DataGenerator to have to know about its downstream client; furthermore, my problem statement was a bit simplified: in reality there are 256 DataGenerators with an aggregate of 10-60K changes per second. (most of the time only a few are active, but I can't predict which ones are at any given instant. It's a long story.) The property-change approach is nice in that I just get notified as necessary.Barnet
But you still have to register at some instance at the beginning. Does it matter if you are storing the reference for polling or calling 'reference.addPropertyChangeListener(this)'? I agree that it may be a cleaner concept to have an active, decoupled model - but not with this data amount (and MVP pattern changes responsibilities for such reasons). I would write an Aggregator that knows your DataGenerators, collects the data and can be polled by the presenter. In fact this Aggregator even could send 20 events per second. I just don't see 60.000 event processings as efficient or useful.Stovepipe
An interesting blog entry regarding this topic has just been posted.Stovepipe
L
3

It sounds like your DataGenerator doing a lot of non-GUI work on the EDT-thread. I would recommend that your DataGenerator extend SwingWorker and by that doing the work in a background thread, implemented in doInBackground. The SwingWorker could then publish intermediate results to the GUI, while you have a process method that receives the last few published chunks on the EDT and update your GUI.

The SwingWorkers process method does coalesce the published chunks, so it will not run once for every published intermediate result.

If you only care of the last result on the EDT you can use this code, that only cares about the last chunk in the list:

 @Override
 protected void process(List<Integer> chunks) {

     // get the *last* chunk, skip the others
     doSomethingWith( chunks.get(chunks.size() - 1) );
 }

Read more on SwingWorker: Tasks that Have Interim Results.

Lacee answered 8/2, 2012 at 22:42 Comment(3)
DataGenerator isn't in the EDT thread and for various reasons happens in another thread managed by an ExecutorService.Barnet
@JasonS: I see. But SwingWorker can also be run on an ExecutorService, see the SwingWorker-documentation: Because SwingWorker implements Runnable, a SwingWorker can be submitted to an Executor for execution.Lacee
@JasonS: I have a full example on how to use the latest intermediate result from a SwingWorker in my answer to Stop/cancel SwingWorker thread?Lacee
U
1

You could use a ArrayBlockingQueue of size 1, in which you would push your data with offer() function (means if the queue is full, it does nothing)

Then create a ScheduledThreadPoolExecutor which periodically polls the queue.

This way you loose the coupling between generation and display.

Generator -> Queue -> Formatting/Display

Ute answered 8/2, 2012 at 22:25 Comment(1)
But I don't want a queue; I want to coalesce events. If a DataGenerator says "My value is blue!" and then later "My value is red!" and then later "My value is purple!" I don't care about the first two, I just care about the latest one.Barnet
C
1

Another possibility is to just add the listener to your generator, but instead of directly reacting to the change you just start a timer. So your listener would look like (in sort of pseudo-code since I am too lazy to fire up my IDE or look up the exact method signatures)

Timer timer = new Timer( 100, new ActionListener(){//update the UI in this listener};

public void propertyChange( PropertyChangeEvent event ){
 if ( timer.isRunning() ){
   timer.restart();
 } else {
   timer.start();
 }
}

This will work unless your data generator keeps on generating data the whole time, or if you want intermediate updates as well. In that case you can just remove the timer.restart() call, or opt for any of the other suggestions in this thread (the polling mechanism, or the SwingWorker)

Charters answered 8/2, 2012 at 23:57 Comment(0)
T
0

If you write your own small change listener that has a blocking flag and a timer, you can:

syncronized onChangeRequest() {
    if (flag) {
      flag = false;
      startTimer();
    }
}

timerEvent() {
    notify all your listeners;
}

I believe there is actually an excellent blocking concurrency flag that could be used but I cannot for the life of me remember what it is called!

Thill answered 8/2, 2012 at 22:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.