diff --git a/bin/trick-jperf b/bin/trick-jperf new file mode 100755 index 000000000..7fb8d3468 --- /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 edff7600b..35f519543 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 8ad72103e..3c5fde622 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 ee69706ca..c9cecb6a7 100644 --- a/trick_source/java/pom.xml +++ b/trick_source/java/pom.xml @@ -282,6 +282,22 @@ MM + + + jobperf + package + + shade + + + + + trick.jobperf.JobPerf + + + JPerf + + 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 000000000..fd92e4f4a --- /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 { + 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 { + 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 jobEvents; + public double start; + public double stop; + /** + * Constructor + */ + public FrameRecord() { + start = 0.0; + stop = 0.0; + jobEvents = new ArrayList(); + } + + /** + * @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 000000000..c8a7be9c1 --- /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 000000000..2e0f8b971 --- /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 000000000..faded8ec7 --- /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 000000000..0f7ad614d --- /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. +* identifies the job. and specify the +* clock times at which the job started and finished. +* 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 000000000..c933cf09d --- /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 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] \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. + */ + private ArrayList getJobExecutionEventList( String fileName, + JobSpecificationMap jobSpecificationMap ) { + String line; + String field[]; + + ArrayList jobExecEvtList = new ArrayList(); + 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 000000000..0b8e465f1 --- /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 000000000..af6996bd8 --- /dev/null +++ b/trick_source/java/src/main/java/trick/jobperf/JobSpecificationMap.java @@ -0,0 +1,59 @@ +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 jobSpecMap; + + /** + * Constructor + */ + public JobSpecificationMap( File file ) throws IOException, FileNotFoundException { + jobSpecMap = new HashMap(); + 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); + } + } + } + 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 000000000..4df1e3521 --- /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 { + 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 { + 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 { + 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 { + 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 { + 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 jobStatisticsList; + + /** + * Constructor + * @param jobExecList - the timeline data. + */ + public JobStats( ArrayList jobExecList ) { + + Map runRegistryMap + = new HashMap(); + + 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(); + + for (Map.Entry 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 000000000..51cb675b1 --- /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 000000000..f8f9af15e --- /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 000000000..5e727453c --- /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 colorMap; + int minLuminance; + String fileName; + + /** + * Constructor + */ + public KeyedColorMap(String fileName) { + this.fileName = fileName; + colorMap = new HashMap(); + 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 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 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 000000000..94d3407b7 --- /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 timeSpanList; + /* + * Constructor + */ + public RunRegistry() { + timeSpanList = new ArrayList(); + } + 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 000000000..a1e851b84 --- /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 000000000..f441fd4c6 --- /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 000000000..1e5d72063 --- /dev/null +++ b/trick_source/java/src/main/java/trick/jobperf/TraceViewCanvas.java @@ -0,0 +1,446 @@ +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 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 frameList = new ArrayList(); + 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 ( ((frameRenderRange.first + advance) > 0) && + // ((frameRenderRange.first + advance) < frameArray.length )) + // + // + // + // } + // } + + 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 , 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 000000000..6caf34709 --- /dev/null +++ b/trick_source/java/src/main/java/trick/jobperf/TraceViewInputToolBar.java @@ -0,0 +1,160 @@ +package trick.jobperf; + +import java.awt.*; +import java.awt.event.*; +import java.io.*; +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("Advance"); + advanceRangeButton.addActionListener(this); + advanceRangeButton.setActionCommand("advance-frame-range"); + advanceRangeButton.setToolTipText("Advance the selected range of frames to be displayed."); + add(advanceRangeButton); + + advanceRangeButton = new JButton("Retreat"); + advanceRangeButton.addActionListener(this); + advanceRangeButton.setActionCommand("retreat-frame-range"); + advanceRangeButton.setToolTipText("Retreat the selected range of frames to be displayed."); + add(advanceRangeButton); + + add( new JLabel(" ")); + + // Add Trick LOGO here. + } + + @Override + public void actionPerformed(ActionEvent e) { + String s = e.getActionCommand(); + switch (s) { + case "display-frame-details": + traceView.displaySelectedFrame(); + break; + case "advance-frame-range": + // DO ACTION + break; + case "retreat-frame-range": + // DO ACTION + break; + default: + System.out.println("Unknown Action Command:" + s); + break; + } + } + + 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 000000000..602c849e9 --- /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 000000000..5ea038ed5 --- /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 000000000..c87f1b2d3 --- /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 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 2eb4516e3..131fc9ba1 100644 --- a/trick_source/sim_services/FrameLog/FrameLog.cpp +++ b/trick_source/sim_services/FrameLog/FrameLog.cpp @@ -384,6 +384,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) { @@ -391,6 +392,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. */ @@ -582,8 +585,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") ; @@ -591,11 +627,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