JavaFX line chart freeze after it got an error - Bug?

I'm using JavaFX with Gluon Mobile. Very good framework for creating mobile applications. But I have a JavaFX line chart that freeze after it got an error.

Exception in thread "JavaFX Application Thread"
java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
    at java.util.ArrayList$Itr.next(ArrayList.java:859)
    at java.util.Collections$UnmodifiableCollection$1.next(Collections.java:1042)
    at javafx.scene.chart.LineChart.layoutPlotChildren(LineChart.java:468)
    at javafx.scene.chart.XYChart.layoutChartChildren(XYChart.java:731)
    at javafx.scene.chart.Chart$1.layoutChildren(Chart.java:94)
    at javafx.scene.Parent.layout(Parent.java:1087)
    at javafx.scene.Parent.layout(Parent.java:1093)
    at javafx.scene.Parent.layout(Parent.java:1093)
    at javafx.scene.Parent.layout(Parent.java:1093)
    at javafx.scene.Scene.doLayoutPass(Scene.java:552)
    at javafx.scene.Scene$ScenePulseListener.pulse(Scene.java:2397)
    at com.sun.javafx.tk.Toolkit.lambda$runPulse$2(Toolkit.java:398)
    at java.security.AccessController.doPrivileged(Native Method)
    at com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:397)
    at com.sun.javafx.tk.Toolkit.firePulse(Toolkit.java:424)
    at com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:518)
    at com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:498)
    at com.sun.javafx.tk.quantum.QuantumToolkit.pulseFromQueue(QuantumToolkit.java:491)
    at com.sun.javafx.tk.quantum.QuantumToolkit.lambda$runToolkit$11(QuantumToolkit.java:319)
    at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
    at com.sun.glass.ui.gtk.GtkApplication._runLoop(Native Method)
    at com.sun.glass.ui.gtk.GtkApplication.lambda$null$5(GtkApplication.java:139)
    at java.lang.Thread.run(Thread.java:748)

The line chart looks like this.

enter image description here

But after a while, I can see dots after dots disapear slowly and then it's nothing there. It's like the Java FX line chart stopped to update. I have posted some comments where it breaks and what's really happens.

enter image description here

I'm using the line chart for real time logging. Here is the Java code.

                    /*
                     * Get the time format in HH:mm:ss
                     */
                    LocalDateTime now = LocalDateTime.now();
                    String time = dtf.format(now); 

                    if (countMeasurements < MEASUREMENTS) {
                        time_output.getData().add(new XYChart.Data<String, Number>(time, output));
                        time_input.getData().add(new XYChart.Data<String, Number>(time, input));
                        countMeasurements++;
                    } else {
                        /*
                         * Now insert
                         */
                        time_output.getData().add(new XYChart.Data<String, Number>(time, output)); // <-- No update after a while
                        time_input.getData().add(new XYChart.Data<String, Number>(time, input)); // <-- No update after a while
                        /*
                         * Delete the first object
                         */
                        time_output.getData().remove(0); // <--- This works
                        time_input.getData().remove(0); // <-- This works
                    }

Edit:

Here is how it looks when I got the error. The dots deletes more and more. You see that the line have disapear?

enter image description here

Whole code

package com.gluonapplication.thread;


import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import com.gluonhq.charm.glisten.mvc.View;
import de.re.easymodbus.modbusclient.ModbusClient;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.XYChart;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;

public class ModbusConnection extends Thread{

    /*
     * Static
     */
    private static boolean running;
    private static boolean start;

    /*
     * Fields from Scene Builder
     */
    private TextField statusTextField;
    private TextField ipAddressTextField;
    private ComboBox<String> startSignalComboBox;
    private TextField predictHorizonTextField;
    private TextField controlHorizonTextField;
    private TextField sampleTimeTextField;
    private TextField referencePointTextField;
    private TextField portTextField;
    private TextField slopeTextField;
    private TextField offsetTextField;
    private LineChart<String, Number> lineChart_primary;
    private Series<String, Number> time_output;
    private LineChart<String, Number> lineChart_third;
    private Series<String, Number> time_input;

    /*
     * Modbus
     */
    private ModbusClient modbusClient;
    private int MEASUREMENTS = 20;
    DateTimeFormatter dtf;
    int[] writeRegisters = new int[11];
    private int countMeasurements;

