0

I have some signal processing data which gets fed at roughly at 50Hz. I need to update a rectangle's opacity based on the signal value in real time. I am trying to develop the UI in JavaFX 8.

For time being I am simulating the signal value using random number generator in JavaFX service in my code.

I am using Platform.runLater to update the UI, however this doesn't update values in real time, I read through similar problems encountered by others and the normal suggestion is that not to call Platform.runLater often but to batch the updates.

In my case if I batch my updates, the frequency at which the opacity changes will not be equal to the signal frequency.

Any thoughts on how to achieve this?

public class FlickerController
{
@FXML
private Rectangle leftBox;
@FXML
private Rectangle rightBox;
@FXML 
private ColorPicker leftPrimary;
@FXML 
private ColorPicker leftSecondary;
@FXML 
private ColorPicker rightPrimary;
@FXML 
private ColorPicker rightSecondary;
@FXML
private Slider leftFrequency;
@FXML
private Slider rightFrequency;

@FXML
private Button startButton;
@FXML
private Label leftfreqlabel;

@FXML
private Label rightfreqlabel;

@FXML
private Label rightBrightness;
@FXML
private Label leftBrightness;


private boolean running = false;

  DoubleProperty leftopacity = new SimpleDoubleProperty(1); 
  DoubleProperty rightopacity = new SimpleDoubleProperty(1);    

  private FlickerThread ftLeft;
  private FlickerThread ftRight;

public void initialize()
{


    leftopacity.addListener(new ChangeListener<Number>() {

        @Override
        public void changed(ObservableValue<? extends Number> observable,
                Number oldValue, Number newValue)
        {
            Platform.runLater(new Runnable()
            {

                @Override
                public void run()
                {
                    double brightness = leftopacity.doubleValue();
                    leftBrightness.setText(""+brightness);
                    leftBox.opacityProperty().set(brightness);
                }

            });
        }
    });
    rightopacity.addListener(new ChangeListener<Number>() {

        @Override
        public void changed(ObservableValue<? extends Number> observable,
                Number oldValue, Number newValue)
        {
            Platform.runLater(new Runnable()
            {

                @Override
                public void run()
                {
                    double brightness = rightopacity.doubleValue();
                    rightBrightness.setText(""+brightness);
                    rightBox.opacityProperty().set(brightness);
                }

            });
        }
    });

    startButton.setOnAction(new EventHandler<ActionEvent>() {

        @Override
        public void handle(ActionEvent event)
        {
            if(running)
            {
                synchronized(this)
                {
                    running=false;
                }
                    startButton.setText("Start");
            }
            else
            {
                running=true;
                ftLeft = new FlickerThread((int)leftFrequency.getValue(),leftopacity);
                ftRight = new FlickerThread((int)rightFrequency.getValue(), rightopacity);

                try
                {
                    ftLeft.start(); 
                    ftRight.start();
                }
                catch(Throwable t)
                {
                    t.printStackTrace();
                }
                startButton.setText("Stop");
            }
        }
    });

    leftFrequency.valueProperty().addListener(new ChangeListener<Number>() {

        @Override
        public void changed(ObservableValue<? extends Number> observable,
                Number oldValue, Number newValue)
        {
            leftfreqlabel.setText(newValue.intValue()+"");
        }
    });

    rightFrequency.valueProperty().addListener(new ChangeListener<Number>() {

        @Override
        public void changed(ObservableValue<? extends Number> observable,
                Number oldValue, Number newValue)
        {
            rightfreqlabel.setText(newValue.intValue()+"");
        }
    });
}



class FlickerThread extends Service<Void>
{
    private long sleeptime;

    DoubleProperty localval = new SimpleDoubleProperty(1) ;

    public FlickerThread(int freq, DoubleProperty valtoBind)
    {
        this.sleeptime = (1/freq)*1000;
        valtoBind.bind(localval);
    }


