Fix bug in simulation runner ThreadPoolExecutor
[debian/openrocket] / core / src / net / sf / openrocket / gui / main / SimulationRunDialog.java
1 package net.sf.openrocket.gui.main;
2
3
4 import java.awt.Dialog;
5 import java.awt.Dimension;
6 import java.awt.Window;
7 import java.awt.event.ActionEvent;
8 import java.awt.event.ActionListener;
9 import java.awt.event.WindowAdapter;
10 import java.awt.event.WindowEvent;
11 import java.util.Iterator;
12 import java.util.List;
13 import java.util.concurrent.Executors;
14 import java.util.concurrent.LinkedBlockingQueue;
15 import java.util.concurrent.ThreadFactory;
16 import java.util.concurrent.ThreadPoolExecutor;
17 import java.util.concurrent.TimeUnit;
18
19 import javax.swing.JButton;
20 import javax.swing.JDialog;
21 import javax.swing.JLabel;
22 import javax.swing.JOptionPane;
23 import javax.swing.JPanel;
24 import javax.swing.JProgressBar;
25
26 import net.miginfocom.swing.MigLayout;
27 import net.sf.openrocket.document.Simulation;
28 import net.sf.openrocket.gui.dialogs.DetailDialog;
29 import net.sf.openrocket.gui.util.GUIUtil;
30 import net.sf.openrocket.gui.util.SwingPreferences;
31 import net.sf.openrocket.l10n.Translator;
32 import net.sf.openrocket.logging.LogHelper;
33 import net.sf.openrocket.rocketcomponent.Configuration;
34 import net.sf.openrocket.rocketcomponent.MotorMount;
35 import net.sf.openrocket.rocketcomponent.MotorMount.IgnitionEvent;
36 import net.sf.openrocket.simulation.FlightEvent;
37 import net.sf.openrocket.simulation.SimulationStatus;
38 import net.sf.openrocket.simulation.exception.SimulationCancelledException;
39 import net.sf.openrocket.simulation.exception.SimulationException;
40 import net.sf.openrocket.simulation.exception.SimulationLaunchException;
41 import net.sf.openrocket.simulation.listeners.AbstractSimulationListener;
42 import net.sf.openrocket.simulation.listeners.SimulationListener;
43 import net.sf.openrocket.startup.Application;
44 import net.sf.openrocket.unit.Unit;
45 import net.sf.openrocket.unit.UnitGroup;
46 import net.sf.openrocket.util.MathUtil;
47
48
49 public class SimulationRunDialog extends JDialog {
50         private static final LogHelper log = Application.getLogger();
51         private static final Translator trans = Application.getTranslator();
52         
53         
54         /** Update the dialog status every this many ms */
55         private static final long UPDATE_MS = 200;
56         
57         /** Flight progress at motor burnout */
58         private static final double BURNOUT_PROGRESS = 0.4;
59         
60         /** Flight progress at apogee */
61         private static final double APOGEE_PROGRESS = 0.7;
62         
63         
64         /**
65          * A single ThreadPoolExecutor that will be used for all simulations.
66          * This executor must not be shut down.
67          */
68         private static final ThreadPoolExecutor executor;
69         static {
70                 int n = SwingPreferences.getMaxThreadCount();
71                 executor = new ThreadPoolExecutor(n, n,
72                                 0L, TimeUnit.MILLISECONDS,
73                                 new LinkedBlockingQueue<Runnable>(),
74                                 new ThreadFactory() {
75                                         private ThreadFactory factory = Executors.defaultThreadFactory();
76                                         
77                                         @Override
78                                         public Thread newThread(Runnable r) {
79                                                 Thread t = factory.newThread(r);
80                                                 t.setDaemon(true);
81                                                 return t;
82                                         }
83                                 });
84         }
85         
86         
87         
88         private final JLabel simLabel, timeLabel, altLabel, velLabel;
89         private final JProgressBar progressBar;
90         
91         
92         /*
93          * NOTE:  Care must be used when accessing the simulation parameters, since they
94          * are being run in another thread.  Mutexes are used to avoid concurrent usage, which
95          * will result in an exception being thrown!
96          */
97         private final Simulation[] simulations;
98         private final String[] simulationNames;
99         private final SimulationWorker[] simulationWorkers;
100         private final SimulationStatus[] simulationStatuses;
101         private final double[] simulationMaxAltitude;
102         private final double[] simulationMaxVelocity;
103         private final boolean[] simulationDone;
104         
105         public SimulationRunDialog(Window window, Simulation... simulations) {
106                 //// Running simulations...
107                 super(window, trans.get("SimuRunDlg.title.RunSim"), Dialog.ModalityType.APPLICATION_MODAL);
108                 
109                 if (simulations.length == 0) {
110                         throw new IllegalArgumentException("Called with no simulations to run");
111                 }
112                 
113                 this.simulations = simulations;
114                 
115                 
116                 // Randomize the simulation random seeds
117                 for (Simulation sim : simulations) {
118                         sim.getOptions().randomizeSeed();
119                 }
120                 
121                 // Initialize the simulations
122                 int n = simulations.length;
123                 simulationNames = new String[n];
124                 simulationWorkers = new SimulationWorker[n];
125                 simulationStatuses = new SimulationStatus[n];
126                 simulationMaxAltitude = new double[n];
127                 simulationMaxVelocity = new double[n];
128                 simulationDone = new boolean[n];
129                 
130                 for (int i = 0; i < n; i++) {
131                         simulationNames[i] = simulations[i].getName();
132                         simulationWorkers[i] = new InteractiveSimulationWorker(simulations[i], i);
133                         executor.execute(simulationWorkers[i]);
134                 }
135                 
136                 // Build the dialog
137                 JPanel panel = new JPanel(new MigLayout("fill", "[][grow]"));
138                 
139                 //// Running ...
140                 simLabel = new JLabel(trans.get("SimuRunDlg.lbl.Running"));
141                 panel.add(simLabel, "spanx, wrap para");
142                 //// Simulation time: 
143                 panel.add(new JLabel(trans.get("SimuRunDlg.lbl.Simutime") + " "), "gapright para");
144                 timeLabel = new JLabel("");
145                 panel.add(timeLabel, "growx, wmin 200lp, wrap rel");
146                 
147                 //// Altitude:
148                 panel.add(new JLabel(trans.get("SimuRunDlg.lbl.Altitude") + " "));
149                 altLabel = new JLabel("");
150                 panel.add(altLabel, "growx, wrap rel");
151                 
152                 //// Velocity:
153                 panel.add(new JLabel(trans.get("SimuRunDlg.lbl.Velocity") + " "));
154                 velLabel = new JLabel("");
155                 panel.add(velLabel, "growx, wrap para");
156                 
157                 progressBar = new JProgressBar();
158                 panel.add(progressBar, "spanx, growx, wrap para");
159                 
160                 
161                 // Add cancel button
162                 JButton cancel = new JButton(trans.get("dlg.but.cancel"));
163                 cancel.addActionListener(new ActionListener() {
164                         @Override
165                         public void actionPerformed(ActionEvent e) {
166                                 cancelSimulations();
167                         }
168                 });
169                 panel.add(cancel, "spanx, tag cancel");
170                 
171                 
172                 // Cancel simulations when user closes the window
173                 this.addWindowListener(new WindowAdapter() {
174                         @Override
175                         public void windowClosing(WindowEvent e) {
176                                 cancelSimulations();
177                         }
178                 });
179                 
180                 
181                 this.add(panel);
182                 this.setMinimumSize(new Dimension(300, 0));
183                 this.setLocationByPlatform(true);
184                 this.validate();
185                 this.pack();
186                 
187                 GUIUtil.setDisposableDialogOptions(this, null);
188                 
189                 updateProgress();
190         }
191         
192         
193         /**
194          * Cancel the currently running simulations.  This is equivalent to clicking
195          * the Cancel button on the dialog.
196          */
197         public void cancelSimulations() {
198                 for (SimulationWorker w : simulationWorkers) {
199                         w.cancel(true);
200                 }
201                 executor.purge();
202         }
203         
204         
205         /**
206          * Static helper method to run simulations.
207          * 
208          * @param parent                the parent Window of the dialog to use.
209          * @param simulations   the simulations to run.
210          */
211         public static void runSimulations(Window parent, Simulation... simulations) {
212                 new SimulationRunDialog(parent, simulations).setVisible(true);
213         }
214         
215         
216         
217         
218         private void updateProgress() {
219                 int index;
220                 for (index = 0; index < simulations.length; index++) {
221                         if (!simulationDone[index])
222                                 break;
223                 }
224                 
225                 if (index >= simulations.length) {
226                         // Everything is done, close the dialog
227                         log.debug("Everything done.");
228                         this.dispose();
229                         return;
230                 }
231                 
232                 // Update the progress bar status
233                 int progress = 0;
234                 for (SimulationWorker s : simulationWorkers) {
235                         progress += s.getProgress();
236                 }
237                 progress /= simulationWorkers.length;
238                 progressBar.setValue(progress);
239                 log.debug("Progressbar value " + progress);
240                 
241                 // Update the simulation fields
242                 simLabel.setText("Running " + simulationNames[index]);
243                 if (simulationStatuses[index] == null) {
244                         log.debug("No simulation status data available, setting empty labels");
245                         timeLabel.setText("");
246                         altLabel.setText("");
247                         velLabel.setText("");
248                         return;
249                 }
250                 
251                 Unit u = UnitGroup.UNITS_FLIGHT_TIME.getDefaultUnit();
252                 timeLabel.setText(u.toStringUnit(simulationStatuses[index].getSimulationTime()));
253                 
254                 u = UnitGroup.UNITS_DISTANCE.getDefaultUnit();
255                 altLabel.setText(u.toStringUnit(simulationStatuses[index].getRocketPosition().z) + " (max. " +
256                                 u.toStringUnit(simulationMaxAltitude[index]) + ")");
257                 
258                 u = UnitGroup.UNITS_VELOCITY.getDefaultUnit();
259                 velLabel.setText(u.toStringUnit(simulationStatuses[index].getRocketVelocity().z) + " (max. " +
260                                 u.toStringUnit(simulationMaxVelocity[index]) + ")");
261         }
262         
263         
264         
265         /**
266          * A SwingWorker that performs a flight simulation.  It periodically updates the
267          * simulation statuses of the parent class and calls updateProgress().
268          * The progress of the simulation is stored in the progress property of the
269          * SwingWorker.
270          * 
271          * @author Sampo Niskanen <sampo.niskanen@iki.fi>
272          */
273         private class InteractiveSimulationWorker extends SimulationWorker {
274                 
275                 private final int index;
276                 private final double burnoutTimeEstimate;
277                 private volatile double burnoutVelocity;
278                 private volatile double apogeeAltitude;
279                 
280                 /*
281                  * -2 = time from 0 ... burnoutTimeEstimate
282                  * -1 = velocity from v(burnoutTimeEstimate) ... 0
283                  *  0 ... n = stages from alt(max) ... 0
284                  */
285                 private volatile int simulationStage = -2;
286                 
287                 private int progress = 0;
288                 
289                 
290                 public InteractiveSimulationWorker(Simulation sim, int index) {
291                         super(sim);
292                         this.index = index;
293                         
294                         // Calculate estimate of motor burn time
295                         double launchBurn = 0;
296                         double otherBurn = 0;
297                         Configuration config = simulation.getConfiguration();
298                         String id = simulation.getOptions().getMotorConfigurationID();
299                         Iterator<MotorMount> iterator = config.motorIterator();
300                         while (iterator.hasNext()) {
301                                 MotorMount m = iterator.next();
302                                 if (m.getIgnitionEvent() == IgnitionEvent.LAUNCH)
303                                         launchBurn = MathUtil.max(launchBurn, m.getMotor(id).getBurnTimeEstimate());
304                                 else
305                                         otherBurn = otherBurn + m.getMotor(id).getBurnTimeEstimate();
306                         }
307                         burnoutTimeEstimate = Math.max(launchBurn + otherBurn, 0.1);
308                         
309                 }
310                 
311                 
312                 /**
313                  * Return the extra listeners to use, a progress listener and cancel listener.
314                  */
315                 @Override
316                 protected SimulationListener[] getExtraListeners() {
317                         return new SimulationListener[] { new SimulationProgressListener() };
318                 }
319                 
320                 
321                 /**
322                  * Processes simulation statuses published by the simulation listener.
323                  * The statuses of the parent class and the progress property are updated.
324                  */
325                 @Override
326                 protected void process(List<SimulationStatus> chunks) {
327                         
328                         // Update max. altitude and velocity
329                         for (SimulationStatus s : chunks) {
330                                 simulationMaxAltitude[index] = Math.max(simulationMaxAltitude[index],
331                                                 s.getRocketPosition().z);
332                                 simulationMaxVelocity[index] = Math.max(simulationMaxVelocity[index],
333                                                 s.getRocketVelocity().length());
334                         }
335                         
336                         // Calculate the progress
337                         SimulationStatus status = chunks.get(chunks.size() - 1);
338                         simulationStatuses[index] = status;
339                         
340                         // 1. time = 0 ... burnoutTimeEstimate
341                         if (simulationStage == -2 && status.getSimulationTime() < burnoutTimeEstimate) {
342                                 log.debug("Method 1:  t=" + status.getSimulationTime() + "  est=" + burnoutTimeEstimate);
343                                 setSimulationProgress(MathUtil.map(status.getSimulationTime(), 0, burnoutTimeEstimate,
344                                                 0.0, BURNOUT_PROGRESS));
345                                 updateProgress();
346                                 return;
347                         }
348                         
349                         if (simulationStage == -2) {
350                                 simulationStage++;
351                                 burnoutVelocity = MathUtil.max(status.getRocketVelocity().z, 0.1);
352                                 log.debug("CHANGING to Method 2, vel=" + burnoutVelocity);
353                         }
354                         
355                         // 2. z-velocity from burnout velocity to zero
356                         if (simulationStage == -1 && status.getRocketVelocity().z >= 0) {
357                                 log.debug("Method 2:  vel=" + status.getRocketVelocity().z + " burnout=" + burnoutVelocity);
358                                 setSimulationProgress(MathUtil.map(status.getRocketVelocity().z, burnoutVelocity, 0,
359                                                 BURNOUT_PROGRESS, APOGEE_PROGRESS));
360                                 updateProgress();
361                                 return;
362                         }
363                         
364                         if (simulationStage == -1 && status.getRocketVelocity().z < 0) {
365                                 simulationStage++;
366                                 apogeeAltitude = MathUtil.max(status.getRocketPosition().z, 1);
367                                 log.debug("CHANGING to Method 3, apogee=" + apogeeAltitude);
368                         }
369                         
370                         // 3. z-position from apogee to zero
371                         // TODO: MEDIUM: several stages
372                         log.debug("Method 3:  alt=" + status.getRocketPosition().z + "  apogee=" + apogeeAltitude);
373                         setSimulationProgress(MathUtil.map(status.getRocketPosition().z,
374                                         apogeeAltitude, 0, APOGEE_PROGRESS, 1.0));
375                         updateProgress();
376                 }
377                 
378                 /**
379                  * Marks this simulation as done and calls the progress update.
380                  */
381                 @Override
382                 protected void simulationDone() {
383                         simulationDone[index] = true;
384                         log.debug("Simulation done");
385                         setSimulationProgress(1.0);
386                         updateProgress();
387                 }
388                 
389                 
390                 /**
391                  * Marks the simulation as done and shows a dialog presenting
392                  * the error, unless the simulation was cancelled.
393                  */
394                 @Override
395                 protected void simulationInterrupted(Throwable t) {
396                         
397                         if (t instanceof SimulationCancelledException) {
398                                 simulationDone();
399                                 return; // Ignore cancellations
400                         }
401                         
402                         // Analyze the exception type
403                         if (t instanceof SimulationLaunchException) {
404                                 
405                                 DetailDialog.showDetailedMessageDialog(SimulationRunDialog.this,
406                                                 new Object[] {
407                                                                 //// Unable to simulate:
408                                                                 trans.get("SimuRunDlg.msg.Unabletosim"),
409                                                                 t.getMessage()
410                                                 },
411                                                 null, simulation.getName(), JOptionPane.ERROR_MESSAGE);
412                                 
413                         } else if (t instanceof SimulationException) {
414                                 
415                                 DetailDialog.showDetailedMessageDialog(SimulationRunDialog.this,
416                                                 new Object[] {
417                                                                 //// A error occurred during the simulation:
418                                                                 trans.get("SimuRunDlg.msg.errorOccurred"),
419                                                                 t.getMessage()
420                                                 },
421                                                 null, simulation.getName(), JOptionPane.ERROR_MESSAGE);
422                                 
423                         } else {
424                                 
425                                 Application.getExceptionHandler().handleErrorCondition("An exception occurred during the simulation", t);
426                                 
427                         }
428                         simulationDone();
429                 }
430                 
431                 
432                 private void setSimulationProgress(double p) {
433                         int exact = Math.max(progress, (int) (100 * p + 0.5));
434                         progress = MathUtil.clamp(exact, 0, 100);
435                         log.debug("Setting progress to " + progress + " (real " + exact + ")");
436                         super.setProgress(progress);
437                 }
438                 
439                 
440                 /**
441                  * A simulation listener that regularly updates the progress property of the 
442                  * SimulationWorker and publishes the simulation status for the run dialog to process.
443                  * 
444                  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
445                  */
446                 private class SimulationProgressListener extends AbstractSimulationListener {
447                         private long time = 0;
448                         
449                         @Override
450                         public boolean handleFlightEvent(SimulationStatus status, FlightEvent event) {
451                                 switch (event.getType()) {
452                                 case APOGEE:
453                                         simulationStage = 0;
454                                         apogeeAltitude = status.getRocketPosition().z;
455                                         log.debug("APOGEE, setting progress");
456                                         setSimulationProgress(APOGEE_PROGRESS);
457                                         publish(status);
458                                         break;
459                                 
460                                 case LAUNCH:
461                                         publish(status);
462                                         break;
463                                 
464                                 case SIMULATION_END:
465                                         log.debug("END, setting progress");
466                                         setSimulationProgress(1.0);
467                                         break;
468                                 }
469                                 return true;
470                         }
471                         
472                         @Override
473                         public void postStep(SimulationStatus status) {
474                                 if (System.currentTimeMillis() >= time + UPDATE_MS) {
475                                         time = System.currentTimeMillis();
476                                         publish(status);
477                                 }
478                         }
479                 }
480         }
481 }