    @SuppressWarnings("unchecked")
    public ModbusConnection(View primaryView, View secondaryView, View thirdView) {
        /*
         * We start this thread as default
         */
        start = true; 

        /*
         * Initial modbus start
         */
        writeRegisters[6] = 0; 

        /*
         * For secondaryView
         */
        statusTextField = (TextField) secondaryView.lookup("#statusTextField");
        ipAddressTextField = (TextField) secondaryView.lookup("#ipAddressTextField");
        startSignalComboBox = (ComboBox<String>) secondaryView.lookup("#startSignalComboBox");
        predictHorizonTextField = (TextField) secondaryView.lookup("#predictHorizonTextField");
        controlHorizonTextField = (TextField) secondaryView.lookup("#controlHorizonTextField");
        sampleTimeTextField = (TextField) secondaryView.lookup("#sampleTimeTextField");
        referencePointTextField = (TextField) secondaryView.lookup("#referencePointTextField");
        portTextField = (TextField) secondaryView.lookup("#portTextField");
        slopeTextField = (TextField) secondaryView.lookup("#slopeTextField");
        offsetTextField = (TextField) secondaryView.lookup("#offsetTextField");

        /*
         * For primaryView
         */
        lineChart_primary = (LineChart<String, Number>) primaryView.lookup("#lineChart");

        /*
         * Declare the data object inside the chart
         */
        time_output = new Series<String, Number>();
        time_output.setName("Output");
        lineChart_primary.getData().add(time_output);

        /*
         * For thirdView
         */
        lineChart_third = (LineChart<String, Number>) thirdView.lookup("#lineChart");

        /*
         * Declare the data object inside the chart
         */
        time_input = new Series<String, Number>();
        time_input.setName("Input");
        lineChart_third.getData().add(time_input);


        /*
         * This will prevent so we don't get NullPointerException
         */
        modbusClient = null;

        /*
         * For time
         */
        dtf = DateTimeFormatter.ofPattern("HH:mm:ss");  

        /*
         * Reset 
         */
        countMeasurements = 0;
    }

    @Override
    public void run() {
        while (start) {
            while (running == true || writeRegisters[6] == 1) {

                /*
                 * Connect to Modbus server 
                 */
                if(modbusClient == null) {
                    modbusClient = new ModbusClient(ipAddressTextField.getText(),Integer.parseInt(portTextField.getText()));
                    try {
                        modbusClient.Connect();
                    } catch (Exception e) {
                        statusTextField.setText("Cannot connect");
                    }
                }else if(modbusClient.isConnected() == false){
                    modbusClient = new ModbusClient(ipAddressTextField.getText(),Integer.parseInt(portTextField.getText()));
                    try {
                        modbusClient.Connect();
                    } catch (Exception e) {
                        statusTextField.setText("Cannot connect");
                    }
                }

                /*
                 * Write registers at address 0
                 */
                try {                   

                    /*
                     * What start signal should we use. 
                     */
                    int mode = startSignalComboBox.getSelectionModel().getSelectedIndex();
                    switch (mode) {
                        case 0:
                            writeRegisters[0] = 255; // PWM 100%
                            break;
                        case 1:
                            writeRegisters[0] = 230; // PWM 90%
                            break;
                        case 2:
                            writeRegisters[0] = 204; // PWM 80%
                            break;
                        case 3:
                            writeRegisters[0] = 179; // PWM 70%
                            break;
                        case 4:
                            writeRegisters[0] = 153; // PWM 60%
                            break;
                        case 5:
                            writeRegisters[0] = 128; // PWM 50%
                            break;
                        case 6:
                            writeRegisters[0] = 102; // PWM 40%
                            break;
                        case 7:
                            writeRegisters[0] = 77; // PWM 30%
                            break;
                        case 8:
                            writeRegisters[0] = 51; // PWM 20%
                            break;
                        case 9:
                            writeRegisters[0] = 26; // PWM 10%
                            break;
                        default:
                            writeRegisters[0] = 255; // PWM 100%
                            break;
                    }

                    /*
                     * Get the prediction horizon as int
                     */
                    writeRegisters[1] = Integer.parseInt(predictHorizonTextField.getText());

                    /*
                     * Get the control horizon as int
                     */
                    writeRegisters[2] = Integer.parseInt(controlHorizonTextField.getText());

                    /*
                     * Get the sample time in as int
                     */
                    writeRegisters[3] = Integer.parseInt(sampleTimeTextField.getText()); 

                    /*
                     * Get the reference point in two ints
                     */
                    writeRegisters[4] = (int) Float.parseFloat(referencePointTextField.getText());  
                    writeRegisters[5] = (int) ((Float.parseFloat(referencePointTextField.getText()) - ((float) writeRegisters[4])) * 10000); 

                    /*
                     * Get if the system is running
                     */
                    writeRegisters[6] = running ? 1 : 0;

                    /*
                     * Get the slope
                     */
                    writeRegisters[7] = (int) Float.parseFloat(slopeTextField.getText());
                    writeRegisters[8] = (int) ((Float.parseFloat(slopeTextField.getText()) - ((float) writeRegisters[7])) * 10000); 

                    /*
                     * Get the offset
                     */
                    writeRegisters[9] = (int) Float.parseFloat(offsetTextField.getText());  
                    writeRegisters[10] = (int) ((Float.parseFloat(offsetTextField.getText()) - ((float) writeRegisters[9])) * 10000); 

                    /*
                     * Write 11 elements from address 0
                     */
                    modbusClient.WriteMultipleRegisters(0, writeRegisters);

                } catch (Exception e) {
                    statusTextField.setText("Cannot write");
                } 


                /*
                 * Read 3 registers at the beginning from address 12
                 */
                try {
                    int[] registersRead = modbusClient.ReadHoldingRegisters(12, 3); // two first are output (float) and the last is the input (int)

                    /*
                     * Get the output value 
                     */

                    System.out.println("registersRead[0] = " + registersRead[0] + " registersRead[1] = " + registersRead[1] );
                    double output = ((double) registersRead[0]) + ((double) registersRead[1]) / 10000.0;
                    System.out.println("Output : " + output);

                    /*
                     * Get the input value
                     */
                    int input = registersRead[2];

                    /*
                     * Get the time format in HH:mm:ss
                     */
                    LocalDateTime now = LocalDateTime.now();
                    String time = dtf.format(now); 
                    System.out.println("Time : " + time);

                    if (countMeasurements < MEASUREMENTS) {
                        time_output.getData().add(new XYChart.Data<String, Number>(time, output));
                        time_input.getData().add(new XYChart.Data<String, Number>(time, input));
                        countMeasurements++;
                    } else {
                        /*
                         * Now insert
                         */
                        System.out.println("Add time_output");
                        time_output.getData().add(new XYChart.Data<String, Number>(time, output));
                        System.out.println("Add time_input");
                        time_input.getData().add(new XYChart.Data<String, Number>(time, input));
                        /*
                         * Delete the first object
                         */
                        System.out.println("Delete");
                        time_output.getData().remove(0); 
                        time_input.getData().remove(0);
                    }

                } catch (Exception e) {
                    statusTextField.setText("Cannot read");
                } 


                try {
                    Thread.sleep((long) (1000 * Double.parseDouble(sampleTimeTextField.getText())));
                } catch (Exception e) {
                    statusTextField.setText("Cannot delay");
                }

                statusTextField.setText("Running");

            }

            /*
             * This is because we don't want update so fast
             */
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                statusTextField.setText("Cannot delay");
            }

            /*
             * Disconnect if we have been using modbusClient object before
             */
            if(modbusClient != null) {
                try {
                    if(modbusClient.isConnected()) {
                        modbusClient.Disconnect();
                        statusTextField.setText("Stopped");
                    }
                } catch (Exception e) {
                    statusTextField.setText("Cannot disconnect");
                }
            }
        }
    }

    public static boolean isRunning() {
        return running;
    }

    public static void setRunning(boolean running) {
        ModbusConnection.running = running;
    }

    public static boolean isStart() {
        return start;
    }

    public static void setStart(boolean start) {
        ModbusConnection.start = start;
    }

}