    @Override
    protected Task <Void>createTask()
    {
        return new Task<Void>() {

            @Override
            protected Void call() throws Exception
            {
                while(running)
                {
                    double val =  Math.random();
                    System.out.println(val);
                    localval.setValue(val);

                    Thread.sleep(sleeptime);
                }
                return null;
            }
        };
    }

}
}
abacusreader
  • 547
  • 1
  • 7
  • 21
  • Don't bind things in the scene graph (e.g. node opacity) with a property which is modified in another thread (it may cause unpredictable behaviour). Additionally, I don't think you should have runLater calls in the opacity change listeners, nor should you have synchronized statements on the JavaFX application thread. That was just a quick review, you may have other issues. Also, what is the value range for the leftFrequency slider? – jewelsea Jun 30 '14 at 18:52
  • Frequency range on both the sliders is between 1-100 – abacusreader Jul 01 '14 at 01:46
  • I found this [http://stackoverflow.com/questions/23488280/throttling-javafx-gui-updates] and I am modifying my code to use AtomicReference to achieve what I want, will update – abacusreader Jul 01 '14 at 02:54

2 Answers2

1
 class FlickerThread extends Thread
{
    private long sleeptime;
    final AtomicReference<Double> counter = new AtomicReference<>(new Double(-1.0));
    private Label label;
    private Rectangle myrect;
    public FlickerThread(int freq, Label label,Rectangle rect)
    {
        this.sleeptime = (long) ((1.0/freq)*1000.0);
        System.out.println("Sleep time is "+sleeptime);
        this.label = label;
        this.myrect = rect;
    }


    @Override
    public void run() {
        double count = 1.0 ;
        while (running) {
            count = Math.random();
            if (counter.getAndSet(count) == -1) {
                updateUI(counter, label,myrect);

                try
                {
                    Thread.sleep(sleeptime);
                } catch (InterruptedException e)
                {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }

}
private void updateUI(final AtomicReference<Double> counter,
        final Label label, final Rectangle myrect) {
    Platform.runLater(new Runnable() {
        @Override
        public void run() {
            double val = counter.getAndSet(-1.0);
            final String msg = String.format("Brt: %,f", val);
            label.setText(msg);
            myrect.opacityProperty().set(val);
        }
    });
}
abacusreader
  • 547
  • 1
  • 7
  • 21
  • Just an update, I retested the above code without Atomic reference and the UI updates as good as it was with Atomic reference. It looks more like implementing the Platform.runlater correctly matters. – abacusreader Jul 03 '14 at 07:53
0

You have a calculation error in your code.

Consider:

1/100*1000=0

But:

1.0/100*1000=10.0

i.e. you need to use floating point arithmetic, not integer arithmetic.

There are numerous other issues with your code as pointed out in my previous comment, so this answer is more of a code review and suggested approach than anything else.

You can batch updates to runLater as in James's answer to Throttling javafx gui updates. But for an update rate of 100 hertz max, it isn't going to make a lot of difference performance-wise as JavaFX generally operates on a 60 hertz pulse cycle, unless you really overload it (which you aren't really doing in your example). So the savings you get by throttling updates will be pretty minimal.

Here is a sample you can try out (it uses James's input throttling technique):

import javafx.application.*;
import javafx.beans.property.DoubleProperty;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

import java.util.Random;
import java.util.concurrent.atomic.AtomicLong;

public class InputApp extends Application {
    private final ToggleButton controlButton = new ToggleButton("Start");
    private final Rectangle box = new Rectangle(100, 100, Color.BLUE);
    private final Label brightness = new Label();
    private final Label frequencyLabel = new Label();
    private final Slider frequency = new Slider(1, 100, 10);
    private       InputTask task;

    @Override
    public void start(Stage stage) throws Exception {
        // initialize bindings.
        brightness.textProperty().bind(
                box.opacityProperty().asString("%.2f")
        );

        frequencyLabel.textProperty().bind(
                frequency.valueProperty().asString("%.0f")
        );

        frequency.valueChangingProperty().addListener((observable, oldValue, newValue) -> {
            if (controlButton.isSelected()) {
                controlButton.fire();
            }
        });

        // start and stop the input task.
        controlButton.selectedProperty().addListener((observable, wasSelected, isSelected) -> {
            if (isSelected) {
                task = new InputTask(
                        (int) frequency.getValue(),
                        box.opacityProperty()
                );
                Thread inputThread = new Thread(task, "input-task");
                inputThread.setDaemon(true);
                inputThread.start();

                controlButton.setText("Stop");
            } else {
                if (task != null) {
                    task.cancel();
                }

                controlButton.setText("Start");
            }
        });

        // create the layout
        VBox layout = new VBox(
                10,
                frequency,
                new HBox(5, new Label("Frequency: " ), frequencyLabel, new Label("Hz"),
                controlButton,
                box,
                new HBox(5, new Label("Brightness: " ), brightness)
        );
        layout.setPadding(new Insets(10));

        // display the scene
        stage.setScene(new Scene(layout));
        stage.show();
    }

    // simulates accepting random input from an input feed at a given frequency.
    class InputTask extends Task<Void> {
        private final DoubleProperty changeableProperty;
        private final long sleeptime;
        final AtomicLong counter = new AtomicLong(-1);
        final Random random = new Random(42);

        public InputTask(int inputFrequency, DoubleProperty changeableProperty) {
            this.changeableProperty = changeableProperty;
            this.sleeptime = (long) ((1.0 / inputFrequency) * 1_000);
        }

        @Override
        protected Void call() throws InterruptedException {
            long count = 0 ;
            while (!Thread.interrupted()) {
                count++;
                double newValue = random.nextDouble(); // input simulation
                if (counter.getAndSet(count) == -1) {
                    Platform.runLater(() -> {
                        changeableProperty.setValue(newValue);
                        counter.getAndSet(-1);
                    });
                }
                Thread.sleep(sleeptime);
            }

            return null;
        }
    }

    public static void main(String[] args) {
        System.out.println(1.0/100*1000);
    }
}
Community
  • 1
  • 1
jewelsea
  • 150,031
  • 14
  • 366
  • 406
  • Many thanks for taking the time to resolve my issue, you are correct about my sleep time calculation. However the original issue of UI lag has been resolved by the code I have posted below. – abacusreader Jul 03 '14 at 04:01