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();
}
}
}
}
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.