1

Background:

A well-known Swing best-practice requirement is that code that interacts with the Swing framework must also execute in EDT (Event Dispatch Thread).

I thus changed my code to have my JFreeChart-based updates to run in EDT. However, a complete chart display task that usually took about 7 minutes to finish on a “normal” thread, become a several hours task when running in EDT!


What am I doing wrong? Did I misunderstood the Swing Concurrency lesson? Do I really have to run org.jfree.data.time.TimeSeries.addOrUpdate(date, double) inside EDT?

Please advise!


Details:

Clicking a Swing GUI button, my program triggers a time-consuming task. Basically, it reads a (large) file with pair-values (date, double) and then shows them by using the JFreeChart framework.

Because this is a time-consuming task, while reading and displaying data, a JProgreessBar shows user the progress status in foreground, while the chart is updated in background (user is still able to visually see every chart update, behind the progress bar).

This worked fine, until I decided to review the code to have my chart data being updated and displayed inside Swing EDT. Then, a complete task that usually took about 7 minutes to finish, started to take several hours to complete!


Here’s the list of threads I’m using:

1) Since the task is triggered by a Swing Button Listener, it is running in EDT. The JProgressBar is also running in this same thread;

2) While showing the JProgressBar, a second (“normal”) thread is created and executed in the background. This is where the heavy work is done.

It includes the update of the JProgressBar status on the other thread (by calling JProgressBar.setvalue()) and the update of my JFreeChart chart (by calling TimeSeries.addOrUpdate(date, double), which automatically updates a org.jfree.chart.ChartPanel).

Updating the chart in my second (“normal”) thread usually took about 7 minutes to finish. Without any noticeable issue.


However, knowing that most Swing object methods are not "thread safe" and ChartPanel is just a Swing GUI component for displaying a JFreeChart object, I decided to run my chart update code TimeSeries.addOrUpdate(date, double) inside EDT.

Still running in my second “normal” thread, I tested with the following asynchronous code:

javax.swing.SwingUtilities.invokeLater(new Runnable() {
    public void run() {
        TimeSeries.addOrUpdate(date, double);
    }
});

but I realized my JProgressBar would reach 100% much before the chart was updated. I guess this was expected as displaying chart data is much slower than getting and processing the data.

I then tried following synchronous code:

try {
    javax.swing.SwingUtilities.invokeAndWait(new Runnable() {
        public void run() {
            TimeSeries.addOrUpdate(date, double);
        }
    });
} catch (InvocationTargetException | InterruptedException e) {
    e.printStackTrace();
}   

And this is where I found the performance issue: now a complete task that used to take about 7 minutes to finish, started to take hours to complete!


So, my question is:

What am I doing wrong? Did I misunderstood the Swing Concurrency lesson? Do I really have to run TimeSeries.addOrUpdate(date, double) inside EDT?

Please advise!


UPDATE:
The complete code would be too large to show here, but you can find a code snapshot below.
Perhaps, the only thing noticeable about the code is that I use Reflection. This is because I use a generic ProgressBar Class that invokes in background whatever class I send it as an argument (though this is not clearly shown in the snapshot below).

//BUTTON LISTENER (EDT)
public void actionPerformed(ActionEvent arg0) {
    new Process_offline_data();
}


