001/*
002 * Copyright (c) 2015 The Regents of the University of California.
003 * All rights reserved.
004 *
005 * '$Author: crawl $'
006 * '$Date: 2015-11-02 20:02:37 +0000 (Mon, 02 Nov 2015) $' 
007 * '$Revision: 34199 $'
008 * 
009 * Permission is hereby granted, without written agreement and without
010 * license or royalty fees, to use, copy, modify, and distribute this
011 * software and its documentation for any purpose, provided that the above
012 * copyright notice and the following two paragraphs appear in all copies
013 * of this software.
014 *
015 * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
016 * FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
017 * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
018 * THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
019 * SUCH DAMAGE.
020 *
021 * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
022 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
023 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
024 * PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
025 * CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
026 * ENHANCEMENTS, OR MODIFICATIONS.
027 *
028 */
029package org.kepler.profiling.gui;
030
031import java.awt.Color;
032import java.awt.Component;
033import java.awt.Dimension;
034import java.awt.Font;
035import java.awt.GridBagConstraints;
036import java.awt.GridBagLayout;
037import java.awt.Insets;
038import java.awt.Point;
039import java.awt.event.ActionEvent;
040import java.awt.event.ActionListener;
041import java.awt.event.MouseAdapter;
042import java.awt.event.MouseEvent;
043import java.text.DateFormat;
044import java.text.SimpleDateFormat;
045import java.util.ArrayList;
046import java.util.Collections;
047import java.util.Date;
048import java.util.HashMap;
049import java.util.List;
050import java.util.Map;
051import java.util.concurrent.atomic.AtomicBoolean;
052
053import javax.swing.JButton;
054import javax.swing.JDialog;
055import javax.swing.JLabel;
056import javax.swing.JPopupMenu;
057import javax.swing.JScrollPane;
058import javax.swing.JTable;
059import javax.swing.JViewport;
060import javax.swing.SwingUtilities;
061import javax.swing.table.AbstractTableModel;
062import javax.swing.table.JTableHeader;
063import javax.swing.table.TableCellRenderer;
064
065import org.kepler.objectmanager.lsid.KeplerLSID;
066import org.kepler.provenance.FireState;
067import org.kepler.provenance.ProvenanceEnabledListener;
068import org.kepler.provenance.Recording;
069import org.kepler.provenance.RecordingException;
070import org.kepler.provenance.SimpleFiringRecording;
071
072import ptolemy.actor.Actor;
073import ptolemy.actor.CompositeActor;
074import ptolemy.actor.FiringEvent;
075import ptolemy.actor.IOPort;
076import ptolemy.actor.IOPortEvent;
077import ptolemy.actor.TypedIOPort;
078import ptolemy.actor.gui.PtolemyFrame;
079import ptolemy.domains.pn.kernel.PNDirector;
080import ptolemy.kernel.util.Nameable;
081import ptolemy.kernel.util.NamedObj;
082import ptolemy.util.MessageHandler;
083
084/** A panel that displays actor execution information. The execution
085 *  information is driven by calling methods in the _executionMonitorRecording
086 *  object. This class is abstract, and subclasses must instantiate
087 *  _executionMonitorRecording and set its container to be the workflow
088 *  to be monitored.
089 *  
090 *  @see Recording
091 * 
092 *  @author Daniel Crawl
093 *  @version $Id: ExecutionMonitorPanel.java 34199 2015-11-02 20:02:37Z crawl $
094 * 
095 */
096public abstract class ExecutionMonitorPanel extends ViewWatchingTabPane 
097    implements ProvenanceEnabledListener {
098
099    /** Create a new ExecutionMonitorPanel with the specified name. */
100    public ExecutionMonitorPanel(String name) {
101        super(new GridBagLayout());
102        _tabName = name;
103    }
104    
105    /** Initialize the tab. */
106    @Override
107    public void initializeTab() throws Exception {
108        
109        super.initializeTab();
110        
111        _workflow = (CompositeActor) ((PtolemyFrame)_frame).getModel();
112        
113        _executionTable = new JTable() {
114         
115            @Override
116            public Component prepareRenderer(TableCellRenderer renderer, int row, int column) {
117                Component component = super.prepareRenderer(renderer, row, column);
118                if(row == _selectedRow) {
119                    component.setBackground(Color.BLUE);
120                    component.setForeground(Color.WHITE);
121                } else {
122                    component.setBackground(_actorsList.get(row).status.getColor());
123                    component.setForeground(Color.BLACK);
124                }
125                Font font = component.getFont();
126                component.setFont(new Font(font.getName(), font.getStyle(), FONT_SIZE));
127                return component;
128            }            
129        };
130               
131        _executionTable.setPreferredScrollableViewportSize(new Dimension(500, 500));
132        
133        _executionTable.addMouseListener(new MouseAdapter() {
134            
135            @Override
136            public void mousePressed(MouseEvent mouseEvent) {
137                Point point = mouseEvent.getPoint();
138                _selectedRow = _executionTable.rowAtPoint(point);
139                
140                // see if it's a double click in the error column
141                if(mouseEvent.getClickCount() == 2 && 
142                    _executionTable.columnAtPoint(point) == Column.Message.ordinal()) {
143                    
144                    // display the error message in a dialog box if not empty
145                    String message = (String) _executionTableModel.getValueAt(_selectedRow, Column.Message.ordinal());
146                    if(message != null && !message.trim().isEmpty()) {
147                        MessageHandler.error(message);
148                    }
149                }
150            }
151            
152            @Override
153            public void mouseReleased(MouseEvent mouseEvent) {
154                Point point = mouseEvent.getPoint();
155                _selectedRow = _executionTable.rowAtPoint(point);
156            }
157        });
158
159        _executionTable.setRowHeight(20);
160        
161        _executionTableModel = new ExecutionMonitorTableModel();
162        _executionTable.setModel(_executionTableModel);
163        
164        _executionTable.setAutoCreateRowSorter(true);
165        
166        _initColumnSizes();
167                
168        _initContextMenu();
169        
170        JScrollPane scrollPane = new JScrollPane(_executionTable);
171        
172        scrollPane.setColumnHeader(new JViewport() {
173            
174            @Override
175            public Dimension getPreferredSize() {
176                Dimension dimension = super.getPreferredSize();
177                dimension.height = 30;
178                return dimension;
179            }
180            
181        });
182
183        scrollPane.getViewport().setBackground(Color.WHITE);
184
185        JTableHeader header = _executionTable.getTableHeader();
186        Font headerFont = header.getFont();
187        header.setFont(new Font(headerFont.getName(), headerFont.getStyle(), FONT_SIZE));
188
189        _replayButton = new JButton("Replay");
190        _replayButton.addActionListener(new ActionListener() {
191            @Override
192            public void actionPerformed(ActionEvent e) {
193                _replay();
194            }
195        });
196        _replayButton.setEnabled(false);
197
198        JButton configureButton = new JButton("Configure");
199        configureButton.addActionListener(new ActionListener() {
200            @Override
201            public void actionPerformed(ActionEvent e) {
202                JDialog dialog = 
203                        new ExecutionPanelConfigureDialog(ExecutionMonitorPanel.this,
204                                _isRunning.get());
205                dialog.pack();
206                dialog.setVisible(true);
207            }
208        });
209        
210        _statusLabel = new JLabel("");
211        
212        GridBagConstraints c = new GridBagConstraints();
213        c.anchor = GridBagConstraints.LINE_START;
214
215        c.weightx = 1;
216        c.weighty = 1;        
217        c.gridx = 0;
218        c.gridy = 0;
219        c.gridwidth = 3;
220        c.fill = GridBagConstraints.BOTH;
221        add(scrollPane, c);
222
223        c.weightx = 0;
224        c.weighty = 0;
225        c.fill = GridBagConstraints.NONE;
226
227        c.gridy = 1;
228        c.gridwidth = 1;
229        add(_replayButton, c);
230        
231        c.gridx = 1;
232        add(configureButton, c);
233        
234        c.gridx = 2;
235        c.gridwidth = GridBagConstraints.REMAINDER;
236        c.insets = new Insets(0, 10, 0, 0);
237        add(_statusLabel, c);
238
239        setBackground(Color.WHITE);   
240    }
241
242    /** Clean up resources since panel is being closed. */
243    @Override
244    public void removeNotify() {
245        super.removeNotify();
246        _actorsInfo.clear();
247        _actorsList.clear();
248        _executionMonitorRecording = null;
249    }
250    
251    /** This method is called when provenance is enabled or disabled.
252     * @param enabled True if provenance is enabled.
253     */
254    @Override
255    public void toggle(final boolean enabled) {
256        SwingUtilities.invokeLater(new Runnable() {
257            @Override
258            public void run() {
259                if(enabled) {
260                    _statusLabel.setText("");
261                } else {
262                    _statusLabel.setText("Turn on provenance to monitor execution.");
263                }
264            }
265        });
266    }
267
268    ///////////////////////////////////////////////////////////////////
269    ////                       protected methods                   ////
270
271    /** Get the ActorInfo for the selected row in the table. If no
272     *  row is selected, returns null.
273     */
274    protected ActorInfo _getSelectedRowInfo() {
275        if(_selectedRow > 0) {
276            return _actorsList.get(_selectedRow);
277        }
278        return null;
279    }
280    
281    /** Initialize the pop up menu for the execution table. */
282    protected void _initContextMenu() {    
283        _executionTableContextMenu = new JPopupMenu();
284        _executionTable.setComponentPopupMenu(_executionTableContextMenu);        
285    }
286
287    /** Replay an execution. */
288    abstract protected void _replay();
289    
290    ///////////////////////////////////////////////////////////////////
291    ////                       protected variables                 ////
292
293    /** The workflow object. */
294    protected CompositeActor _workflow;
295    
296    /** The recording class that updates the table. */
297    protected ExecutionMonitorRecording _executionMonitorRecording;
298
299    /** Status label at the bottom of the table. */
300    protected JLabel _statusLabel;
301
302    /** If true, the current time can be used to calculate the elapsed
303     *  execution time of an actor.
304     */
305    protected boolean _useCurrentTimeForElapsed = true;
306
307    /** Context menu for execution table. */
308    protected JPopupMenu _executionTableContextMenu;
309    
310    ///////////////////////////////////////////////////////////////////
311    ////                  package protected methods                ////
312
313    /** Get if executions for an actor should be separate rows. */
314    boolean getSeparateActorExecutions() {
315        return _separateActorExecutions;
316    }
317    
318    /** Get if executions for composite actors should be shown. */
319    boolean getShowCompositeActorExecutions() {
320        return _showCompositeActorExecutions;
321    }
322    
323    /** Get the frequency to update the table. */ 
324    int getUpdateDelay() {
325        return _updateFrequency;
326    }
327    
328    /** Set if executions for an actor should be separate rows. */
329    void setSeparateActorExecutions(boolean separate) {
330        _separateActorExecutions = separate;
331    }
332
333    /** Set if executions for composite actors should be shown. */
334    void setShowCompositeActorExecutions(boolean show) {
335        _showCompositeActorExecutions = show;
336    }
337    
338    /** Set the frequency to update the table. */ 
339    void setUpdateDelay(int delay) {
340        _updateFrequency = delay;
341    }
342    
343    ///////////////////////////////////////////////////////////////////
344    ////                       private methods                     ////
345    
346    /** Set column width in the execution table. */
347    private void _initColumnSizes() {
348        
349        _executionTable.getColumnModel()
350            .getColumn(Column.Actor.ordinal())
351            .setPreferredWidth(400);
352        
353        _executionTable.getColumnModel()
354            .getColumn(Column.ElapsedTime.ordinal())
355            .setPreferredWidth(100);
356
357        _executionTable.getColumnModel()
358            .getColumn(Column.ExecutionNumber.ordinal())
359            .setMaxWidth(55);
360
361        _executionTable.getColumnModel()
362            .getColumn(Column.Start.ordinal())
363            .setPreferredWidth(150);
364        
365        _executionTable.getColumnModel()
366            .getColumn(Column.End.ordinal())
367            .setPreferredWidth(150);
368
369        _executionTable.getColumnModel()
370            .getColumn(Column.Message.ordinal())
371            .setPreferredWidth(350);
372    }
373        
374    ///////////////////////////////////////////////////////////////////
375    ////                       inner classes                       ////
376
377    /** Table model for the execution table. */
378    private class ExecutionMonitorTableModel extends AbstractTableModel {
379        
380        /** Get the class of the value in the column. The class type
381         *  is used to correctly sort the column.
382         */
383        @Override
384        public Class<?> getColumnClass(int col) {
385            Class<?> clazz = Column.values()[col].getColumnClass();
386            if(clazz != null) {
387                return clazz;
388            }
389            return Object.class;
390        }
391        
392        /** Get the number of rows. */
393        @Override
394        public int getRowCount() {
395            return _actorsList.size();
396        }
397
398        /** Get the number of columns. */
399        @Override
400        public int getColumnCount() {
401            return Column.values().length;
402        }
403
404        /** Get the column header name. */
405        @Override
406        public String getColumnName(int col) {
407            return Column.values()[col].getName();
408        }
409
410        /** Get the value at a row and column. */
411        @Override
412        public Object getValueAt(int rowIndex, int columnIndex) {
413            ActorInfo info = _actorsList.get(rowIndex);
414            synchronized(info) {
415                return info.getColumn(columnIndex);
416            }
417        }
418    }
419    
420    /** A class to monitor workflow execution by receiving provenance events. */
421    protected class ExecutionMonitorRecording extends SimpleFiringRecording<Integer> {
422
423        public ExecutionMonitorRecording() throws RecordingException {
424            super();
425        }
426
427        /** Record an actor firing. */
428        @Override
429        public void actorFire(FiringEvent event, Date timestamp) throws RecordingException
430        {
431            Actor actor = event.getActor();
432            
433            //System.out.println(event);
434                        
435            // TODO if show composite actor executions turned on
436            // during workflow execution, actor info below is null and
437            // leads to NPE.
438            if(actor instanceof CompositeActor &&
439                    !_showCompositeActorExecutions) {
440                return;
441            }
442            
443            FiringEvent.FiringEventType curEventType = event.getType();
444            FireState<Integer> fireState = _fireStateTable.get(actor);
445            
446            if(fireState == null) {
447                System.err.println("WARNING: received actor fire event" +
448                        " for unregistered actor: " + actor.getFullName());
449                return;
450                /*
451                throw new RecordingException(
452                    "Received actor fire event for unregistered actor: " +
453                    actor.getFullName());
454                */
455            }
456
457            String actorName = actor.getName(_recordingWorkflow);
458            ActorInfo info = _actorsInfo.get(actorName);
459            
460            synchronized(fireState) {
461                // get the last type of firing start
462                FiringEvent.FiringEventType lastStartType =
463                    fireState.getLastStartFireType();
464
465                // see if current firing is new iteration:
466                // NOTE: PN does not report iterate firings so the iteration
467                // may begin with prefire if the last type of firing was not
468                // iterate.
469                if(curEventType == FiringEvent.BEFORE_ITERATE ||
470                    (curEventType == FiringEvent.BEFORE_PREFIRE &&
471                    lastStartType != FiringEvent.BEFORE_ITERATE)) {
472                    
473                    int firing = fireState.getNumberOfFirings() + 1;
474                    fireState.fireStart(curEventType, firing);
475
476                    boolean newActorInfo = false;
477                    if(info == null || 
478                            (_separateActorExecutions && firing > 0)) {
479                        info = new ActorInfo(actorName);
480                        info.useCurrentTimeForElapsed = _useCurrentTimeForElapsed;
481                        newActorInfo = true;
482                    }
483                    
484                    synchronized(info) {
485                        info.firing = firing;
486                        info.status = ActorStatus.Running;
487                        if(timestamp == null) {
488                            info.start = new Date();
489                        } else {
490                            info.start = timestamp;
491                        }
492    
493                        if(newActorInfo) {
494                            _actorsInfo.put(actorName, info);
495                            _actorsList.add(info);
496                        }
497                    }
498                    
499                    /*
500                    SwingUtilities.invokeLater(new Runnable() {
501                        @Override
502                        public void run() {
503                            _executionTableModel.fireTableDataChanged();
504                        }
505                    });
506                    */
507                }
508                // see if current firing is end of iteration:
509                else if(curEventType == FiringEvent.AFTER_ITERATE ||
510                    (curEventType == FiringEvent.AFTER_POSTFIRE &&
511                    lastStartType == FiringEvent.BEFORE_PREFIRE)) {
512                    // NOTE: if the type is a AFTER_POSTFIRE and last start
513                    // type is BEFORE_PREFIRE, we are running in PN.
514                    // in this case, tell the fireState to stop firing using
515                    // AFTER_PREFIRE instead of AFTER_POSTFIRE, since we never
516                    // told the fireState about the BEFORE_POSTFIRE.
517                    if(curEventType == FiringEvent.AFTER_POSTFIRE) {
518                        fireState.fireStop(FiringEvent.AFTER_PREFIRE);
519                    } else {
520                        fireState.fireStop(curEventType);
521                    }
522                    
523                    synchronized(info) {
524                        info.status = ActorStatus.Done;
525                        if(timestamp == null) {
526                            info.end = new Date();
527                        } else {
528                            info.end = timestamp;
529                        }
530                    }
531                    
532                    /*
533                    SwingUtilities.invokeLater(new Runnable() {
534                        @Override
535                        public void run() {
536                            _executionTableModel.fireTableDataChanged();
537                        }
538                    });
539                    */
540                }
541            }
542        }
543
544        /** The workflow had an error.
545        *
546        * @param source The source of the error. This may be null.
547        * @param throwable The error.
548        */
549        @Override
550        public void executionError(Nameable source, Throwable throwable)
551            throws RecordingException
552        {
553            _isFinished.set(true);
554            
555            if(source instanceof Actor) {
556                ActorInfo info;
557                if(source == _recordingWorkflow) {
558                    info = _actorsInfo.get("Workflow");
559                    if(info == null) {
560                        info = new ActorInfo("Workflow");
561                        info.useCurrentTimeForElapsed = _useCurrentTimeForElapsed;
562                        info.status = ActorStatus.Running;
563                        info.start = new Date();
564                        info.firing = 1;
565                        
566                        _actorsInfo.put("Workflow", info);
567                        _actorsList.add(info);
568                    }
569                } else {
570                    String actorName = ((Actor)source).getName(_recordingWorkflow);
571                    info = _actorsInfo.get(actorName);
572                    if(info == null) {
573                        info = new ActorInfo(actorName);
574                        info.useCurrentTimeForElapsed = _useCurrentTimeForElapsed;
575                        _actorsInfo.put(actorName, info);
576                        _actorsList.add(info);
577                    }
578                }
579                
580                synchronized(info) {
581                    info.status = ActorStatus.Error;
582                    info.message = throwable.getMessage();
583                }
584                
585                SwingUtilities.invokeLater(new Runnable() {
586                    @Override
587                    public void run() {
588                        _executionTableModel.fireTableDataChanged();
589                    }
590                });
591            }
592        }
593
594        /** Record the starting of workflow execution. */
595        @Override
596        public void executionStart(KeplerLSID executionLSID, Date timestamp) throws RecordingException
597        {           
598            _isFinished.set(false);
599
600            // create a row in the execution table for the workflow
601            ActorInfo info = new ActorInfo("Workflow");
602            info.useCurrentTimeForElapsed = _useCurrentTimeForElapsed;
603            info.status = ActorStatus.Running;
604            if(timestamp == null) {
605                info.start = new Date();
606            } else {
607                info.start = timestamp;
608            }
609            info.firing = 1;
610            
611            _actorsInfo.put("Workflow", info);
612            _actorsList.add(info);
613            
614            _isRunning.set(true);
615            
616            SwingUtilities.invokeLater(new Runnable() {
617                @Override
618                public void run() {
619                    _statusLabel.setText("Running.");
620                    _executionTableModel.fireTableDataChanged();
621                }
622            });
623            
624            // create a thread to periodically update the execution table
625            _updateThread = new Thread(new Runnable() {
626                @Override
627                public void run() {
628                    while(!_isFinished.get()) {
629                        synchronized(_updateLock) {
630                            try {
631                                _updateLock.wait(_updateFrequency);
632                            } catch (InterruptedException e) {
633                                MessageHandler.error("Interrupted while waiting for update lock.", e);;
634                            }
635                            SwingUtilities.invokeLater(new Runnable() {
636                                @Override
637                                public void run() {
638                                    _executionTableModel.fireTableDataChanged();
639                                }
640                            });
641                        }
642                    }
643                }
644            });
645            _updateThread.start();
646        }
647
648        /** Record the stopping of workflow execution. */
649        @Override
650        public void executionStop(KeplerLSID executionLSID, Date timestamp) throws RecordingException
651        {
652            _isFinished.set(true);
653
654            // stop all the actors.
655            synchronized(_actorsInfo) {
656                for(ActorInfo info : _actorsInfo.values()) {
657                    synchronized(info) {
658                        if(info.status == ActorStatus.Running ||
659                                info.status == ActorStatus.Waiting) {
660                            info.status = ActorStatus.Done;
661                            if(info.status != ActorStatus.Waiting) {
662                                if(timestamp == null) {
663                                    info.end = new Date();
664                                } else {
665                                    info.end = timestamp;
666                                }
667                            }
668                        }
669                    }
670                }
671            }
672                        
673            // wake up the updater thread and wait until it finishes.
674            synchronized(_updateLock) {
675                _updateLock.notify();
676            }
677            
678            try {
679                _updateThread.join();
680            } catch (InterruptedException e) {
681                MessageHandler.error("Error joining update thread.", e);
682            }
683            
684            _isRunning.set(false);
685
686            SwingUtilities.invokeLater(new Runnable() {
687                @Override
688                public void run() {
689                    _statusLabel.setText("Done.");
690                    _executionTableModel.fireTableDataChanged();
691                }
692            });
693            
694            _blockingPorts.clear();
695            
696            SwingUtilities.invokeLater(new Runnable() {
697                @Override
698                public void run() {
699                    _replayButton.setEnabled(true);
700                }
701            });
702            
703        }
704        
705        /** Record a port event. */
706        @Override
707        public void portEvent(IOPortEvent event, Date timestamp) throws RecordingException
708        {
709            String actorName = _blockingPorts.get(event.getPort());
710            ActorInfo info;
711            
712            // if actor name is not null, then reading the port blocks
713            // actor execution.
714            if(actorName != null) {
715                info = _actorsInfo.get(actorName);
716                
717                // info is null if we are not showing composite
718                // actor executions and the port is contained by
719                // a composite actor.
720                if(info != null) {
721
722                    switch(event.getEventType()) {
723                    case IOPortEvent.GET_BEGIN:
724                        synchronized(info) {
725                            info.status = ActorStatus.Waiting;
726                        }
727                        //System.out.println("waiting " + info._name);
728                        //System.out.flush();
729                        /*
730                        SwingUtilities.invokeLater(new Runnable() {
731                            @Override
732                            public void run() {
733                                _executionTableModel.fireTableDataChanged();
734                            }
735                        });
736                        */
737                        break;
738                    case IOPortEvent.GET_END:
739                        synchronized(info) {
740                            info.status = ActorStatus.Running;
741                        }
742                        //System.out.println("running " + info._name);
743                        //System.out.flush();
744                        /*
745                        SwingUtilities.invokeLater(new Runnable() {
746                            @Override
747                            public void run() {
748                                _executionTableModel.fireTableDataChanged();
749                            }
750                        });
751                        */
752                        break;
753                    default:
754                        // do nothing
755                        break;
756                    }
757                }
758            }
759        }
760                                        
761        /** Register a port. */
762        @Override 
763        public boolean regPort(TypedIOPort port) {
764            
765            // add the port to the set of blocking ports if the port
766            // is an input port and the executive director is PN.
767            // we check the executive director since if the actor is
768            // a composite actor, we want the outside director.
769            if(port.isInput() &&
770                    (port.getContainer() instanceof Actor) &&
771                    ((Actor)port.getContainer()).getExecutiveDirector() instanceof PNDirector) {                
772                _blockingPorts.put(port, ((Actor)port.getContainer()).getName(_recordingWorkflow));
773                //System.out.println("blocking ports added " + port.getFullName());
774            }
775            return false;
776        }
777        
778        /** Clear data structures before starting. */
779        @Override
780        public void specificationStart() throws RecordingException {
781            super.specificationStart();
782            _actorsInfo.clear();
783            _actorsList.clear();
784            _blockingPorts.clear();
785            _recordingWorkflow = _recorderContainer.toplevel();
786            SwingUtilities.invokeLater(new Runnable() {
787                @Override
788                public void run() {
789                    _replayButton.setEnabled(false);
790                }
791            });
792        }
793        
794        /** True when the workflow finishes. */
795        private final AtomicBoolean _isFinished = new AtomicBoolean(false);
796                        
797        /** A map of ports to actor names for input ports that block. */
798        private final Map<IOPort, String> _blockingPorts =
799                Collections.synchronizedMap(new HashMap<IOPort, String>());
800        
801        /** A thread that periodically updates the execution table. */
802        private Thread _updateThread;
803        
804        /** A lock for the update thread. */
805        private final Object _updateLock = new Object();
806        
807        /** The workflow. */
808        private NamedObj _recordingWorkflow;
809    }
810    
811    /** A utility class to hold information about a single
812     *  execution of an actor.
813     */
814    protected static class ActorInfo {
815        
816        /** Create a new ActorInfo with an actor's name. */
817        public ActorInfo(String name) {
818            this.name = name;
819        }
820        
821        /** Get the object for a column in the execution table. */
822        public Object getColumn(int index) {
823            switch(Column.values()[index]) {
824            case Actor:
825                //System.out.println("name = " + _name);
826                //System.out.flush();
827                return name;
828            case ElapsedTime:
829                if(status == ActorStatus.Done || status == ActorStatus.Error) {
830                    if(end == null || start == null) {
831                        return "-";
832                    }
833                    return String.format("%.2f",
834                        Double.valueOf(end.getTime() - start.getTime()) / _MILLI_SEC_IN_SEC);
835                } else if(start != null && useCurrentTimeForElapsed) {
836                    return String.format("%.2f",
837                        Double.valueOf(new Date().getTime() - start.getTime()) / _MILLI_SEC_IN_SEC);
838                } else {
839                    return "-";
840                }
841            case End:
842                if(end == null) {
843                    return "-";
844                }
845                return _dateFormat.format(end);
846            case ExecutionNumber:
847                return firing;
848            case Message:
849                return message;
850            /*
851            case Status:
852                //System.out.println("status = " + status);
853                //System.out.flush();
854                return status;
855            */
856            case Start:
857                if(start == null) {
858                    return "-";
859                }
860                return _dateFormat.format(start);
861            default:
862                System.out.println("WARNING: ActorInfo.getColumn for unknown index: " + index);
863                return null;
864            }
865        }
866        
867        @Override
868        public String toString() {
869            return name + " " + status.name();
870        }
871        
872        /** The time that execution started. */
873        //public long startTime = -1;
874        
875        /** The time that execution stopped. */
876        //public long endTime = -1;
877        
878        /** The timestamp that execution started. */
879        public Date start;
880
881        /** The timestamp that execution end. */
882        public Date end;
883        
884        /** The current firing number of the actor. */
885        public int firing = -1;
886        
887        /** The execution status. */
888        public ActorStatus status = ActorStatus.NotRun;
889        
890        /** An optional execution message */
891        public String message = "";
892        
893        /** If true, the current time can be used to calculate the elapsed
894         *  execution time of an actor.
895         */
896        public boolean useCurrentTimeForElapsed;
897        
898        /** The fully-qualified name of the actor (with no leading period.) */
899        public String name;
900    }
901    
902    /** The columns in the execution table. */
903    private enum Column { 
904        Actor("Actor"),
905        ElapsedTime("Elapsed", Integer.class),
906        ExecutionNumber("Num", Integer.class),
907        Start("Start"),
908        End("End"),
909        //Status("Status"),
910        Message("Message");
911    
912        Column(String name) {
913            _name = name;
914            _class = String.class;
915        }
916        
917        Column(String name, Class<?> clazz) {
918            _name = name;
919            _class = clazz;
920        }
921        
922        /** Get the class of the value in the column. */
923        public Class<?> getColumnClass() {
924            return _class;
925        }
926        
927        /** Get the name of the column */
928        public String getName() {
929            return _name;
930        }
931        
932        /** The column name. */
933        private String _name;
934        
935        /** The column class type. */
936        private Class<?> _class;
937        
938    };
939    
940    /** The actor execution status. */
941    private enum ActorStatus { 
942        Done,
943        Error,
944        Running,
945        NotRun,
946        Waiting;
947    
948        /** Get the color for the status. */
949        public Color getColor() {
950            switch(this) {
951            case Done:
952                return Color.LIGHT_GRAY;
953            case Error:
954                return Color.RED;
955            case NotRun:
956                return Color.WHITE;
957            case Running:
958                return Color.GREEN;
959            case Waiting:
960                return Color.YELLOW;
961            default:
962                return Color.WHITE;
963            }
964        }
965    
966    };
967    
968    ///////////////////////////////////////////////////////////////////
969    ////                       private variables                   ////
970    
971    /** If true, provenance is turned on. */
972    //private boolean _provenanceEnabled;
973    
974    /** The execution table. */
975    private JTable _executionTable;
976        
977    /** The execution table model. */
978    private AbstractTableModel _executionTableModel;
979        
980    private final Map<String,ActorInfo> _actorsInfo = 
981            Collections.synchronizedMap(new HashMap<String,ActorInfo>());
982    
983    private final List<ActorInfo> _actorsList =
984            Collections.synchronizedList(new ArrayList<ActorInfo>());
985    
986    /** If true, a new row is created for each actor firing in the execution table. */
987    private boolean _separateActorExecutions = true;
988
989    /** If true, a new row is created in the execution table for
990     *  each composite actor firing.
991     */
992    private boolean _showCompositeActorExecutions = false;
993
994    /** Index of selected row in the execution table. */
995    private int _selectedRow = -1;
996    
997    /** How often (in milliseconds) to update the execution table. */
998    private int _updateFrequency = 5000;
999    
1000    /** Timestamp format for execution table. */
1001    private final static DateFormat _dateFormat = new SimpleDateFormat("hh:mm:ss.S");
1002    
1003    /** Execution table font size for cell and column header. */
1004    public final static int FONT_SIZE = 14;
1005        
1006    /** Number of milliseconds in one second. */
1007    private final static double _MILLI_SEC_IN_SEC = 1000.0;
1008    
1009    /** Button for replay. */
1010    private JButton _replayButton;    
1011    
1012    /** True when the workflow is executing. */
1013    private AtomicBoolean _isRunning = new AtomicBoolean(false);
1014    
1015}