From 304abf8bee123ee0d3f64b249ddc379d45f458d5 Mon Sep 17 00:00:00 2001
From: jmpenn <john.m.penn@nasa.gov>
Date: Tue, 18 Feb 2025 11:48:39 -0600
Subject: [PATCH] Frame Performance Tool- jperf (#1785)

* Initial Commit of Frame Performance - jperf

* Fix KeyStroke.getKeyStroke call.

* Separate reading of file from processing it.

* Refactor JobPerf.java to clean up names and organization.

* Add statistics reporting to jperf.

* Fix min/max issue, and rename types and variables for clarity.

* Break up and document the classes that compose JobPerf.

* Frame range selection and speed improvements.

* Indicate selected frame

* Added Jobs-stats and, Frame-details windows, job names,classes, contained jobs and so forth.

* Refactor timeline log and improve frame boundary determination.

* Fix check of whether one job executes within the bounds of another.

* Handle case where and id in the timeline is not found in the S-job_execution file.

* Add Trick Logo and Buttons that will be activated in the near future.

* Add functionality for advance retreat buttons.

* Fix replacement of minIndex with 0.
---
 bin/trick-jperf                               |   8 +
 include/trick/FrameLog.hh                     |   2 +
 include/trick/JobData.hh                      |   6 +
 trick_source/java/pom.xml                     |  16 +
 .../main/java/trick/jobperf/FrameRecord.java  |  76 +++
 .../java/trick/jobperf/FrameViewCanvas.java   |  99 ++++
 .../java/trick/jobperf/FrameViewWindow.java   |  31 ++
 .../jobperf/InvalidFrameBoundsExpection.java  |  12 +
 .../java/trick/jobperf/JobExecutionEvent.java |  64 +++
 .../src/main/java/trick/jobperf/JobPerf.java  | 202 ++++++++
 .../java/trick/jobperf/JobSpecification.java  |  33 ++
 .../trick/jobperf/JobSpecificationMap.java    |  69 +++
 .../src/main/java/trick/jobperf/JobStats.java | 198 ++++++++
 .../trick/jobperf/JobStatsViewCanvas.java     |  99 ++++
 .../trick/jobperf/JobStatsViewWindow.java     | 113 +++++
 .../java/trick/jobperf/KeyedColorMap.java     | 125 +++++
 .../main/java/trick/jobperf/RunRegistry.java  |  73 +++
 .../java/trick/jobperf/StatisticsRecord.java  |  28 ++
 .../src/main/java/trick/jobperf/TimeSpan.java |  24 +
 .../java/trick/jobperf/TraceViewCanvas.java   | 456 ++++++++++++++++++
 .../trick/jobperf/TraceViewInputToolBar.java  | 176 +++++++
 .../java/trick/jobperf/TraceViewMenuBar.java  |  98 ++++
 .../trick/jobperf/TraceViewOutputToolBar.java | 104 ++++
 .../java/trick/jobperf/TraceViewWindow.java   |  65 +++
 .../sim_services/FrameLog/FrameLog.cpp        |  50 +-
 25 files changed, 2222 insertions(+), 5 deletions(-)
 create mode 100755 bin/trick-jperf
 create mode 100644 trick_source/java/src/main/java/trick/jobperf/FrameRecord.java
 create mode 100644 trick_source/java/src/main/java/trick/jobperf/FrameViewCanvas.java
 create mode 100644 trick_source/java/src/main/java/trick/jobperf/FrameViewWindow.java
 create mode 100644 trick_source/java/src/main/java/trick/jobperf/InvalidFrameBoundsExpection.java
 create mode 100644 trick_source/java/src/main/java/trick/jobperf/JobExecutionEvent.java
 create mode 100644 trick_source/java/src/main/java/trick/jobperf/JobPerf.java
 create mode 100644 trick_source/java/src/main/java/trick/jobperf/JobSpecification.java
 create mode 100644 trick_source/java/src/main/java/trick/jobperf/JobSpecificationMap.java
 create mode 100644 trick_source/java/src/main/java/trick/jobperf/JobStats.java
 create mode 100644 trick_source/java/src/main/java/trick/jobperf/JobStatsViewCanvas.java
 create mode 100644 trick_source/java/src/main/java/trick/jobperf/JobStatsViewWindow.java
 create mode 100644 trick_source/java/src/main/java/trick/jobperf/KeyedColorMap.java
 create mode 100644 trick_source/java/src/main/java/trick/jobperf/RunRegistry.java
 create mode 100644 trick_source/java/src/main/java/trick/jobperf/StatisticsRecord.java
 create mode 100644 trick_source/java/src/main/java/trick/jobperf/TimeSpan.java
 create mode 100644 trick_source/java/src/main/java/trick/jobperf/TraceViewCanvas.java
 create mode 100644 trick_source/java/src/main/java/trick/jobperf/TraceViewInputToolBar.java
 create mode 100644 trick_source/java/src/main/java/trick/jobperf/TraceViewMenuBar.java
 create mode 100644 trick_source/java/src/main/java/trick/jobperf/TraceViewOutputToolBar.java
 create mode 100644 trick_source/java/src/main/java/trick/jobperf/TraceViewWindow.java

diff --git a/bin/trick-jperf b/bin/trick-jperf
new file mode 100755
index 00000000..7fb8d346
--- /dev/null
+++ b/bin/trick-jperf
@@ -0,0 +1,8 @@
+#!/usr/bin/perl
+
+use FindBin qw($RealBin);
+use lib ("$RealBin/../libexec/trick/pm", "$RealBin/../lib/trick/pm") ;
+use launch_java ;
+
+launch_java("JPERF", "JPerf") ;
+
diff --git a/include/trick/FrameLog.hh b/include/trick/FrameLog.hh
index edff7600..35f51954 100644
--- a/include/trick/FrameLog.hh
+++ b/include/trick/FrameLog.hh
@@ -20,6 +20,8 @@ namespace Trick {
     /** Data to save for each timeline sample.\n */
     struct timeline_t {
         bool trick_job;
+        bool isEndOfFrame;
+        bool isTopOfFrame;
         double id;
         long long start;
         long long stop;
diff --git a/include/trick/JobData.hh b/include/trick/JobData.hh
index 8ad72103..3c5fde62 100644
--- a/include/trick/JobData.hh
+++ b/include/trick/JobData.hh
@@ -47,6 +47,12 @@ namespace Trick {
             /** Indicates if a scheduler is handling this job */
             bool handled;                   /**< trick_units(--) */
 
+            /** Indicates whether this is an "top_of_frame" job. */
+            bool isTopOfFrame;              /**< trick_units(--) */
+
+            /** Indicates whether this is an "end_of_frame" job. */
+            bool isEndOfFrame;              /**< trick_units(--) */
+
             /** The cycle time */
             double cycle;                   /**< trick_units(s) */
 
diff --git a/trick_source/java/pom.xml b/trick_source/java/pom.xml
index ee69706c..c9cecb6a 100644
--- a/trick_source/java/pom.xml
+++ b/trick_source/java/pom.xml
@@ -282,6 +282,22 @@
               <finalName>MM</finalName>
             </configuration>
           </execution>
+          <execution>
+
+            <id>jobperf</id>
+            <phase>package</phase>
+            <goals>
+              <goal>shade</goal>
+            </goals>
+            <configuration>
+              <transformers>
+                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+                  <mainClass>trick.jobperf.JobPerf</mainClass>
+                </transformer>
+              </transformers>
+              <finalName>JPerf</finalName>
+            </configuration>
+          </execution>
 
         </executions>
       </plugin> 
diff --git a/trick_source/java/src/main/java/trick/jobperf/FrameRecord.java b/trick_source/java/src/main/java/trick/jobperf/FrameRecord.java
new file mode 100644
index 00000000..fd92e4f4
--- /dev/null
+++ b/trick_source/java/src/main/java/trick/jobperf/FrameRecord.java
@@ -0,0 +1,76 @@
+package trick.jobperf;
+import java.util.*;
+
+/**
+* Class CompareByDuration compares two JobExecutionEvent's by their duration.
+*/
+class CompareByDuration implements Comparator<JobExecutionEvent> {
+    public int compare(JobExecutionEvent a, JobExecutionEvent b) {
+        Double dur_a = a.stop - a.start;
+        Double dur_b = b.stop - b.start;
+        if ( dur_a > dur_b) return -1;
+        if ( dur_a < dur_b) return  1;
+        return 0;
+    }
+}
+
+/**
+* Class CompareByDuration compares two JobExecutionEvent's by their start time.
+*/
+class CompareByStartTime implements Comparator<JobExecutionEvent> {
+    public int compare(JobExecutionEvent a, JobExecutionEvent b) {
+        if ( a.start < b.start) return -1;
+        if ( a.start > a.start) return  1;
+        return 0;
+    }
+}
+
+/**
+* Class FrameRecord represents the set of jobs that have been executed during a
+* frame.
+*/
+public class FrameRecord {
+    public ArrayList<JobExecutionEvent> jobEvents;
+    public double start;
+    public double stop;
+    /**
+     * Constructor
+     */
+    public FrameRecord() {
+        start = 0.0;
+        stop  = 0.0;
+        jobEvents = new ArrayList<JobExecutionEvent>();
+    }
+
+    /**
+     * @return the stop time minus the start time.
+     */
+    public double getDuration() {
+        return stop - start;
+    }
+
+    public void SortByJobEventDuration() {
+        Collections.sort( jobEvents, new CompareByDuration());
+    }
+
+    public void SortByStartTime() {
+        Collections.sort( jobEvents, new CompareByStartTime());
+    }
+
+    /**
+     * For each jobEvent in the frame, record the number of times
+     * its start time is contained within
+     * another jobs stop/stop range.
+     */
+    public void CalculateJobContainment() {
+        SortByJobEventDuration();
+        int N = jobEvents.size();
+        for (int i = 0 ; i < (N-1); i++) {
+            for (int j = i+1 ; j < N; j++) {
+                if ( jobEvents.get(i).contains( jobEvents.get(j) )) {
+                    jobEvents.get(j).contained ++ ;
+                }
+            }
+        }
+    }
+}
diff --git a/trick_source/java/src/main/java/trick/jobperf/FrameViewCanvas.java b/trick_source/java/src/main/java/trick/jobperf/FrameViewCanvas.java
new file mode 100644
index 00000000..c8a7be9c
--- /dev/null
+++ b/trick_source/java/src/main/java/trick/jobperf/FrameViewCanvas.java
@@ -0,0 +1,99 @@
+package trick.jobperf;
+
+import java.awt.*;
+import java.io.*;
+import java.util.*;
+import javax.swing.*;
+
+public class FrameViewCanvas extends JPanel {
+    private FrameRecord frame;
+    private TraceViewCanvas tvc;
+    private Font headingsFont;
+    private Font dataFont;
+
+    public FrameViewCanvas( TraceViewCanvas tvc, FrameRecord frame ) {
+        this.tvc = tvc;
+        this.frame = frame;
+        dataFont      = new Font("Arial", Font.PLAIN, 18);
+        headingsFont  = new Font("Arial", Font.BOLD, 18);
+
+        setPreferredSize(new Dimension(800, neededPanelHeight()));
+    }
+
+    private void doDrawing(Graphics g) {
+        Graphics2D g2d = (Graphics2D) g;
+
+        RenderingHints rh = new RenderingHints(
+                RenderingHints.KEY_ANTIALIASING,
+                RenderingHints.VALUE_ANTIALIAS_ON);
+
+        rh.put(RenderingHints.KEY_RENDERING,
+               RenderingHints.VALUE_RENDER_QUALITY);
+
+        // Panel Background Color Fill
+        g2d.setPaint(Color.WHITE);
+        g2d.fillRect(0, 0, getWidth(), getHeight());
+
+        // TITLE
+        g2d.setFont(headingsFont);
+        g2d.setPaint( Color.RED );
+        g2d.drawString("Frame Details", 100, 50);
+
+        // Column Headings
+        g2d.setFont(headingsFont);
+        g2d.setPaint( Color.BLUE );
+        g2d.drawString("Job-ID", 100, 80);
+        g2d.drawString("Job-Class", 180, 80);
+        g2d.drawString("Start-Time", 420, 80);
+        g2d.drawString("Stop-Time", 520, 80);
+        g2d.drawString("Duration", 620, 80);
+        g2d.drawString("Job-Name", 740, 80);
+
+        frame.SortByStartTime();
+
+        // For each job in the frame.
+        int jobY = 100;
+        for (JobExecutionEvent jobExec : frame.jobEvents) {
+            g2d.setPaint( tvc.idToColorMap.getColor( jobExec.id ) );
+            g2d.fillRect(50, jobY, 20, 20);
+            g2d.setPaint( Color.BLACK );
+            jobY += 20;
+            double duration = jobExec.stop - jobExec.start;
+
+            g2d.setFont(dataFont);
+            g2d.drawString(jobExec.id, 100, jobY);
+            g2d.drawString( String.format("%12.6f", jobExec.start), 420, jobY);
+            g2d.drawString( String.format("%12.6f", jobExec.stop), 520, jobY);
+            g2d.drawString( String.format("%12.6f", duration), 620, jobY);
+
+            JobSpecification jobSpec = tvc.jobSpecificationMap.getJobSpecification(jobExec.id);
+            if ( jobSpec == null) {
+                g2d.setPaint( Color.RED );
+                g2d.drawString("UNKNOWN", 180, jobY);
+                g2d.drawString("UNKNOWN", 740, jobY);
+            } else {
+                g2d.drawString(jobSpec.jobClass, 180, jobY);
+                g2d.drawString(jobSpec.name, 740, jobY);
+            }
+        }
+        frame.SortByJobEventDuration();
+    }
+
+    /**
+     * Calculate the height of the FrameViewCanvas (JPanel) needed to render the
+     * jobs in the frame.
+     */
+    private int neededPanelHeight() {
+        return 20 * frame.jobEvents.size() + 100;
+    }
+
+    /**
+     * This function paints the FrameViewCanvas (i.e, JPanel) when required.
+     */
+    @Override
+    public void paintComponent(Graphics g) {
+        super.paintComponent(g);
+        doDrawing(g);
+    }
+
+}
diff --git a/trick_source/java/src/main/java/trick/jobperf/FrameViewWindow.java b/trick_source/java/src/main/java/trick/jobperf/FrameViewWindow.java
new file mode 100644
index 00000000..2e0f8b97
--- /dev/null
+++ b/trick_source/java/src/main/java/trick/jobperf/FrameViewWindow.java
@@ -0,0 +1,31 @@
+package trick.jobperf;
+
+import java.awt.*;
+import java.io.*;
+import java.util.*;
+import javax.swing.*;
+
+public class FrameViewWindow extends JFrame {
+    public FrameViewWindow( TraceViewCanvas tvc, FrameRecord frame, int frameNumber ) {
+
+        FrameViewCanvas frameViewCanvas = new FrameViewCanvas(tvc, frame);
+
+        JScrollPane scrollPane = new JScrollPane( frameViewCanvas );
+        scrollPane.getVerticalScrollBar().setUnitIncrement( 20 );
+
+        JPanel scrollingFrameViewCanvas = new JPanel();
+        scrollingFrameViewCanvas.add(scrollPane);
+        scrollingFrameViewCanvas.setLayout(new BoxLayout(scrollingFrameViewCanvas, BoxLayout.X_AXIS));
+
+        setTitle("Frame " + frameNumber);
+        setPreferredSize(new Dimension(1200, 400));
+        add(scrollingFrameViewCanvas);
+        pack();
+        setVisible(true);
+        setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
+        setFocusable(true);
+        setVisible(true);
+
+        frameViewCanvas.repaint();
+    }
+}
diff --git a/trick_source/java/src/main/java/trick/jobperf/InvalidFrameBoundsExpection.java b/trick_source/java/src/main/java/trick/jobperf/InvalidFrameBoundsExpection.java
new file mode 100644
index 00000000..faded8ec
--- /dev/null
+++ b/trick_source/java/src/main/java/trick/jobperf/InvalidFrameBoundsExpection.java
@@ -0,0 +1,12 @@
+package trick.jobperf;
+
+/**
+* Class InvalidFrameBoundsExpection is an exception indicating
+* that the user has specified an illegal range for the frames
+* to be rendered.
+*/
+class InvalidFrameBoundsExpection extends Exception {
+    public InvalidFrameBoundsExpection(String message) {
+        super(message);
+    }
+}
diff --git a/trick_source/java/src/main/java/trick/jobperf/JobExecutionEvent.java b/trick_source/java/src/main/java/trick/jobperf/JobExecutionEvent.java
new file mode 100644
index 00000000..0f7ad614
--- /dev/null
+++ b/trick_source/java/src/main/java/trick/jobperf/JobExecutionEvent.java
@@ -0,0 +1,64 @@
+package trick.jobperf;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.awt.image.BufferedImage;
+import java.io.*;
+import java.lang.Math;
+import java.util.*;
+import java.util.List;
+import javax.swing.*;
+import javax.swing.event.*;
+import java.net.URL;
+
+/**
+* Class JobExecutionEvent represents one execution/run of a Trick job.
+* <id> identifies the job. <start> and <stop> specify the
+* clock times at which the job started and finished.
+* <isTOF> indicates whether the job was run as
+* an "top-of-frame" job.
+*/
+class JobExecutionEvent {
+    public String id;
+    public boolean isTOF;
+    public boolean isEOF;
+    public double start;
+    public double stop;
+    public int contained;
+
+    /**
+     * @param identifier identifies the relavant Trick job.
+     * @param isTopOfFrame true if the job is a "top-of-frame" job, otherwise false.
+     * @param isEndOfFrame true if the job is a "end-of-frame" job, otherwise false.
+     * @param start_time the start time (seconds) of the identified job.
+     * @param stop_time the stop time (seconds) of the identified job.
+     */
+    public JobExecutionEvent(String id, boolean isTOF, boolean isEOF, double start, double stop) {
+        this.id = id;
+        this.isTOF = isTOF;
+        this.isEOF = isEOF;
+        this.start = start;
+        this.stop = stop;
+        contained = 1;
+    }
+
+    /**
+    * Determine whether a job's start time is contained
+    * within another jobs stop/stop range.
+    */
+    public boolean contains( JobExecutionEvent other ) {
+        if ((other.start > this.start) &&
+            (other.start < this.stop)) {
+                return true;
+            }
+        return false;
+    }
+
+    /**
+     * Create a String representation of an object of this class.
+     */
+     @Override
+    public String toString() {
+        return ( "JobExecutionEvent: " + id + "," + start + "," + stop );
+    }
+}
diff --git a/trick_source/java/src/main/java/trick/jobperf/JobPerf.java b/trick_source/java/src/main/java/trick/jobperf/JobPerf.java
new file mode 100644
index 00000000..c933cf09
--- /dev/null
+++ b/trick_source/java/src/main/java/trick/jobperf/JobPerf.java
@@ -0,0 +1,202 @@
+package trick.jobperf;
+
+import java.awt.*;
+import java.io.*;
+import java.util.*;
+import javax.swing.*;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+/**
+* Class JobPerf is an application that renders time-line data from a Trick based
+  simulation. It also generates run-time statistics reports for the simulation
+  jobs. It can be run with or without a GUI.
+*/
+public class JobPerf {
+    ArrayList<JobExecutionEvent> jobExecEvtList;
+    JobStats jobStats;
+
+    /**
+     * Constructor
+     * @param args the command line arguments.
+     */
+    public JobPerf( String[] args ) {
+        TraceViewWindow traceViewWindow;
+        boolean interactive = true;
+        boolean printReport = false;
+        JobStats.SortCriterion sortOrder = JobStats.SortCriterion.MEAN;
+        String timeLineFileName = "in.csv";
+
+        int ii = 0;
+        while (ii < args.length) {
+            switch (args[ii]) {
+                case "-h" :
+                case "--help" : {
+                    printHelpText();
+                    System.exit(0);
+                } break;
+                case "-x" :
+                case "--nogui" : {
+                    interactive = false;
+                } break;
+                case "-p" :
+                case "--report" : {
+                    printReport = true;
+                } break;
+                case "-s0" :
+                case "--sort=id" : {
+                    sortOrder = JobStats.SortCriterion.ID;
+                } break;
+                case "-s1" :
+                case "--sort=mean" : {
+                    sortOrder = JobStats.SortCriterion.MEAN;
+                } break;
+                case "-s2" :
+                case "--sort=stddev" : {
+                    sortOrder = JobStats.SortCriterion.STDDEV;
+                } break;
+                case "-s3" :
+                case "--sort=max" : {
+                    sortOrder = JobStats.SortCriterion.MAX;
+                } break;
+                case "-s4" :
+                case "--sort=min" : {
+                    sortOrder = JobStats.SortCriterion.MIN;
+                } break;
+                default : {
+                    timeLineFileName = args[ii];
+                } break;
+            } //switch
+            ++ii;
+        } // while
+
+        // All files shall be in the same directory as the timeline file.
+        String filesDir = Paths.get(timeLineFileName).toAbsolutePath().getParent().toString();
+        System.out.println( "\n\nFilesDir = " + filesDir + "\n\n");
+
+        // Generate the JobSpecificationMap from information extracted from the S_job_execution
+        // file, that should be in the same directory as the time-line file.
+        File s_job_execution_file = new File( filesDir + "/S_job_execution" );
+        JobSpecificationMap jobSpecificationMap = null;
+        try {
+            jobSpecificationMap = new JobSpecificationMap( s_job_execution_file );
+        } catch ( java.io.FileNotFoundException e ) {
+            System.out.println("File \"" + s_job_execution_file.toString() + "\" not found.\n");
+            System.exit(0);
+        } catch ( java.io.IOException e ) {
+            System.out.println("IO Exception while attempting to read " + s_job_execution_file.toString() + ".\n");
+            System.exit(0);
+        }
+
+        // Read Color Map
+        KeyedColorMap idToColorMap = null;
+        File colorMapFile = null;
+        try {
+            colorMapFile = new File(filesDir + "/IdToColors.txt");
+            idToColorMap = new KeyedColorMap( colorMapFile.toString());
+            if ( colorMapFile.exists()) {
+                 idToColorMap.readFile();
+            }
+        } catch ( java.io.IOException e ) {
+            System.out.println("IO Exception while attempting to read " + colorMapFile.toString() + ".\n");
+            System.exit(0);
+        }
+
+        jobExecEvtList = getJobExecutionEventList(timeLineFileName, jobSpecificationMap);
+
+        if (printReport) {
+            jobStats = new JobStats(jobExecEvtList);
+            if (sortOrder == JobStats.SortCriterion.ID ) jobStats.SortByID();
+            if (sortOrder == JobStats.SortCriterion.MEAN ) jobStats.SortByMeanValue();
+            if (sortOrder == JobStats.SortCriterion.STDDEV ) jobStats.SortByStdDev();
+            if (sortOrder == JobStats.SortCriterion.MAX ) jobStats.SortByMaxValue();
+            if (sortOrder == JobStats.SortCriterion.MIN ) jobStats.SortByMinValue();
+            jobStats.write( jobSpecificationMap);
+        }
+        if (interactive) {
+            traceViewWindow = new TraceViewWindow(jobExecEvtList, idToColorMap, jobSpecificationMap);
+        }
+    }
+
+    /**
+     * Print the usage instructions to the terminal.
+     */
+    private static void  printHelpText() {
+        System.out.println(
+            "----------------------------------------------------------------------\n"
+            + "usage: trick-jperf [options] <file-name>\n\n"
+            + "options: \n"
+            + "-h, --help\n"
+            + "    Print this help text and exit.\n"
+            + "-x, --nogui\n"
+            + "    Don't run as a GUI application. Command line only.\n"
+            + "-p, --report\n"
+            + "    Write sorted job statics report to the terminal.\n"
+            + "-s0, --sort=id\n"
+            + "    Sort job statistics by identifier.\n"
+            + "-s1, --sort=mean   [default]\n"
+            + "    Sort job statistics by mean duration.\n"
+            + "-s2, --sort=stddev\n"
+            + "    Sort job statistics by standard deviation of duration.\n"
+            + "-s3, --sort=min\n"
+            + "    Sort job statistics by minimum duration.\n"
+            + "-s4, --sort=max\n"
+            + "    Sort job statistics by maximum duration.\n"
+            + "----------------------------------------------------------------------\n"
+          );
+    }
+
+    /**
+     * Read the timeline file, resulting in a ArrayList<JobExecutionEvent>.
+     */
+    private ArrayList<JobExecutionEvent> getJobExecutionEventList( String fileName,
+                                                                   JobSpecificationMap jobSpecificationMap ) {
+        String line;
+        String field[];
+
+        ArrayList<JobExecutionEvent> jobExecEvtList = new ArrayList<JobExecutionEvent>();
+        try {
+            BufferedReader in = new BufferedReader( new FileReader(fileName) );
+
+            // Strip the header line off the CSV file.
+            line = in.readLine();
+
+            // Iterate through and process each of the data lines.
+            while( (line = in.readLine()) !=null) {
+                 boolean isTOF = false;
+                 boolean isEOF = false;
+                 field   = line.split(",");
+
+                 String id    = field[0].trim();
+                 JobSpecification jobSpec = jobSpecificationMap.getJobSpecification(id);
+                 if (jobSpec != null) {
+                     if (jobSpec.jobClass.equals("top_of_frame")) {
+                         isTOF = true;
+                     } else if (jobSpec.jobClass.equals("end_of_frame")) {
+                         isEOF = true;
+                     }
+                 } 
+                 double start = Double.parseDouble( field[1]);
+                 double stop  = Double.parseDouble( field[2]);
+                 if (start < stop) {
+                     JobExecutionEvent evt = new JobExecutionEvent(id, isTOF, isEOF, start, stop);
+                     jobExecEvtList.add( evt);
+                 }
+             }
+         } catch ( java.io.FileNotFoundException e ) {
+             System.out.println("File \"" + fileName + "\" not found.\n");
+             System.exit(0);
+         } catch ( java.io.IOException e ) {
+             System.out.println("IO Exception.\n");
+             System.exit(0);
+         }
+         return jobExecEvtList;
+    }
+
+    /**
+     * Entry point for the Java application.
+     */
+    public static void main(String[] args) {
+        JobPerf jobPerf = new JobPerf( args );
+    }
+}
diff --git a/trick_source/java/src/main/java/trick/jobperf/JobSpecification.java b/trick_source/java/src/main/java/trick/jobperf/JobSpecification.java
new file mode 100644
index 00000000..0b8e465f
--- /dev/null
+++ b/trick_source/java/src/main/java/trick/jobperf/JobSpecification.java
@@ -0,0 +1,33 @@
+package trick.jobperf;
+
+import java.awt.*;
+import java.io.*;
+import java.util.*;
+
+
+/**
+* Class JobSpecification represents ...
+*/
+class JobSpecification {
+    public String name;
+    public String jobClass;
+    public int phase;
+
+    /**
+     * @param name identifies the relevant Trick job.
+     * @param jobClass the Trick job class.
+     * @param phase the Trick phase number of the Trick job.
+     */
+    public JobSpecification(String name, String jobClass, int phase) {
+        this.name = name;
+        this.jobClass = jobClass;
+        this.phase = phase;
+    }
+    /**
+     * Create a String representation of an object of this jobClass.
+     */
+     @Override
+    public String toString() {
+        return ( "JobSpecification: " + name + "," + jobClass + "," + phase );
+    }
+}
diff --git a/trick_source/java/src/main/java/trick/jobperf/JobSpecificationMap.java b/trick_source/java/src/main/java/trick/jobperf/JobSpecificationMap.java
new file mode 100644
index 00000000..973b01ff
--- /dev/null
+++ b/trick_source/java/src/main/java/trick/jobperf/JobSpecificationMap.java
@@ -0,0 +1,69 @@
+package trick.jobperf;
+
+import java.awt.*;
+import java.io.*;
+import java.util.*;
+import java.nio.file.*;
+
+/**
+* Class JobSpecificationMap associates identifiers with unique RGB colors.
+*/
+public class JobSpecificationMap {
+    private Map<String, JobSpecification> jobSpecMap;
+
+    /**
+    * Constructor
+    */
+    public JobSpecificationMap( File file ) throws IOException, FileNotFoundException {
+        jobSpecMap = new HashMap<String, JobSpecification>();
+        System.out.println( "INSTANCIATING JobSpecificationMap("+ file.toString() +").");
+        BufferedReader in = new BufferedReader( new FileReader( file.toString()) );
+        String line;
+        String field[];
+
+        while( (line = in.readLine()) != null) {
+            if ( line.matches("\\s+1 [|].*$") ) {
+                field   = line.split("[|]");
+                if (field.length == 9) {
+                    String jobclass = field[2].trim();
+                    int phase = Integer.parseInt( field[3].trim());
+                    String id = String.format("%.2f", Double.parseDouble( field[7].trim()));
+                    String name = field[8].trim();
+                    jobSpecMap.put(id, new JobSpecification(name, jobclass, phase));
+                    //System.out.println("JobSpec = " + id + "," + jobclass + "," + name);
+                }
+            }
+        }
+// TODO:
+// Sometimes job specifications, and therefore their IDs are not recorded in the S_job_execution file.
+// So, when we attempt to find one of these unrecorded Job specs by ID, we get null.
+//       
+// Job IDs are of the form XX.YY (e.g., 12.03). The XX part is associated with
+// a particular component sim object. The YY part identifies a specific job from that
+// sim object.
+// If an unknown ID shares the XX part of the ID, then perhaps we could at least report which
+// simobject the job came from.
+
+        in.close();
+    }
+
+    /**
+    * Add an identifier, and a JobSpecification to the JobSpecificationMap.
+    * @ param identifier  Specifies the key.
+    */
+    public void addKey( String identifier,  JobSpecification jobSpec) {
+        if (!jobSpecMap.containsKey(identifier)) {
+            jobSpecMap.put(identifier, jobSpec);
+        }
+    }
+
+    /**
+    * Given an identifier, return the corresponding JobSpecification.
+    * @param identifier  the key.
+    * @return the JobSpecification associated with the key.
+    */
+    public JobSpecification getJobSpecification(String identifier) {
+        return jobSpecMap.get(identifier);
+    }
+
+} // class JobSpecificationMap
diff --git a/trick_source/java/src/main/java/trick/jobperf/JobStats.java b/trick_source/java/src/main/java/trick/jobperf/JobStats.java
new file mode 100644
index 00000000..4df1e352
--- /dev/null
+++ b/trick_source/java/src/main/java/trick/jobperf/JobStats.java
@@ -0,0 +1,198 @@
+package trick.jobperf;
+
+import java.io.*;
+import java.util.*;
+
+/**
+* Class CompareByID compares two StatisticsRecord's by id.
+*/
+class CompareByID implements Comparator<StatisticsRecord> {
+    public int compare(StatisticsRecord a, StatisticsRecord b) {
+        return a.id.compareTo(b.id);
+    }
+}
+/**
+* Class CompareByMeanValue compares two StatisticsRecord's by meanValue.
+*/
+class CompareByMeanValue implements Comparator<StatisticsRecord> {
+    public int compare(StatisticsRecord a, StatisticsRecord b) {
+        if ( a.meanValue < b.meanValue) return -1;
+        if ( a.meanValue > b.meanValue) return  1;
+        return 0;
+    }
+}
+/**
+* Class CompareByStdDev compares two StatisticsRecord's by stddev.
+*/
+class CompareByStdDev implements Comparator<StatisticsRecord> {
+    public int compare(StatisticsRecord a, StatisticsRecord b) {
+        if ( a.stddev < b.stddev) return -1;
+        if ( a.stddev > b.stddev) return  1;
+        return 0;
+    }
+}
+/**
+* Class CompareByMaxDuration compares two StatisticsRecord's by maxValue.
+*/
+class CompareByMaxDuration implements Comparator<StatisticsRecord> {
+    public int compare(StatisticsRecord a, StatisticsRecord b) {
+        if ( a.maxValue < b.maxValue) return -1;
+        if ( a.maxValue > b.maxValue) return  1;
+        return 0;
+    }
+}
+/**
+* Class CompareByMinDuration compares two StatisticsRecord's by minValue.
+*/
+class CompareByMinDuration implements Comparator<StatisticsRecord> {
+    public int compare(StatisticsRecord a, StatisticsRecord b) {
+        if ( a.minValue > b.minValue) return -1;
+        if ( a.minValue < b.minValue) return  1;
+        return 0;
+    }
+}
+
+/**
+* Class JobStats represents the statistics, i.e., mean, std deviation, max value,
+* and min value of the run-duration of each of the Trick jobs in jobExecList. The
+* statistic records can be sorted by any of the statistics, and by the job id,
+* prior to being written as a report.
+*/
+public class JobStats {
+
+    /**
+     * Enum SortCriterion enumerates the valid ways that JobStats records can be
+     * sorted.
+     */
+    enum SortCriterion {
+        ID {
+            @Override
+            public String toString() { return "Identifier"; }
+        },
+        MEAN {
+            @Override
+            public String toString() { return "Mean Value"; }
+        },
+        STDDEV {
+            @Override
+            public String toString() { return "Standard Deviation"; }
+        },
+        MAX {
+            @Override
+            public String toString() { return "Maximum Value"; }
+        },
+        MIN {
+            @Override
+            public String toString() { return "Minimum Value"; }
+        }
+    }
+
+    public SortCriterion currentSortCriterion = SortCriterion.MEAN;
+    public ArrayList<StatisticsRecord> jobStatisticsList;
+
+    /**
+     * Constructor
+     * @param jobExecList - the timeline data.
+     */
+    public JobStats( ArrayList<JobExecutionEvent> jobExecList ) {
+
+        Map<String, RunRegistry> runRegistryMap
+            = new HashMap<String, RunRegistry>();
+
+        for (JobExecutionEvent jobExec : jobExecList ) {
+            RunRegistry runRegistry = runRegistryMap.get(jobExec.id);
+            if (runRegistry != null) {
+                runRegistry.addTimeSpan(jobExec.start, jobExec.stop);
+            } else {
+                runRegistry = new RunRegistry();
+                runRegistry.addTimeSpan(jobExec.start, jobExec.stop);
+                runRegistryMap.put(jobExec.id, runRegistry);
+            }
+        }
+
+        jobStatisticsList = new ArrayList<StatisticsRecord>();
+
+        for (Map.Entry<String, RunRegistry> entry : runRegistryMap.entrySet()) {
+            String id = entry.getKey();
+            RunRegistry runRegistry = entry.getValue();
+            double mean   = runRegistry.getMeanDuration();
+            double stddev = runRegistry.getStdDev();
+            double min    = runRegistry.getMinDuration();
+            double max    = runRegistry.getMaxDuration();
+
+            jobStatisticsList.add( new StatisticsRecord(id, mean, stddev, min, max));
+        }
+        SortByMeanValue();
+    }
+
+    /**
+     * Sort by mean duration in descending order.
+     */
+    public void SortByID() {
+       Collections.sort( jobStatisticsList, new CompareByID());
+       currentSortCriterion = SortCriterion.ID;
+    }
+
+    /**
+     * Sort by mean duration in descending order.
+     */
+    public void SortByMeanValue() {
+        Collections.sort( jobStatisticsList, Collections.reverseOrder( new CompareByMeanValue()));
+        currentSortCriterion = SortCriterion.MEAN;
+    }
+
+    /**
+     * Sort by standard deviation of duration in descending order.
+     */
+    public void SortByStdDev() {
+        Collections.sort( jobStatisticsList, Collections.reverseOrder( new CompareByStdDev()));
+        currentSortCriterion = SortCriterion.STDDEV;
+    }
+
+    /**
+     * Sort by maximum duration in descending order.
+     */
+    public void SortByMaxValue() {
+        Collections.sort( jobStatisticsList, Collections.reverseOrder( new CompareByMaxDuration()));
+        currentSortCriterion = SortCriterion.MAX;
+    }
+
+    /**
+     * Sort by minimum duration in descending order.
+     */
+    public void SortByMinValue() {
+        Collections.sort( jobStatisticsList, Collections.reverseOrder( new CompareByMinDuration()));
+        currentSortCriterion = SortCriterion.MIN;
+    }
+
+    /**
+    Write a text report to System.out.
+    */
+    public void write( JobSpecificationMap jobSpecificationMap ) {
+
+        System.out.println("           [Job Duration Statistics Sorted by " + currentSortCriterion +"]");
+        System.out.println("--------------------------------------------------------------------------------------------------");
+        System.out.println("  Job Id   Mean Duration     Std Dev      Min Duration   Max Duration  Name");
+        System.out.println("---------- -------------- -------------- -------------- -------------- ---------------------------");
+
+        for (StatisticsRecord jobStatisticsRecord : jobStatisticsList ) {
+
+           JobSpecification jobSpec = jobSpecificationMap.getJobSpecification( jobStatisticsRecord.id);
+           String jobName = null;
+           if (jobSpec != null) {
+               jobName = jobSpec.name;
+           } else {
+               jobName = "UNKNOWN";
+           }
+           System.out.println( String.format("%10s %14.6f %14.6f %14.6f %14.6f %s",
+                                               jobStatisticsRecord.id,
+                                               jobStatisticsRecord.meanValue,
+                                               jobStatisticsRecord.stddev,
+                                               jobStatisticsRecord.minValue,
+                                               jobStatisticsRecord.maxValue,
+                                               jobName
+                                            )
+                             );
+        }
+    }
+}
diff --git a/trick_source/java/src/main/java/trick/jobperf/JobStatsViewCanvas.java b/trick_source/java/src/main/java/trick/jobperf/JobStatsViewCanvas.java
new file mode 100644
index 00000000..51cb675b
--- /dev/null
+++ b/trick_source/java/src/main/java/trick/jobperf/JobStatsViewCanvas.java
@@ -0,0 +1,99 @@
+package trick.jobperf;
+
+import java.awt.*;
+import java.io.*;
+import java.util.*;
+import javax.swing.*;
+
+public class JobStatsViewCanvas extends JPanel {
+
+    private Font headingsFont;
+    private Font dataFont;
+    JobStats jobStats;
+    JobSpecificationMap jobSpecificationMap;
+
+    public JobStatsViewCanvas( JobStats jobStats,
+                               JobSpecificationMap jobSpecificationMap ) {
+        this.jobStats = jobStats;
+        this.jobSpecificationMap = jobSpecificationMap;
+
+        dataFont      = new Font("Arial", Font.PLAIN, 18);
+        headingsFont  = new Font("Arial", Font.BOLD, 18);
+
+        setPreferredSize(new Dimension(800, neededPanelHeight()));
+    }
+
+    private void doDrawing(Graphics g) {
+
+        Graphics2D g2d = (Graphics2D) g;
+
+        RenderingHints rh = new RenderingHints(
+                RenderingHints.KEY_ANTIALIASING,
+                RenderingHints.VALUE_ANTIALIAS_ON);
+
+        rh.put(RenderingHints.KEY_RENDERING,
+               RenderingHints.VALUE_RENDER_QUALITY);
+
+        // Panel Background Color Fill
+        g2d.setPaint(Color.WHITE);
+        g2d.fillRect(0, 0, getWidth(), getHeight());
+
+        // Title
+        g2d.setFont(headingsFont);
+        g2d.setPaint( Color.RED );
+        g2d.drawString("Jobs Duration Statistics Sorted by " + jobStats.currentSortCriterion, 100, 50);
+
+        // Column Headings
+        g2d.setFont(headingsFont);
+
+        g2d.setPaint( Color.BLUE );
+        g2d.drawString("Job-ID", 100, 80);
+        g2d.drawString("Mean Dur", 200, 80);
+        g2d.drawString("Std Dev", 300, 80);
+        g2d.drawString("Min Dur", 400, 80);
+        g2d.drawString("Max Dur", 500, 80);
+        g2d.drawString("Job-Name", 600, 80);
+
+        // For each record
+        int jobY = 100;
+        for (StatisticsRecord jobStatisticsRecord : jobStats.jobStatisticsList ) {
+
+            g2d.setFont(dataFont);
+            g2d.setPaint( Color.BLACK );
+
+            g2d.drawString(jobStatisticsRecord.id, 100, jobY);
+            g2d.drawString( String.format("%14.6f", jobStatisticsRecord.meanValue), 180, jobY);
+            g2d.drawString( String.format("%14.6f", jobStatisticsRecord.stddev), 280, jobY);
+            g2d.drawString( String.format("%14.6f", jobStatisticsRecord.minValue), 380, jobY);
+            g2d.drawString( String.format("%14.6f", jobStatisticsRecord.maxValue), 480, jobY);
+
+            JobSpecification jobSpec = jobSpecificationMap.getJobSpecification( jobStatisticsRecord.id);
+            if (jobSpec != null) {
+                g2d.drawString(jobSpec.name, 600, jobY);
+            } else {
+                g2d.setPaint( Color.RED );
+                g2d.drawString("UNKNOWN", 600, jobY);
+            }
+
+            jobY += 20;
+        }
+    }
+
+    /**
+     * Calculate the height of the JobStatsCanvas (JPanel) needed to render the
+     * jobs in the frame.
+     */
+    private int neededPanelHeight() {
+        return 20 * jobStats.jobStatisticsList.size() + 100;
+    }
+
+    /**
+     * This function paints the JobStatsCanvas (i.e, JPanel) when required.
+     */
+    @Override
+    public void paintComponent(Graphics g) {
+        super.paintComponent(g);
+        doDrawing(g);
+    }
+
+}
diff --git a/trick_source/java/src/main/java/trick/jobperf/JobStatsViewWindow.java b/trick_source/java/src/main/java/trick/jobperf/JobStatsViewWindow.java
new file mode 100644
index 00000000..f8f9af15
--- /dev/null
+++ b/trick_source/java/src/main/java/trick/jobperf/JobStatsViewWindow.java
@@ -0,0 +1,113 @@
+package trick.jobperf;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import java.util.*;
+import javax.swing.*;
+
+class SortButtonsToolBar extends JToolBar implements ActionListener {
+    JobStatsViewCanvas statsViewCanvas;
+    private JButton sortByIDButton;
+    private JButton sortByMean;
+    private JButton sortByStdDev;
+    private JButton sortByMin;
+    private JButton sortByMax;
+
+    public SortButtonsToolBar( JobStatsViewCanvas statsViewCanvas ) {
+        this.statsViewCanvas = statsViewCanvas;
+
+        add( new JLabel("Sort by : "));
+        sortByIDButton = new JButton("ID");
+        sortByIDButton.addActionListener(this);
+        sortByIDButton.setActionCommand("sort-by-ID");
+        sortByIDButton.setToolTipText("Sort by Job ID");
+        add(sortByIDButton);
+
+        sortByMean = new JButton("Mean");
+        sortByMean.addActionListener(this);
+        sortByMean.setActionCommand("sort-by-mean");
+        sortByMean.setToolTipText("Sort by Mean Job Run Duration");
+        add(sortByMean);
+
+        sortByStdDev = new JButton("Std Dev");
+        sortByStdDev.addActionListener(this);
+        sortByStdDev.setActionCommand("sort-by-std-dev");
+        sortByStdDev.setToolTipText("Sort by Std Deviation of Job Run Duration");
+        add(sortByStdDev);
+
+        sortByMin = new JButton("Min");
+        sortByMin.addActionListener(this);
+        sortByMin.setActionCommand("sort-by-min");
+        sortByMin.setToolTipText("Sort by Minimum Job Run Duration");
+        add(sortByMin);
+
+        sortByMax = new JButton("Max");
+        sortByMax.addActionListener(this);
+        sortByMax.setActionCommand("sort-by-max");
+        sortByMax.setToolTipText("Sort by Maximum Job Run Duration");
+        add(sortByMax);
+    }
+
+    public void actionPerformed(ActionEvent e) {
+        String s = e.getActionCommand();
+        switch (s) {
+            case "sort-by-ID":
+                statsViewCanvas.jobStats.SortByID();
+                statsViewCanvas.repaint();
+                break;
+            case "sort-by-mean":
+                statsViewCanvas.jobStats.SortByMeanValue();
+                statsViewCanvas.repaint();
+                break;
+            case "sort-by-std-dev":
+                statsViewCanvas.jobStats.SortByStdDev();
+                statsViewCanvas.repaint();
+                break;
+            case "sort-by-min":
+                statsViewCanvas.jobStats.SortByMinValue();
+                statsViewCanvas.repaint();
+            break;
+            case "sort-by-max":
+                statsViewCanvas.jobStats.SortByMaxValue();
+                statsViewCanvas.repaint();
+            break;
+            default:
+                System.out.println("Unknown Action Command:" + s);
+                break;
+        }
+    }
+}
+
+public class JobStatsViewWindow extends JFrame {
+
+    public JobStatsViewWindow( JobStats jobStats, JobSpecificationMap jobSpecificationMap ) {
+
+        JobStatsViewCanvas statsViewCanvas = new JobStatsViewCanvas( jobStats, jobSpecificationMap);
+
+        JScrollPane scrollPane = new JScrollPane( statsViewCanvas );
+        scrollPane.setPreferredSize(new Dimension(800, 400));
+        scrollPane.getVerticalScrollBar().setUnitIncrement( 20 );
+
+        SortButtonsToolBar toolbar = new SortButtonsToolBar( statsViewCanvas);
+
+        JPanel scrollingJobStatsCanvas = new JPanel();
+        scrollingJobStatsCanvas.setPreferredSize(new Dimension(800, 400));
+        scrollingJobStatsCanvas.add(toolbar);
+        scrollingJobStatsCanvas.add(scrollPane);
+
+        scrollingJobStatsCanvas.setLayout( new BoxLayout(scrollingJobStatsCanvas, BoxLayout.Y_AXIS));
+
+        setTitle("Job Statistics");
+        // setSize(800, 500);
+        setPreferredSize(new Dimension(1200, 500));
+        add(scrollingJobStatsCanvas);
+        pack();
+        setVisible(true);
+        setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
+        setFocusable(true);
+        setVisible(true);
+
+        statsViewCanvas.repaint();
+    }
+}
diff --git a/trick_source/java/src/main/java/trick/jobperf/KeyedColorMap.java b/trick_source/java/src/main/java/trick/jobperf/KeyedColorMap.java
new file mode 100644
index 00000000..5e727453
--- /dev/null
+++ b/trick_source/java/src/main/java/trick/jobperf/KeyedColorMap.java
@@ -0,0 +1,125 @@
+package trick.jobperf;
+
+import java.awt.*;
+import java.io.*;
+import java.util.*;
+
+/**
+* Class KeyedColorMap associates identifiers with unique RGB colors.
+*/
+public class KeyedColorMap {
+    private Map<String, Color> colorMap;
+    int minLuminance;
+    String fileName;
+
+    /**
+    * Constructor
+    */
+    public KeyedColorMap(String fileName) {
+        this.fileName = fileName;
+        colorMap = new HashMap<String, Color>();
+        minLuminance = 30;
+    }
+
+    /**
+    * Generate a random color, that's not too dark.
+    * @ return the generated color.
+    */
+    private Color generateColor () {
+        Random rand = new Random();
+        boolean found = false;
+        int R = 0;
+        int G = 0;
+        int B = 0;
+
+        while (!found) {
+            R = rand.nextInt(256);
+            G = rand.nextInt(256);
+            B = rand.nextInt(256);
+            found = true;
+            // Reference: https://www.w3.org/TR/AERT/#color-contrast
+            double luminance = (0.299*R + 0.587*G + 0.114*B);
+            if (luminance < minLuminance ) found = false;
+        }
+        return new Color( R,G,B);
+    }
+
+    /**
+    * Add an identifier, and a generated Color to the KeyedColorMap.
+    * The Color will be generated randomly.
+    * @ param identifier  Specifies the key for which a color will be generated.
+    */
+    public void addKey( String identifier ) {
+        if (!colorMap.containsKey(identifier)) {
+            colorMap.put(identifier, generateColor());
+        }
+    }
+
+    /**
+    * Given an identifier, return its color.
+    * @param identifier  the key.
+    * @return the Color associated with the key.
+    */
+    public Color getColor(String identifier) {
+        return colorMap.get(identifier);
+    }
+
+    /**
+    * Given a color, return the associated key, otherwise return null.
+    * @param searchColor  the Color to search for.
+    * @return the identifier associated with the searchColor.
+    */
+    public String getKeyOfColor(Color searchColor) {
+        for (Map.Entry<String, Color> entry : colorMap.entrySet()) {
+            String id = entry.getKey();
+            Color color = entry.getValue();
+            if (color.getRGB() == searchColor.getRGB()) {
+                return id;
+            }
+        }
+        return null;
+    }
+
+    /**
+    * Write the identifier, color key/value pairs of the KeyedColorMap to a file.
+    * @param fileName
+    */
+    public void writeFile() throws IOException {
+        BufferedWriter out = new BufferedWriter( new FileWriter(fileName) );
+        for (Map.Entry<String, Color> entry : colorMap.entrySet()) {
+            String id = entry.getKey();
+            Color color = entry.getValue();
+            String line = String.format(id + "," + color.getRed() +
+                                             "," + color.getGreen() +
+                                             "," + color.getBlue() + "\n");
+            out.write(line, 0, line.length());
+        }
+        out.flush();
+        out.close();
+    } // method writeFile
+
+    /**
+    * Read identifier, color key-value pairs into the KeyedColorMap from a file.
+    * @param fileName
+    */
+    public void readFile() throws IOException {
+        try {
+            BufferedReader in = new BufferedReader( new FileReader(fileName) );
+            String line;
+            String field[];
+
+            while( (line = in.readLine()) !=null) {
+                field   = line.split(",");
+                String id    = field[0];
+                int R = Integer.parseInt( field[1]);
+                int G = Integer.parseInt( field[2]);
+                int B = Integer.parseInt( field[3]);
+                colorMap.put(id, new Color(R,G,B));
+            }
+            in.close();
+        } catch ( java.io.FileNotFoundException e ) {
+           System.out.println("File \"" + fileName + "\" not found.\n");
+        }
+    } // method readFile
+
+} // class KeyedColorMap
diff --git a/trick_source/java/src/main/java/trick/jobperf/RunRegistry.java b/trick_source/java/src/main/java/trick/jobperf/RunRegistry.java
new file mode 100644
index 00000000..94d3407b
--- /dev/null
+++ b/trick_source/java/src/main/java/trick/jobperf/RunRegistry.java
@@ -0,0 +1,73 @@
+package trick.jobperf;
+
+import java.lang.Math;
+import java.util.*;
+
+/**
+* Class RunRegistry represents a list of timeSpan's on which we can calculate
+* the average (mean), standard deviation, minimum, and maximum of the timeSpans
+* in the list.
+*/
+public class RunRegistry {
+    ArrayList<TimeSpan> timeSpanList;
+    /*
+    * Constructor
+    */
+    public RunRegistry() {
+        timeSpanList = new ArrayList<TimeSpan>();
+    }
+    void addTimeSpan(double start, double stop) {
+        TimeSpan timeSpan = new TimeSpan(start, stop);
+        timeSpanList.add(timeSpan);
+    }
+    void addTimeSpan(TimeSpan timeSpan) {
+        timeSpanList.add(timeSpan);
+    }
+    double getMeanDuration() {
+        double mean = 0.0;
+        int N = timeSpanList.size();
+        if (N > 0) {
+            double sum = 0.0;
+            for (TimeSpan timeSpan : timeSpanList ) {
+                sum += timeSpan.getDuration();
+            }
+            mean = sum / N;
+        }
+        return mean;
+    }
+    double getStdDev() {
+        double stddev = 0.0;
+        int N = timeSpanList.size();
+        if (N > 0) {
+            double sum = 0.0;
+            double mean = getMeanDuration();
+            for (TimeSpan timeSpan : timeSpanList ) {
+                double duration = timeSpan.getDuration();
+                double difference = duration - mean;
+                sum += difference * difference;
+            }
+            stddev = Math.sqrt( sum / N );
+        }
+        return stddev;
+    }
+    double getMaxDuration() {
+        double maxDuration = Double.MIN_VALUE;
+        for (TimeSpan timeSpan : timeSpanList ) {
+            double duration = timeSpan.getDuration();
+            if (duration > maxDuration) {
+                maxDuration = duration;
+            }
+        }
+        return maxDuration;
+    }
+    double getMinDuration() {
+        double minDuration = Double.MAX_VALUE;
+        for (TimeSpan timeSpan : timeSpanList ) {
+            double duration = timeSpan.getDuration();
+            if (duration < minDuration) {
+                minDuration = duration;
+            }
+        }
+        return minDuration;
+    }
+}
diff --git a/trick_source/java/src/main/java/trick/jobperf/StatisticsRecord.java b/trick_source/java/src/main/java/trick/jobperf/StatisticsRecord.java
new file mode 100644
index 00000000..a1e851b8
--- /dev/null
+++ b/trick_source/java/src/main/java/trick/jobperf/StatisticsRecord.java
@@ -0,0 +1,28 @@
+package trick.jobperf;
+
+/**
+* Class StatisticsRecord represents the statistics, i.e., mean, std deviation,
+* max value, and min value of the run-duration of an identified Trick job.
+*/
+public class StatisticsRecord {
+    public String id;
+    public double meanValue;
+    public double stddev;
+    public double maxValue;
+    public double minValue;
+    /**
+     * Constructor
+     * @param id - the job identifier.
+     * @param mean - the mean value of job duration.
+     * @param stddev - the standard deviation of job duration.
+     * @param min - the minimum value of job duration.
+     * @param max - the maximum value of job duration.
+     */
+    public StatisticsRecord( String id, double mean, double stddev, double min, double max) {
+        this.id = id;
+        this.meanValue = mean;
+        this.stddev = stddev;
+        this.minValue = min;
+        this.maxValue = max;
+    }
+}
diff --git a/trick_source/java/src/main/java/trick/jobperf/TimeSpan.java b/trick_source/java/src/main/java/trick/jobperf/TimeSpan.java
new file mode 100644
index 00000000..f441fd4c
--- /dev/null
+++ b/trick_source/java/src/main/java/trick/jobperf/TimeSpan.java
@@ -0,0 +1,24 @@
+package trick.jobperf;
+
+/**
+* Class TimeSpan represents a span of time.
+*/
+public class TimeSpan {
+    public double start;
+    public double stop;
+    /**
+     * Constructor
+     * @param begin the start time.
+     * @param end the end time.
+     */
+    public TimeSpan( double begin, double end) {
+        start = begin;
+        stop  = end;
+    }
+    /**
+     * @return the stop time minus the start time.
+     */
+    public double getDuration() {
+        return stop - start;
+    }
+}
diff --git a/trick_source/java/src/main/java/trick/jobperf/TraceViewCanvas.java b/trick_source/java/src/main/java/trick/jobperf/TraceViewCanvas.java
new file mode 100644
index 00000000..580322fd
--- /dev/null
+++ b/trick_source/java/src/main/java/trick/jobperf/TraceViewCanvas.java
@@ -0,0 +1,456 @@
+package trick.jobperf;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.awt.image.BufferedImage;
+import java.io.*;
+import java.util.*;
+import java.util.List;
+import javax.swing.*;
+import javax.swing.event.*;
+
+/**
+* Class TraceViewCanvas renders the simulation timeline data stored in
+* an ArrayList of JobExecutionEvent's [jobExecEvtList]. Information regarding
+* mouse clicks are sent to the TraceViewOutputToolBar [outputToolBar.]
+* @author John M. Penn
+*/
+public class TraceViewCanvas extends JPanel {
+
+    public static final int MIN_TRACE_WIDTH = 12;
+    public static final int DEFAULT_TRACE_WIDTH = 18;
+    public static final int MAX_TRACE_WIDTH = 24;
+    public static final int LEFT_MARGIN = 100;
+    public static final int RIGHT_MARGIN = 100;
+    public static final int TOP_MARGIN = 50;
+    public static final int BOTTOM_MARGIN = 20;
+    public static final int DEFAULT_FRAMES_TO_RENDER = 100;
+
+    public KeyedColorMap idToColorMap;
+    public JobSpecificationMap jobSpecificationMap;
+    public JobStats jobStats;
+
+    private int traceWidth;
+    private double frameSize;
+    private double totalDuration;
+    private FrameRecord[] frameArray;
+    private int selectedFrameNumber;
+    private FrameRange frameRenderRange;
+    private BufferedImage image;
+    private TraceViewOutputToolBar outputToolBar;
+    private Cursor crossHairCursor;
+    private Cursor defaultCursor;
+    private Font frameFont12;
+    private Font frameFont18;
+
+    public class FrameRange {
+        public int first;
+        public int last;
+        FrameRange (int first, int last) {
+            this.first = first;
+            this.last = last;
+        }
+        public boolean contains(int n) {
+            return ((first <= n) && (n <= last));
+        }
+        public int size() {
+            return last - first + 1;
+        }
+    }
+
+    /**
+     * Constructor
+     * @param jobExecEvtList the job time line data.
+     * @param outputToolBar the toolbar to which data is to be sent for display.
+     */
+    public TraceViewCanvas( ArrayList<JobExecutionEvent> jobExecEvtList,
+                            TraceViewOutputToolBar outputToolBar,
+                            KeyedColorMap idToColorMap,
+                            JobSpecificationMap jobSpecificationMap ) {
+
+        traceWidth = DEFAULT_TRACE_WIDTH;
+        frameSize = 1.0;
+        image = null;
+        selectedFrameNumber = 0;
+
+        this.outputToolBar = outputToolBar;
+        this.idToColorMap = idToColorMap;
+        this.jobSpecificationMap = jobSpecificationMap;
+
+        jobStats = new JobStats(jobExecEvtList);
+
+        crossHairCursor = new Cursor( Cursor.CROSSHAIR_CURSOR );
+        defaultCursor = new Cursor( Cursor.DEFAULT_CURSOR );
+
+        frameFont12 = new Font("Arial", Font.PLAIN, 12);
+        frameFont18 = new Font("Arial", Font.PLAIN, 18);
+
+        try {
+           boolean wasTOF = false;
+           boolean wasEOF = false;
+
+            List<FrameRecord> frameList = new ArrayList<FrameRecord>();
+            FrameRecord frameRecord = new FrameRecord();
+            for (JobExecutionEvent jobExec : jobExecEvtList ) {
+
+                if ((!wasTOF && jobExec.isTOF) || ( wasEOF && !jobExec.isEOF )) {
+
+                    // Wrap up the previous frame record.
+                     frameRecord.stop = jobExec.start;
+                     frameRecord.CalculateJobContainment();
+                     frameList.add(frameRecord);
+
+                     // Start a new frame record.
+                     frameRecord = new FrameRecord();
+                     frameRecord.start = jobExec.start;
+                 }
+                 frameRecord.jobEvents.add(jobExec);
+
+                 wasTOF = jobExec.isTOF;
+                 wasEOF = jobExec.isEOF;
+
+                 idToColorMap.addKey(jobExec.id);
+            }
+
+             frameArray = frameList.toArray( new FrameRecord[ frameList.size() ]);
+
+            // Determine the total duration and the average frame size. Notice
+            // that we skip the first frame.
+            totalDuration = 0.0;
+            for (int n=1; n < frameArray.length; n++) {
+                totalDuration += frameArray[n].getDuration();
+            }
+            frameSize = totalDuration/(frameArray.length-1);
+
+            // Set the range of frames to be rendered.
+            int last_frame_to_render = frameArray.length-1;
+            if ( frameArray.length > DEFAULT_FRAMES_TO_RENDER) {
+                last_frame_to_render = DEFAULT_FRAMES_TO_RENDER-1;
+            }
+            frameRenderRange = new FrameRange(0, last_frame_to_render);
+
+            // Write the color map to a file.
+            idToColorMap.writeFile();
+
+           // System.out.println("File loaded.\n");
+        } catch ( java.io.IOException e ) {
+           System.out.println("IO Exception.\n");
+           System.exit(0);
+        }
+
+        setPreferredSize(new Dimension(500, neededPanelHeight()));
+
+        ViewListener viewListener = new ViewListener();
+         addMouseListener(viewListener);
+         addMouseMotionListener(viewListener);
+    }
+
+    public int getFrameTotal() {
+        return frameArray.length;
+    }
+
+    public int getFirstRenderFrame() {
+        return frameRenderRange.first;
+    }
+
+    public int getLastRenderFrame() {
+        return frameRenderRange.last;
+    }
+
+    public void moveRenderFrameRangeBy(int advance) {
+        if ( frameArray.length > 50 ) {
+
+            int maxIndex = frameArray.length - 1;
+            int tFirst = frameRenderRange.first + advance;
+            int tLast = frameRenderRange.last + advance;
+
+            if (tLast > maxIndex) {
+                tLast = maxIndex;
+                tFirst = maxIndex - 49;
+            } else if (tFirst < 0) {
+                tFirst = 0;
+                tLast = 49;
+            }
+            frameRenderRange = new FrameRange(tFirst, tLast);
+            setPreferredSize(new Dimension(500, neededPanelHeight()));
+            repaint();
+        }
+    }
+
+    public void setFirstRenderFrame(int first) throws InvalidFrameBoundsExpection {
+        if ( (first >= 0) && (first <= frameRenderRange.last)) {
+            frameRenderRange = new FrameRange(first, frameRenderRange.last);
+            setPreferredSize(new Dimension(500, neededPanelHeight()));
+            repaint();
+        } else {
+            throw new InvalidFrameBoundsExpection("");
+        }
+    }
+
+    public void setLastRenderFrame(int last) throws InvalidFrameBoundsExpection {
+        if ((last >= frameRenderRange.first) && (last < frameArray.length)) {
+            frameRenderRange = new FrameRange(frameRenderRange.first, last);
+            // Re-render this TraceViewCanvas.
+            setPreferredSize(new Dimension(500, neededPanelHeight()));
+            repaint();
+        } else {
+            throw new InvalidFrameBoundsExpection("");
+        }
+    }
+
+    /**
+     * @return the current working frame size (seconds) used for rendering.
+     * Initially this is estimated from the timeline data, but it can be set to
+     * the actual realtime frame size of the user's sim.
+     */
+    public double getFrameSize() {
+        return frameSize;
+    }
+    /**
+     * Set the frame size (seconds) to be used for rendering the timeline data.
+     * @param duration the frame size.
+     */
+    public void setFrameSize(double time) {
+        frameSize = time;
+        repaint();
+    }
+
+    /**
+     * Increment the width to be used to render the job traces if the current
+     * trace width is less than MAX_TRACE_WIDTH.
+     */
+    public void incrementTraceWidth() {
+        if (traceWidth < MAX_TRACE_WIDTH) {
+            traceWidth ++;
+            repaint();
+        }
+    }
+
+    /**
+     * Decrement the width to be used to render the job traces if the current
+     * trace width is greater than MIN_TRACE_WIDTH.
+     */
+    public void decrementTraceWidth() {
+        if (traceWidth > MIN_TRACE_WIDTH) {
+            traceWidth --;
+            repaint();
+        }
+    }
+
+    /**
+     *
+     */
+    public void displaySelectedFrame() {
+        FrameViewWindow window = new FrameViewWindow( this, frameArray[selectedFrameNumber], selectedFrameNumber);
+    }
+
+    /**
+     *
+     */
+    public void displayJobStatsWindow() {
+        JobStatsViewWindow window = new JobStatsViewWindow( jobStats, jobSpecificationMap);
+    }
+
+    /**
+     * @return true if the trace rectangle contains the point <x,y>, otherwise
+     * false.
+     */
+    private boolean traceRectContains(int x, int y) {
+        int traceRectXMax = getWidth() - RIGHT_MARGIN;
+        if ( x < (LEFT_MARGIN)) return false;
+        if ( x > (traceRectXMax)) return false;
+        if (( y < TOP_MARGIN) || (y > (TOP_MARGIN + traceRectHeight()))) return false;
+        return true;
+    }
+
+    /**
+     * Class ViewListener monitors mouse activity within the TraceViewCanvas.
+     */
+    private class ViewListener extends MouseInputAdapter {
+
+        @Override
+        public void mouseReleased(MouseEvent e) {
+            int x = e.getX();
+            int y = e.getY();
+            Color color = new Color ( image.getRGB(x,y) );
+
+            // Get and display the ID of the job associated with the color.
+            String id = idToColorMap.getKeyOfColor( color );
+            outputToolBar.setJobID(id);
+
+            // Get and display the job name associated with the ID.
+            JobSpecification jobSpec = jobSpecificationMap.getJobSpecification(id);
+            if (jobSpec != null) {
+                outputToolBar.setJobName(jobSpec.name);
+                outputToolBar.setJobClass(jobSpec.jobClass);
+            }
+
+            // Determine the frame number that we clicked on from the y-
+            // coordinate of the click position.
+            if ( y > TOP_MARGIN) {
+                selectedFrameNumber = (y - TOP_MARGIN) / traceWidth + frameRenderRange.first;
+            }
+
+            // Determine the subframe-time where we clicked from the x-coordinate
+            // of the click position.
+            double subFrameClickTime = 0.0;
+            if ( traceRectContains(x, y)) {
+                double pixelsPerSecond = (double)traceRectWidth() / frameSize;
+                subFrameClickTime = (x - LEFT_MARGIN) / pixelsPerSecond;
+            }
+
+            /**
+             * If we clicked on a job trace (above), show the start and stop
+             * times of the job, otherwise clear the start and stop fields.
+             */
+            if (id != null) {
+                FrameRecord frame = frameArray[selectedFrameNumber];
+                Double clickTime = frame.start + subFrameClickTime;
+                for (JobExecutionEvent jobExec : frame.jobEvents) {
+                    if (id.equals( jobExec.id) &&
+                        clickTime >= jobExec.start &&
+                        clickTime <= jobExec.stop ) {
+                        outputToolBar.setJobTimes(jobExec.start, jobExec.stop);
+                    }
+                }
+                repaint();
+            } else {
+                outputToolBar.clearJobFields();
+            }
+        }
+
+        /**
+         * Set the cursor to a crossHairCursor if it's over the frame traces,
+         * otherwise, set it to the defaultCursor.
+         */
+        @Override
+        public void mouseMoved(MouseEvent e) {
+            int x = e.getX();
+            int y = e.getY();
+            if ( traceRectContains(x, y)) {
+                setCursor(crossHairCursor);
+            } else {
+                setCursor(defaultCursor);
+            }
+        }
+    }
+
+    /**
+     * @return the height of the trace rectangle.
+     */
+    private int traceRectHeight() {
+        return traceWidth * frameRenderRange.size();
+    }
+
+    /**
+     * @return the width of the trace rectangle.
+     */
+    private int traceRectWidth() {
+        return ( getWidth() - LEFT_MARGIN - RIGHT_MARGIN);
+    }
+
+    /**
+     * Calculate the height of the TraceViewCanvas (JPanel) needed to render the
+     * selected range of frames.
+     */
+    private int neededPanelHeight() {
+        return traceWidth * frameRenderRange.size() + TOP_MARGIN + BOTTOM_MARGIN;
+    }
+
+    /**
+     * Render the job execution traces in the jobExecEvtList.
+     */
+    private void doDrawing(Graphics g) {
+        Graphics2D g2d = (Graphics2D) g;
+
+        RenderingHints rh = new RenderingHints(
+                RenderingHints.KEY_ANTIALIASING,
+                RenderingHints.VALUE_ANTIALIAS_ON);
+
+        rh.put(RenderingHints.KEY_RENDERING,
+               RenderingHints.VALUE_RENDER_QUALITY);
+
+        int traceRectHeight = traceRectHeight();
+        int traceRectWidth = traceRectWidth();
+        double pixelsPerSecond = (double)traceRectWidth / frameSize;
+
+        // Panel Background Color Fill
+        g2d.setPaint(Color.WHITE);
+        g2d.fillRect(0, 0, getWidth(), getHeight());
+
+        // Frame Trace Rectangle Fill
+        g2d.setPaint(Color.BLACK);
+        g2d.fillRect(LEFT_MARGIN, TOP_MARGIN, traceRectWidth, traceRectHeight());
+
+        if (traceWidth >= DEFAULT_TRACE_WIDTH) {
+            g2d.setFont(frameFont18);
+        } else {
+            g2d.setFont(frameFont12);
+        }
+
+        FontMetrics fm = g2d.getFontMetrics();
+        int TX = 0;
+
+        String FN_text = "Frame #";
+        TX = (LEFT_MARGIN - fm.stringWidth(FN_text))/2;
+        g2d.drawString (FN_text, TX, 40);
+
+        g2d.drawString ("Top of Frame", LEFT_MARGIN, 40);
+
+        String EOF_text = "End of Frame";
+        TX = LEFT_MARGIN + traceRectWidth - fm.stringWidth(EOF_text);
+        g2d.drawString (EOF_text, TX, 40);
+
+        // Draw each frame in the selected range of frames to be rendered.
+        for (int n = frameRenderRange.first;
+                 n <= frameRenderRange.last;
+                 n++) {
+
+            FrameRecord frame = frameArray[n];
+            int jobY = TOP_MARGIN + (n - frameRenderRange.first) * traceWidth;
+
+            // Draw frame number.
+            if (n == selectedFrameNumber) {
+                g2d.setPaint(Color.RED);
+                // g2d.drawString ( "\u25b6", 20, jobY + traceWidth - 2);
+                g2d.drawString ( "\u25c0", 80, jobY + traceWidth - 2);
+                // g2d.fillRect(LEFT_MARGIN-traceWidth, jobY, traceWidth, traceWidth);
+            } else {
+                g2d.setPaint(Color.BLACK);
+            }
+
+            g2d.drawString ( String.format("%d", n), 40, jobY + traceWidth - 2);
+
+            // Draw the frame
+            // NOTE that the jobEvents within the frame are expected to be sorted in duration-order,
+            // so that smaller sub-jobs are not obscurred.
+
+            for (JobExecutionEvent jobExec : frame.jobEvents) {
+                int jobStartX = (int)((jobExec.start - frame.start) * pixelsPerSecond) + LEFT_MARGIN;
+                int jobWidth  = (int)((jobExec.stop - jobExec.start) * pixelsPerSecond);
+                g2d.setPaint( idToColorMap.getColor( jobExec.id ) );
+                int jobHeight = traceWidth - 2;
+                if (jobExec.contained > 1) {
+                    jobHeight = traceWidth / jobExec.contained;
+                }
+
+                // int jobStartY = jobY + (traceWidth - 2) - jobHeight;
+                g2d.fillRect(jobStartX, jobY, jobWidth, jobHeight);
+
+            }
+        }
+    }
+
+    /**
+     * This function paints the TraceViewCanvas (i.e, JPanel) when required.
+     */
+    @Override
+    public void paintComponent(Graphics g) {
+        super.paintComponent(g);
+        image = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);
+        Graphics2D g2 = image.createGraphics();
+        doDrawing(g2);
+        g.drawImage(image, 0, 0, this);
+        g2.dispose();
+    }
+}
diff --git a/trick_source/java/src/main/java/trick/jobperf/TraceViewInputToolBar.java b/trick_source/java/src/main/java/trick/jobperf/TraceViewInputToolBar.java
new file mode 100644
index 00000000..d4393ed6
--- /dev/null
+++ b/trick_source/java/src/main/java/trick/jobperf/TraceViewInputToolBar.java
@@ -0,0 +1,176 @@
+package trick.jobperf;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.awt.image.BufferedImage;
+import java.io.*;
+
+import javax.imageio.ImageIO;
+import javax.swing.*;
+import java.net.URL;
+
+/**
+* Class TraceViewInputToolBar initially displays an estimate of the frame size
+* for the JobPerf input timeline data. A user may also enter the intended frame
+* size into the JTextField, and pressing the "Set" button, which calls
+* traceView.setFrameSize( newFrameSize );
+*
+* Class TraceViewInputToolBar aggregates the following GUI components:
+* TraceViewInputToolBar (isa JToolBar)
+*     JLabel ()
+*     JTextField [frameSizeField]
+*     JLabel
+*     JLabel
+*     JTextField [firstRenderFrameField]
+*     JLabel
+*     JTextField [lastRenderFrameField]
+*/
+public class TraceViewInputToolBar extends JToolBar implements ActionListener {
+
+    private TraceViewCanvas traceView;
+    private JTextField frameSizeField;
+    private JButton frameDetailsButton;
+    private JButton advanceRangeButton;
+    private JButton retreatRangeButton;
+    private JTextField firstRenderFrameField;
+    private JTextField lastRenderFrameField;
+    /**
+     * Constructor
+     * @param tvc TraceViewCanvas to be controlled.
+     */
+    public TraceViewInputToolBar (TraceViewCanvas tvc) {
+        traceView = tvc;
+
+        add( new JLabel("           Frame Size (Avg): "));
+        frameSizeField = new JTextField(10);
+        frameSizeField.setText( String.format("%8.4f", traceView.getFrameSize()) );
+        add(frameSizeField);
+        frameSizeField.addKeyListener( new KeyAdapter() {
+            @Override
+            public void keyPressed(KeyEvent e) {
+                if (e.getKeyCode() == KeyEvent.VK_ENTER) {
+                    setFrameSize();
+                }
+            }
+        });
+
+        frameDetailsButton = new JButton("Frame Details");
+        frameDetailsButton.addActionListener(this);
+        frameDetailsButton.setActionCommand("display-frame-details");
+        frameDetailsButton.setToolTipText("Display the job details of the selected frame.");
+        add(frameDetailsButton);
+
+        add( new JLabel( String.format("          Frames : [%d ... %d]", 0, traceView.getFrameTotal()-1 )));
+
+        add( new JLabel("    Selected Range: "));
+
+        firstRenderFrameField = new JTextField(10);
+        firstRenderFrameField.setText( String.format("%d", traceView.getFirstRenderFrame()) );
+        add(firstRenderFrameField);
+        firstRenderFrameField.addKeyListener( new KeyAdapter() {
+            @Override
+            public void keyPressed(KeyEvent e) {
+                if (e.getKeyCode() == KeyEvent.VK_ENTER) {
+                     setFirstRenderFrame();
+                }
+            }
+        });
+
+        add( new JLabel("..."));
+        lastRenderFrameField = new JTextField(10);
+        lastRenderFrameField.setText( String.format("%d", traceView.getLastRenderFrame()) );
+        add(lastRenderFrameField);
+        lastRenderFrameField.addKeyListener( new KeyAdapter() {
+            @Override
+            public void keyPressed(KeyEvent e) {
+                if (e.getKeyCode() == KeyEvent.VK_ENTER) {
+                     setLastRenderFrame();
+                }
+            }
+        });
+
+        advanceRangeButton = new JButton("\u25bc");
+        advanceRangeButton.addActionListener(this);
+        advanceRangeButton.setActionCommand("advance-frame-range");
+        advanceRangeButton.setToolTipText("Advance the selected range of frames to be displayed.");
+        add(advanceRangeButton);
+
+        retreatRangeButton = new JButton("\u25b2");
+        retreatRangeButton.addActionListener(this);
+        retreatRangeButton.setActionCommand("retreat-frame-range");
+        retreatRangeButton.setToolTipText("Retreat the selected range of frames to be displayed.");
+        add(retreatRangeButton);
+
+        add( new JLabel("               "));
+
+        // Add Trick LOGO.
+        try { 
+            BufferedImage image = ImageIO.read(getClass().getResource("/trick/common/resources/trick_small.gif"));
+            add( new JLabel( new ImageIcon(image)));
+        } catch (IOException e) {
+            System.out.println("Failed to load image.");
+        }
+        
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        String s = e.getActionCommand();
+        switch (s) {
+            case "display-frame-details":
+                traceView.displaySelectedFrame();
+            break;
+            case "advance-frame-range":
+                moveRenderFrameRangeBy(50);
+            break;
+            case "retreat-frame-range":
+                moveRenderFrameRangeBy(-50);
+            break;
+            default:
+                System.out.println("Unknown Action Command:" + s);
+            break;
+        }
+    }
+
+    private void moveRenderFrameRangeBy(int advance) {
+        traceView.moveRenderFrameRangeBy(advance);
+        firstRenderFrameField.setText( String.format("%d", traceView.getFirstRenderFrame()));
+        lastRenderFrameField.setText( String.format("%d", traceView.getLastRenderFrame()));
+    }
+
+    private void setFirstRenderFrame() {
+        int newStartFrame = 0;
+        try {
+            newStartFrame = Integer.parseInt( firstRenderFrameField.getText() );
+            traceView.setFirstRenderFrame( newStartFrame );
+        } catch ( NumberFormatException e) {
+            firstRenderFrameField.setText( String.format("%d", traceView.getFirstRenderFrame()));
+        } catch ( InvalidFrameBoundsExpection e) {
+            firstRenderFrameField.setText( String.format("%d", traceView.getFirstRenderFrame()));
+        }
+    }
+
+    private void setLastRenderFrame() {
+        int newFinalFrame = 0;
+        try {
+            newFinalFrame = Integer.parseInt( lastRenderFrameField.getText() );
+            traceView.setLastRenderFrame( newFinalFrame );
+        } catch ( NumberFormatException e) {
+            lastRenderFrameField.setText( String.format("%d", traceView.getLastRenderFrame()));
+        } catch (InvalidFrameBoundsExpection e) {
+            lastRenderFrameField.setText( String.format("%d", traceView.getLastRenderFrame()));
+        }
+    }
+
+    private void setFrameSize() {
+        double newFrameSize = 0.0;
+        try {
+            newFrameSize = Double.parseDouble( frameSizeField.getText() );
+        } catch ( NumberFormatException e) {
+            frameSizeField.setText( String.format("%8.4f", traceView.getFrameSize()) );
+        }
+        if ( newFrameSize > 0.0) {
+            traceView.setFrameSize( newFrameSize );
+        }
+    }
+}
diff --git a/trick_source/java/src/main/java/trick/jobperf/TraceViewMenuBar.java b/trick_source/java/src/main/java/trick/jobperf/TraceViewMenuBar.java
new file mode 100644
index 00000000..602c849e
--- /dev/null
+++ b/trick_source/java/src/main/java/trick/jobperf/TraceViewMenuBar.java
@@ -0,0 +1,98 @@
+package trick.jobperf;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import javax.swing.*;
+
+/**
+* Class TraceViewMenuBar represents the menu bar of the JobPerf application.
+* It aggregates the following GUI components:
+* JMenuBar [this]
+*     JMenu [fileMenu]
+*         JMenuItem [fileMenuExit], Action: Call System.exit(0);
+*     JMenu [viewMenu]
+*         JMenu [traceSizeMenu]
+*             JMenuItem [traceSizeIncrease], Action: Call traceView.incrementTraceWidth().
+*             JMenuItem [traceSizeDecrease], Action: Call traceView.decrementTraceWidth()
+*/
+public class TraceViewMenuBar extends JMenuBar implements ActionListener {
+
+    private TraceViewCanvas traceView;
+
+    /**
+     * Constructor
+     * @param tvc the TraceViewCanvas to be controlled by this menu bar.
+     */
+    public TraceViewMenuBar(TraceViewCanvas tvc) {
+        traceView = tvc;
+
+        JMenu fileMenu = new JMenu("File");
+        JMenuItem fileMenuExit = new JMenuItem("Exit");
+        fileMenuExit.setActionCommand("exit");
+        KeyStroke ctrlQ  = KeyStroke.getKeyStroke('Q', InputEvent.CTRL_MASK );
+        fileMenuExit.setAccelerator(ctrlQ);
+        fileMenuExit.addActionListener(this);
+        fileMenu.add(fileMenuExit);
+        add(fileMenu);
+
+        JMenu viewMenu = new JMenu("View");
+
+        JMenuItem traceSizeIncrease = new JMenuItem("Increase Trace Width");
+        traceSizeIncrease.setActionCommand("increase-trace_width");
+        KeyStroke ctrlPlus  = KeyStroke.getKeyStroke('=', InputEvent.CTRL_MASK );
+        traceSizeIncrease.setAccelerator(ctrlPlus);
+        traceSizeIncrease.addActionListener(this);
+        viewMenu.add(traceSizeIncrease);
+
+        JMenuItem traceSizeDecrease = new JMenuItem("Decrease Trace Width");
+        traceSizeDecrease.setActionCommand("decrease-trace_width");
+        KeyStroke ctrlMinus = KeyStroke.getKeyStroke('-', InputEvent.CTRL_MASK);
+        traceSizeDecrease.setAccelerator(ctrlMinus);
+        traceSizeDecrease.addActionListener(this);
+        viewMenu.add(traceSizeDecrease);
+
+        viewMenu.addSeparator();
+
+        JMenuItem showFrame = new JMenuItem("Frame Details ...");
+        showFrame.setActionCommand("expand-selected-frame");
+        KeyStroke ctrlF = KeyStroke.getKeyStroke('F', InputEvent.CTRL_MASK);
+        showFrame.setAccelerator(ctrlF);
+        showFrame.addActionListener(this);
+        viewMenu.add(showFrame);
+
+        JMenuItem showStats = new JMenuItem("Job Statistics ...");
+        showStats.setActionCommand("show-job-stats");
+        KeyStroke ctrlV = KeyStroke.getKeyStroke('V', InputEvent.CTRL_MASK);
+        showStats.setAccelerator(ctrlV);
+        showStats.addActionListener(this);
+        viewMenu.add(showStats);
+
+        add(viewMenu);
+
+    }
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        String s = e.getActionCommand();
+        switch (s) {
+            case "increase-trace_width":
+                traceView.incrementTraceWidth();
+            break;
+            case "decrease-trace_width":
+                traceView.decrementTraceWidth();
+            break;
+            case "expand-selected-frame":
+                traceView.displaySelectedFrame();
+            break;
+            case "show-job-stats":
+                traceView.jobStats.SortByID();
+                traceView.displayJobStatsWindow();
+            break;
+            case "exit":
+                System.exit(0);
+            default:
+                System.out.println("Unknown Action Command:" + s);
+            break;
+        }
+    }
+}
diff --git a/trick_source/java/src/main/java/trick/jobperf/TraceViewOutputToolBar.java b/trick_source/java/src/main/java/trick/jobperf/TraceViewOutputToolBar.java
new file mode 100644
index 00000000..5ea038ed
--- /dev/null
+++ b/trick_source/java/src/main/java/trick/jobperf/TraceViewOutputToolBar.java
@@ -0,0 +1,104 @@
+package trick.jobperf;
+
+import java.awt.*;
+import javax.swing.*;
+
+/**
+* Class TraceViewOutputToolBar displays information output from a
+* TraceViewCanvas.
+*/
+class TraceViewOutputToolBar extends JToolBar {
+    private JTextField IDField;
+    private JTextField nameField;
+    private JTextField classField;
+    private JTextField startField;
+    private JTextField stopField;
+    // private JTextField frameNumberField;
+    private JTextField durationField;
+
+    /**
+     * Constructor
+     */
+    public TraceViewOutputToolBar () {
+
+        add( new JLabel(" Job ID: "));
+        IDField = new JTextField(5);
+        IDField.setEditable(false);
+        IDField.setText( "");
+        add(IDField);
+
+        add( new JLabel(" Name: "));
+        nameField = new JTextField(25);
+        nameField.setEditable(false);
+        nameField.setText( "");
+        add(nameField);
+
+        add( new JLabel(" Class: "));
+        classField = new JTextField(12);
+        classField.setEditable(false);
+        classField.setText( "");
+        add(classField);
+
+        add( new JLabel(" Start: "));
+        startField = new JTextField(6);
+        startField.setEditable(false);
+        startField.setText( "");
+        add(startField);
+
+        add( new JLabel(" Stop: "));
+        stopField = new JTextField(6);
+        stopField.setEditable(false);
+        stopField.setText( "");
+        add(stopField);
+
+        add( new JLabel(" Duration: "));
+        durationField = new JTextField(6);
+        durationField.setEditable(false);
+        durationField.setText( "");
+        add(durationField);
+    }
+
+    /**
+     * @param id job identifier to display.
+     */
+    public void setJobID(String id) {
+        IDField.setText( id );
+    }
+
+    /**
+     * @param id job identifier to display.
+     */
+    public void setJobName(String name) {
+        nameField.setText(name);
+    }
+
+    /**
+     * @param id job class to display.
+     */
+    public void setJobClass(String name) {
+        classField.setText(name);
+    }
+
+    /**
+     * @param time to be displayed in the job start field.
+     */
+    public void setJobTimes(Double start_time, Double stop_time) {
+        startField.setText( String.format("%8.4f", start_time) );
+        stopField.setText( String.format("%8.4f", stop_time) );
+        Double duration = stop_time - start_time;
+        durationField.setText( String.format("%8.4f", duration) );
+    }
+
+    /**
+     * Clear the startField and stopField.
+     */
+    public void clearJobFields() {
+        nameField.setText("");
+        classField.setText("");
+        startField.setText("");
+        stopField.setText("");
+        durationField.setText("");
+        IDField.setText("");
+    }
+
+}
diff --git a/trick_source/java/src/main/java/trick/jobperf/TraceViewWindow.java b/trick_source/java/src/main/java/trick/jobperf/TraceViewWindow.java
new file mode 100644
index 00000000..c87f1b2d
--- /dev/null
+++ b/trick_source/java/src/main/java/trick/jobperf/TraceViewWindow.java
@@ -0,0 +1,65 @@
+package trick.jobperf;
+
+import java.awt.*;
+import java.util.*;
+import javax.swing.*;
+
+/**
+* Class TraceViewWindow represents the main window of the JobPerf application.
+* It aggregates the following GUI components:
+*
+* - TraceViewMenuBar [menuBar]
+* - TraceViewInputToolBar [toolbar]
+* - JPanel [mainPanel]
+*     - JPanel [tracePanel]
+*         - JScrollPane [scrollPane]
+*             - TraceViewCanvas [traceViewCanvas]
+* - TraceViewOutputToolBar [outputToolBar]
+*/
+public class TraceViewWindow extends JFrame {
+
+    /**
+     * Constructor
+     * @param jobExecList an ArrayList of JobExecutionEvent, i.e., the job timeline data.
+     */
+    public TraceViewWindow( ArrayList<JobExecutionEvent> jobExecList,
+                            KeyedColorMap idToColorMap,
+                            JobSpecificationMap jobSpecificationMap ) {
+
+        TraceViewOutputToolBar outputToolBar = new TraceViewOutputToolBar();
+
+        TraceViewCanvas traceViewCanvas = new TraceViewCanvas( jobExecList, outputToolBar, idToColorMap, jobSpecificationMap);
+
+        TraceViewMenuBar menuBar = new TraceViewMenuBar( traceViewCanvas);
+        setJMenuBar(menuBar);
+
+        TraceViewInputToolBar nToolBar = new TraceViewInputToolBar( traceViewCanvas );
+        add(nToolBar, BorderLayout.NORTH);
+
+        JScrollPane scrollPane = new JScrollPane( traceViewCanvas );
+        scrollPane.setPreferredSize(new Dimension(800, 400));
+        scrollPane.getVerticalScrollBar().setUnitIncrement( 20 );
+
+        JPanel tracePanel = new JPanel();
+        tracePanel.setPreferredSize(new Dimension(800, 400));
+        tracePanel.add(scrollPane);
+        tracePanel.setLayout(new BoxLayout(tracePanel, BoxLayout.X_AXIS));
+
+        JPanel mainPanel  = new JPanel();
+        mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
+        mainPanel.add(tracePanel);
+
+        add(outputToolBar, BorderLayout.SOUTH);
+
+        setTitle("JobPerf");
+        setSize(800, 500);
+        add(mainPanel);
+        pack();
+        setVisible(true);
+        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+        setFocusable(true);
+        setVisible(true);
+
+        traceViewCanvas.repaint();
+    }
+}
diff --git a/trick_source/sim_services/FrameLog/FrameLog.cpp b/trick_source/sim_services/FrameLog/FrameLog.cpp
index 6a926f94..7be4c410 100644
--- a/trick_source/sim_services/FrameLog/FrameLog.cpp
+++ b/trick_source/sim_services/FrameLog/FrameLog.cpp
@@ -385,6 +385,7 @@ int Trick::FrameLog::frame_clock_stop(Trick::JobData * curr_job) {
                     mode = Run;
                 }
             }
+
             /** @li Save all cyclic job start & stop times for this frame into timeline structure. */
             if ((mode==Run) || (mode==Step)) {                            // cyclic job
                 if (tl_count[thread] < tl_max_samples) {
@@ -392,6 +393,8 @@ int Trick::FrameLog::frame_clock_stop(Trick::JobData * curr_job) {
                     timeline[thread][tl_count[thread]].start = target_job->rt_start_time;
                     timeline[thread][tl_count[thread]].stop  = target_job->rt_stop_time;
                     timeline[thread][tl_count[thread]].trick_job = target_job->tags.count("TRK");
+                    timeline[thread][tl_count[thread]].isEndOfFrame = target_job->isEndOfFrame;
+                    timeline[thread][tl_count[thread]].isTopOfFrame = target_job->isTopOfFrame;
                     tl_count[thread]++;
                 }
             /** @li Save all non-cyclic job start & stop times for this frame into timeline_other structure. */
@@ -583,8 +586,41 @@ int Trick::FrameLog::shutdown() {
         return(0) ;
     }
 
+
+// ================================================================
+// NEW Time-line for Jperf
+// ================================================================
+    for (int thread_num = 0; thread_num < num_threads; thread_num ++) {
+
+        if (thread_num == 0) {
+            snprintf(log_buff, sizeof(log_buff), "%s/log_newtimeline.csv", command_line_args_get_output_dir());
+        } else {
+            snprintf(log_buff, sizeof(log_buff), "%s/log_newtimelineC%d.csv", command_line_args_get_output_dir(), thread_num);
+        }
+
+        FILE *fp_log;
+        if ((fp_log = fopen(log_buff, "w")) == NULL) {
+            message_publish(MSG_ERROR, "Could not open log_timeline.csv file for Job Timeline Logging\n") ;
+            exit(0);
+        }
+
+        fprintf(fp_log,"jobID,startTime,stopTime\n");
+
+        time_scale = 1.0 / exec_get_time_tic_value();
+        tl = timeline[thread_num];
+        for ( ii = 0 ; ii < tl_count[thread_num] ; ii++ ) {
+            start = tl[ii].start * time_scale;
+            stop =  tl[ii].stop  * time_scale;
+            fprintf(fp_log,"%5.2f, %f, %f\n", tl[ii].id, start, stop);
+        }
+        fflush(fp_log);
+        fclose(fp_log);
+    }
+
     /** @li Manually create the log_timeline and log_timeline_init files from saved timeline data. */
     if (fp_time_main == NULL) {
+
+
         snprintf(log_buff, sizeof(log_buff), "%s/log_timeline.csv", command_line_args_get_output_dir());
         if ((fp_time_main = fopen(log_buff, "w")) == NULL) {
             message_publish(MSG_ERROR, "Could not open log_timeline.csv file for Job Timeline Logging\n") ;
@@ -592,11 +628,14 @@ int Trick::FrameLog::shutdown() {
         }
         fprintf(fp_time_main, "trick_frame_log.frame_log.job_time {s},");
         fprintf(fp_time_main, "trick_frame_log.frame_log.job_trick_id {--},frame_log.frame_log.job_user_id {--}");
+
         for (jj=1; jj<num_threads; jj++) {
             fprintf(fp_time_main, ",trick_frame_log.frame_log.job_userC%d_id {--}",jj);
         }
         fprintf(fp_time_main, "\n");
 
+
+
         snprintf(log_buff, sizeof(log_buff), "%s/log_timeline_init.csv", command_line_args_get_output_dir());
         if ((fp_time_other = fopen(log_buff, "w")) == NULL) {
             message_publish(MSG_ERROR, "Could not open log_timeline_init.csv file for Job Timeline Logging\n") ;
@@ -604,6 +643,9 @@ int Trick::FrameLog::shutdown() {
         }
         fprintf(fp_time_other, "trick_frame_log.frame_log.job_init_time {s},");
         fprintf(fp_time_other, "trick_frame_log.frame_log.job_trickinit_id {--},trick_frame_log.frame_log.job_userinit_id {--}\n");
+
+
+
     }
 
     time_scale = 1.0 / exec_get_time_tic_value();
@@ -615,17 +657,20 @@ int Trick::FrameLog::shutdown() {
             //               stop  job time, 0, 0
     /** @li print a 0 id before each start time & after each stop time for a stairstep effect in plot. */
     // cyclic jobs
+
     for ( thread = 0 ; thread < num_threads ; thread++ ) {
         tl = timeline[thread];
         for ( ii = 0 ; ii < tl_count[thread] ; ii++ ) {
             // start & stop time are in tics, so convert to seconds
             start = tl[ii].start * time_scale;
             stop =  tl[ii].stop  * time_scale;
+
             fprintf(fp_time_main,      "%f,0", start);        // start stairstep
             for (jj=0; jj<num_threads; jj++) {
                 fprintf(fp_time_main,  ",0");
             }
             fprintf(fp_time_main,      "\n");
+
             if (tl[ii].trick_job) {
                 fprintf(fp_time_main, "%f,%f", start, tl[ii].id);    // trick job start
                 for (jj=0; jj<num_threads; jj++) {
@@ -714,11 +759,6 @@ int Trick::FrameLog::create_DP_files() {
 int Trick::FrameLog::create_DP_Product_dir() {
     int ret=0;
     DP_dir = "DP_Product";
-    if (std::string(command_line_args_get_user_output_dir()) != std::string(command_line_args_get_output_dir())) {
-        if (!std::string(command_line_args_get_user_output_dir()).empty()) {
-            DP_dir = std::string(command_line_args_get_user_output_dir()) + "/DP_Product";
-        }
-    } 
     ret = mkdir(DP_dir.c_str(), 0777);
     if (ret == -1) {
         if (errno == EEXIST) {