public Process_offline_data() {
    //GET DATA
    String[][] data = get_data_from_file();

    //CREATE PROGRESS BAR
    int maximum_progressBar = data.length;
    JProgressBar jpb = init_jpb(maximum_progressBar);

    //JDIALOG MODAL WINDOW
    JDialog jdialog = create_jdialog_window(jpb);


    Object class_to_invoke_obj = (Object) new Show_data_on_chart();
    String main_method_str = "do_heavy_staff"; 

    Runnable r = new Runnable() {
        public void run() {             

            //REFLECTION
            Method method = null;
            try {
                method = class_to_invoke_obj.getClass().getDeclaredMethod(main_method_str, JProgressBar.class, String[][].class);
            } catch (NoSuchMethodException | SecurityException e1) {
                e1.printStackTrace();
                jdialog.dispose();      //UNBLOCKS MAIN THREAD
                return;
            }

            try {
                method.invoke(class_to_invoke_obj, jpb, data);
            } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e1) {
                e1.printStackTrace();
                jdialog.dispose();      //UNBLOCKS MAIN THREAD
                return;
            }
            //----------------


            jdialog.dispose();      //UNBLOCKS MAIN THREAD
        }
    };
    new Thread(r).start();
    //----------------


    //THIS IS STILL EDT
    jdialog.setVisible(true);       //BLOCKS HERE UNTIL THE THREAD CALLS jdialog.dispose();
}