Edit 2: enter image description here

1 answer

  • answered 2019-03-20 12:17 José Pereda

    As explained in the comments, you are creating a background task:

    public class ModbusConnection extends Thread {
    
        @Override
        public void run() {
            while (start) {
                ...
            }
        }
    }
    

    and while that is perfectly fine for your background task, i.e. your modbus connection and communications, you should never do UI related tasks on it.

    A Chart is a JavaFX node, and when you add a new data point to the one of its series like this:

    public class ModbusConnection extends Thread {
    
        @Override
        public void run() {
            while (start) {
                ...
                time_output.getData().add(new XYChart.Data<String, Number>(time, output));
                ...               
            }
        }
    }
    

    that triggers a layout pass to render the related node, but that should be done only in the UI thread (the JavaFX Application thread).

    So as an initial fix, modify your code to do:

    public class ModbusConnection extends Thread {
    
        @Override
        public void run() {
            while (start) {
                ...
                Platform.runLater(() -> 
                    time_output.getData().add(new XYChart.Data<String, Number>(time, output)));
                ...               
            }
        }
    }
    

    Note that as @Slaw mentioned in the comments above, the JavaDoc for Platform::runLater says that:

    public static void runLater​(Runnable runnable)

    Run the specified Runnable on the JavaFX Application Thread at some unspecified time in the future. This method, which may be called from any thread, will post the Runnable to an event queue and then return immediately to the caller.

    So this looks exactly what we need in this case.

    But if you keep reading:

    NOTE: applications should avoid flooding JavaFX with too many pending Runnables. Otherwise, the application may become unresponsive. Applications are encouraged to batch up multiple operations into fewer runLater calls. Additionally, long-running operations should be done on a background thread where possible, freeing up the JavaFX Application Thread for GUI operations.

    So in a second step, you should try to batch all the calls from the background task, like:

    public class ModbusConnection extends Thread {
    
        @Override
        public void run() {
            while (start) {
                ...
                Platform.runLater(() -> {
                    time_output.getData().add(new XYChart.Data<String, Number>(time, output));
                    time_input.getData().add(new XYChart.Data<String, Number>(time, input));
                    if (time_output.getData().size() > MEASUREMENTS) {
                        time_output.getData().remove(0); 
                        time_input.getData().remove(0);
                    }
                });
                ...               
            }
        }
    }