public class Show_data_on_chart {
    public void do_heavy_staff(JProgressBar jpb, String[][] data) {

        TimeSeries time_series = get_TimeSeries();      //JFreeChart datamodel

        int len = data.length;
        for (int i=0; i<len; i++) {
            jpb.setValue(i+1);

            Millisecond x_axys_millisecond = convert_str2date(data[i][0]);
            Double y_axys_double = convert_str2double(data[i][1]);

            try {
                javax.swing.SwingUtilities.invokeAndWait(new Runnable() {
                    public void run() {
                        //AUTOMATICALLY UPDATES org.jfree.chart.ChartPanel
                        time_series.addOrUpdate(x_axys_millisecond, y_axys_double);
                   }
                });
            } catch (InvocationTargetException | InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
caecilius
  • 55
  • 6
  • You should invoke your update on EDT but maybe you do it to often. If you have large set of data consider redrawing chart less often. – hal Feb 06 '20 at 10:43
  • @hal: Thks for your fast response, namely for confirming that my update should be invoked on EDT. However, it's hard to believe I'm redrawing too often... My sample, which includes 71571 points, takes almost 5 hours(!) to complete on EDT. And only a simple JProgressBar is running "simultaneously" on EDT. Besides, using a "regular" thread, it takes less than 7 minutes to complete! – caecilius Feb 06 '20 at 18:21
  • Do I understand correctly that with invokeLater application takes proper time to finish, and slows down after switching to invokeAndWait? – hal Feb 06 '20 at 19:29
  • Yes, that is correct. – caecilius Feb 06 '20 at 20:37
  • In fact, with invokeLater, it takes ~1 more minute to finish when compared with a "normal" thread (i.e., about 8 minutes). Still, a long way from the ~5 hours with invokeAndWait. – caecilius Feb 06 '20 at 20:40
  • Could You share main code for the app? Then I could run it and try to solve the issue. – hal Feb 06 '20 at 21:16
  • @hal Please see my update - Thanks! – caecilius Feb 06 '20 at 23:20

1 Answers1

1

This is how i solved the problem of updating the chart.

import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.data.xy.XYSeries;
import org.jfree.ui.ApplicationFrame;
import org.jfree.ui.RefineryUtilities;
import org.jfree.chart.plot.XYPlot;
import java.lang.reflect.InvocationTargetException;
import javax.swing.SwingUtilities;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.data.xy.XYSeriesCollection;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;

public class App extends ApplicationFrame {

    XYSeries sin = new XYSeries("Sin");

    public App(String applicationTitle, String chartTitle) {
        super(applicationTitle);
        JFreeChart xylineChart = ChartFactory.createXYLineChart(chartTitle, "X", "Y",                                                                                                 new XYSeriesCollection(sin),
            PlotOrientation.VERTICAL, false, true, false);

    ChartPanel chartPanel = new ChartPanel(xylineChart);
    chartPanel.setPreferredSize(new java.awt.Dimension(560, 367));
    final XYPlot plot = xylineChart.getXYPlot();

    XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer(true, false);
    plot.setRenderer(renderer);
    setContentPane(chartPanel);
}

public Runnable r = new Runnable() {
    double x, y;
    int i;

    public void run() {
        int steps = 69999;
        for (i = 0; i < steps; i++) {
            //sample plot data
            x = Math.PI * 2.0 * 10.0 / ((double) steps) * ((double) i);
            y = Math.sin(x);

            try {
                SwingUtilities.invokeAndWait(new Runnable() {
                    public void run() {
                        if ((i % 1000) == 0) {
                            //adding data and redrawing chart
                            sin.addOrUpdate(x, y);
                        } else {
                            //adding point without redrawing of the chart
                            sin.add(x, y, false);
                        }
                    }
                });
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //redrawing chart if all data loaded
        try {
            SwingUtilities.invokeAndWait(new Runnable() {
                public void run() {
                    sin.fireSeriesChanged();
                }
            });
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
};

public Runnable rupdate = new Runnable() {
    public void run() {
        while (true) {
            //redrawing chart
            try {
                SwingUtilities.invokeAndWait(new Runnable() {
                    public void run() {
                        sin.fireSeriesChanged();
                    }
                });
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //waiting for next update
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
};

public static void main(String[] args) {
    final App chart [] = new App[1];

    try {
        SwingUtilities.invokeAndWait(new Runnable() {
            public void run() {
                chart[0] = new App(null, null);
                chart[0].pack();
                RefineryUtilities.centerFrameOnScreen(chart[0]);
                chart[0].setVisible(true);
            }
        });
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    Thread job = new Thread(chart[0].r);
    job.start();
    Thread job2 = new Thread(chart[0].rupdate);
    job2.start();
}
}

The above code includes two solutions. You can use either of them or both. Chart can be updated during data feeding. For example every 100th point and after last poit. Eventually you can make external thread that updates chart after some time. I have used updateAndWait every time instead of updateLater.

In your code do not use reflections like that. You should make interface. For example:

public interface IHardWork {
    public void do_heavy_staff(JProgressBar jpb, String[][] data);
}

and implement it on every object that do the work:

public class Show_data_on_chart implements IHardWork {
    public void do_heavy_staff(JProgressBar jpb, String[][] data) {
        // TODO Auto-generated method stub
    }
}

then use it:

IHardWork hwObj = new Show_data_on_chart();
hwObj.do_heavy_staff(jpb, data);
hwObj = new OtherHWObj();
hwObj.do_heavy_staff(jpb, data);

Eventualy You can make a base class for it and use polymorphism.

hal
  • 831
  • 3
  • 13
  • 32
  • Thnks again! Let me analyze your answer and I will give some feedback on it (though most probably, not before tomorrow) – caecilius Feb 08 '20 at 15:50
  • Also consider `SwingWorker`, for [example](https://stackoverflow.com/a/13205322/230513). Note that `ApplicationFrame` should be constructed on the [event dispatch thread](http://docs.oracle.com/javase/tutorial/uiswing/concurrency/initial.html), too. – trashgod Feb 09 '20 at 10:16
  • Can I create App outside of EDT since it is not displayed yet? – hal Feb 09 '20 at 10:58
  • @hal: I general, no; more [here](https://stackoverflow.com/a/7158505/230513). – trashgod Feb 10 '20 at 18:48
  • I have changed creation of App to EDT. – hal Feb 11 '20 at 05:41
  • Sorry for late feedback, I've been involved in other priority issues... Anyway, I expect to get back to this one in next few days. Again, thank you all for your feedback! – caecilius Feb 18 '20 at 16:59
  • @hal Wow!! From about 7 minutes on a "normal" thread or about 5 hours(!) inside EDT to less than 30... seconds inside EDT, but now using your proposed solution!! You were right, I was redrawing too fast! Thank you! (I voted your solution up, but it seems that "Votes cast by those with less than 15 reputation are recorded, but do not change the publicly displayed post score.") – caecilius Feb 19 '20 at 22:42
  • @trashgod Thank you also for your feedback! – caecilius Feb 19 '20 at 22:46