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 @@
MM
+
+
+ jobperf
+ package
+
+ shade
+
+
+
+
+ trick.jobperf.JobPerf
+
+
+ JPerf
+
+
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..c410178d
--- /dev/null
+++ b/trick_source/java/src/main/java/trick/jobperf/JobPerf.java
@@ -0,0 +1,553 @@
+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.*;
+
+class JobExecutionEvent {
+ public String id;
+ public boolean isEOF;
+ public boolean isTOF;
+ public double start;
+ public double stop;
+
+ public JobExecutionEvent(String identifier, boolean isTopOfFrame, boolean isEndOfFrame, double start_time, double stop_time) {
+ id = identifier;
+ isEOF = isEndOfFrame;
+ isTOF = isTopOfFrame;
+ start = start_time;
+ stop = stop_time;
+ }
+
+ public String toString() {
+ return ( "JobExecutionEvent: " + id + "," + start + "," + stop );
+ }
+}
+
+class KeyedColorMap {
+ private Map colorMap;
+ int minColorIntensity;
+
+ public KeyedColorMap() {
+ colorMap = new HashMap();
+ minColorIntensity = 100;
+ }
+
+ 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;
+ if ((R < minColorIntensity) && (G < minColorIntensity) && (B < minColorIntensity)) {
+ found = false;
+ }
+ }
+ return new Color( R,G,B);
+ }
+
+ public void addKey( String identifier ) {
+ if (!colorMap.containsKey(identifier)) {
+ colorMap.put(identifier, generateColor());
+ }
+ }
+
+ // Given the key, return the color.
+ public Color getColor(String identifier) {
+ return colorMap.get(identifier);
+ }
+
+ // Given the color, return the key.
+ public String getKeyOfColor(Color search_color) {
+ for (Map.Entry entry : colorMap.entrySet()) {
+ String id = entry.getKey();
+ Color color = entry.getValue();
+ if (color.getRGB() == search_color.getRGB()) {
+ return id;
+ }
+ }
+ return null;
+ }
+
+ public void readFile(String fileName) 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");
+ }
+ }
+
+ public void writeFile(String fileName) 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();
+ }
+} // KeyedColorMap
+
+class TraceViewPanel extends JPanel {
+
+ public static final int MIN_TRACE_WIDTH = 4;
+ public static final int MAX_TRACE_WIDTH = 30;
+ public static final int LEFT_MARGIN = 100;
+ public static final int RIGHT_MARGIN = 100;
+ public static final int TOP_MARGIN = 20;
+ public static final int BOTTOM_MARGIN = 20;
+
+ private int traceWidth;
+ private double frameDuration;
+ private List jobExecList;
+ private KeyedColorMap idToColorMap;
+ private BufferedImage image;
+ private SouthToolBar sToolBar;
+ private Cursor crossHairCursor;
+ private Cursor defaultCursor;
+
+ public TraceViewPanel( String fileName, SouthToolBar southToolBar ) {
+
+ traceWidth = 10;
+ frameDuration = 1.0;
+ image = null;
+ sToolBar = southToolBar;
+ crossHairCursor = new Cursor( Cursor.CROSSHAIR_CURSOR );
+ defaultCursor = new Cursor( Cursor.DEFAULT_CURSOR );
+ double smallestJobTime = Double.MAX_VALUE;
+ double largestJobTime = -Double.MAX_VALUE;
+
+ // Read the Job execution file to build the jobExecList.
+ try {
+ BufferedReader in = new BufferedReader( new FileReader(fileName) );
+ jobExecList = new ArrayList();
+ idToColorMap = new KeyedColorMap();
+ idToColorMap.readFile("IdToColors.txt");
+ Random rand = new Random();
+ String line;
+ String field[];
+
+ // Strip the header off the CSV file.
+ line = in.readLine();
+ System.out.println("LINE = \"" + line + "\".\n");
+
+ boolean wasTOF = false;
+ double startOfFrame = 0.0;
+ double lastStartOfFrame = 0.0;
+ double frameSizeSum = 0.0;
+ int frameNumber = 0;
+ int frameSizeCount = 0;
+
+ while( (line = in.readLine()) !=null) {
+ field = line.split(",");
+ String id = field[0];
+ boolean isTOF = false;
+ boolean isEOF = false;
+ if (Integer.parseInt(field[1]) == 1) isTOF = true;
+ if (Integer.parseInt(field[2]) == 1) isEOF = true;
+ double start = Double.parseDouble( field[3]);
+ double stop = Double.parseDouble( field[4]);
+
+ if (start < stop) {
+ JobExecutionEvent evt = new JobExecutionEvent(id, isTOF, isEOF, start, stop);
+ jobExecList.add( evt);
+ if (start < smallestJobTime) smallestJobTime = start;
+ if (stop > largestJobTime) largestJobTime = stop;
+ // Calculate the average frame size.
+ if (!wasTOF && isTOF) {
+ startOfFrame = start;
+ if (frameNumber > 0) {
+ double frameSize = (startOfFrame - lastStartOfFrame);
+ frameSizeSum += frameSize;
+ frameSizeCount ++;
+ }
+ lastStartOfFrame = startOfFrame;
+ frameNumber++;
+ }
+ wasTOF = isTOF;
+ }
+ idToColorMap.addKey(id);
+ }
+
+ // Calculate the average frame size.
+ frameDuration = frameSizeSum / frameSizeCount;
+ idToColorMap.writeFile("IdToColors.txt");
+
+ System.out.println("File loaded.\n");
+ } catch ( java.io.FileNotFoundException e ) {
+ System.out.println("File not found.\n");
+ System.exit(0);
+ } catch ( java.io.IOException e ) {
+ System.out.println("IO Exception.\n");
+ System.exit(0);
+ }
+
+ int preferredHeight = traceWidth * (int)((largestJobTime - smallestJobTime) / frameDuration) + TOP_MARGIN;
+ setPreferredSize(new Dimension(500, preferredHeight));
+
+ ViewListener viewListener = new ViewListener();
+ addMouseListener(viewListener);
+ addMouseMotionListener(viewListener);
+ }
+
+ public double getFrameDuration() {
+ return frameDuration;
+ }
+
+ public void setFrameDuration(double duration) {
+ frameDuration = duration;
+ repaint();
+ }
+
+ public void increaseTraceWidth() {
+ if (traceWidth < MAX_TRACE_WIDTH) {
+ traceWidth ++;
+ repaint();
+ }
+ }
+
+ public void decreaseTraceWidth() {
+ if (traceWidth > MIN_TRACE_WIDTH) {
+ traceWidth --;
+ repaint();
+ }
+ }
+
+ 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) return false;
+ return true;
+ }
+
+ private boolean timeRectContains(int x, int y) {
+ int timeRectXMin = 30;
+ int timeRectXMax = LEFT_MARGIN;
+ if ( x < 30 ) return false;
+ if ( x > LEFT_MARGIN) return false;
+ if ( y < TOP_MARGIN) return false;
+ return true;
+ }
+
+ private class ViewListener extends MouseInputAdapter {
+ public void mouseReleased(MouseEvent e) {
+ int x = e.getX();
+ int y = e.getY();
+ Color color = new Color ( image.getRGB(x,y) );
+
+ String id = idToColorMap.getKeyOfColor( color );
+ sToolBar.setJobID(id);
+
+ if ( y > TOP_MARGIN) {
+ int frameNumber = (y - TOP_MARGIN) / traceWidth;
+ sToolBar.setFrameNumber(frameNumber);
+ }
+ if ( traceRectContains(x, y)) {
+ double pixelsPerSecond = (double)calcTraceRectWidth() / frameDuration;
+ double subFrameTime = (x - LEFT_MARGIN) / pixelsPerSecond;
+ sToolBar.setSubFrameTime(subFrameTime);
+ }
+ }
+
+ public void mouseMoved(MouseEvent e) {
+ int x = e.getX();
+ int y = e.getY();
+ if ( traceRectContains(x, y)) {
+ setCursor(crossHairCursor);
+ } else {
+ setCursor(defaultCursor);
+ }
+ }
+ }
+
+ private int calcTraceRectHeight() {
+ return ( getHeight() - TOP_MARGIN - BOTTOM_MARGIN);
+ }
+
+ private int calcTraceRectWidth() {
+ return ( getWidth() - LEFT_MARGIN - RIGHT_MARGIN);
+ }
+
+ 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 = calcTraceRectHeight();
+ int traceRectWidth = calcTraceRectWidth();
+ double pixelsPerSecond = (double)traceRectWidth / frameDuration;
+
+ // 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);
+
+ boolean wasEOF = false;
+ boolean wasTOF = false;
+ double startOfFrame = 0.0;
+ int frameNumber = 0;
+
+ for (JobExecutionEvent jobExec : jobExecList ) {
+
+ if (!wasTOF && jobExec.isTOF) {
+ startOfFrame = jobExec.start;
+ frameNumber ++;
+ }
+
+ wasTOF = jobExec.isTOF;
+ wasEOF = jobExec.isEOF;
+
+ int jobY = TOP_MARGIN + frameNumber * traceWidth;
+ int jobStartX = LEFT_MARGIN + (int)((jobExec.start - startOfFrame) * pixelsPerSecond);
+ int jobWidth = (int)( (jobExec.stop - jobExec.start) * pixelsPerSecond);
+
+ g2d.setPaint(Color.BLACK);
+ g2d.drawString ( String.format("%8.3f", startOfFrame), 30, jobY + traceWidth/2);
+ g2d.setPaint( idToColorMap.getColor( jobExec.id ) );
+ g2d.fillRect(jobStartX, jobY, jobWidth, traceWidth-2);
+
+ } // for
+ } // doDrawing
+
+ @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();
+ }
+} // class TraceViewPanel
+
+class NorthToolBar extends JToolBar implements ActionListener {
+
+ private TraceViewPanel traceView;
+ private JTextField frameDurationField;
+
+ public NorthToolBar (TraceViewPanel tv) {
+ traceView = tv;
+ add( new JLabel(" Frame Size: "));
+ frameDurationField = new JTextField(15);
+ frameDurationField.setText( String.format("%8.4f", traceView.getFrameDuration()) );
+ add(frameDurationField);
+
+ JButton setButton = new JButton("Set");
+ setButton.addActionListener(this);
+ setButton.setActionCommand("setFrameSize");
+ setButton.setToolTipText("Set frame size in seconds.");
+ add(setButton);
+ }
+ public void actionPerformed(ActionEvent event) {
+ String s = event.getActionCommand();
+ switch (s) {
+ case "setFrameSize":
+ double newFrameSize = 0.0;
+ try {
+ newFrameSize = Double.parseDouble( frameDurationField.getText() );
+ } catch ( NumberFormatException e) {
+ frameDurationField.setText( String.format("%8.4f", traceView.getFrameDuration()) );
+ }
+ if ( newFrameSize > 0.0) {
+ traceView.setFrameDuration( newFrameSize );
+ }
+ break;
+ default:
+ System.out.println("Unknown Action Command:" + s);
+ break;
+ }
+ }
+} // class NorthToolBar
+
+class SouthToolBar extends JToolBar {
+ private JTextField IDField;
+ private JTextField frameNumberField;
+ private JTextField subFrameTimeField;
+
+ public SouthToolBar () {
+
+ add( new JLabel(" Job ID: "));
+ IDField = new JTextField(15);
+ IDField.setEditable(false);
+ IDField.setText( "");
+ add(IDField);
+
+ add( new JLabel(" Frame Number: "));
+ frameNumberField = new JTextField(15);
+ frameNumberField.setEditable(false);
+ frameNumberField.setText( "0");
+ add(frameNumberField);
+
+ add( new JLabel(" Subframe Time: "));
+ subFrameTimeField = new JTextField(15);
+ subFrameTimeField.setEditable(false);
+ subFrameTimeField.setText( "0.00");
+ add(subFrameTimeField);
+ }
+ public void setJobID(String id) {
+ IDField.setText( id );
+ }
+ public void setFrameNumber(int fn) {
+ frameNumberField.setText( String.format("%d", fn));
+ }
+ public void setSubFrameTime(double time) {
+ subFrameTimeField.setText( String.format("%8.4f", time));
+ }
+} // class SouthToolBar
+
+class PerfMenuBar extends JMenuBar implements ActionListener {
+
+ private TraceViewPanel traceView;
+
+ public PerfMenuBar(TraceViewPanel tv) {
+ traceView = tv;
+
+ JMenu fileMenu = new JMenu("File");
+ JMenuItem fileMenuExit = new JMenuItem("Exit");
+ fileMenuExit.setActionCommand("exit");
+ fileMenuExit.addActionListener(this);
+ fileMenu.add(fileMenuExit);
+ add(fileMenu);
+
+ JMenu optionsMenu = new JMenu("Options");
+ JMenu traceSizeMenu = new JMenu("TraceSize");
+ JMenuItem traceSizeMenuIncrease = new JMenuItem("Increase Trace Width");
+ traceSizeMenuIncrease.setActionCommand("increase-trace_width");
+ KeyStroke ctrlPlus = KeyStroke.getKeyStroke(KeyEvent.VK_P, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx());
+ traceSizeMenuIncrease.setAccelerator(ctrlPlus);
+ traceSizeMenuIncrease.addActionListener(this);
+ traceSizeMenu.add(traceSizeMenuIncrease);
+ JMenuItem traceSizeMenuDecrease = new JMenuItem("Decrease Trace Width");
+ traceSizeMenuDecrease.setActionCommand("decrease-trace_width");
+ KeyStroke ctrlMinus = KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx());
+ traceSizeMenuDecrease.setAccelerator(ctrlMinus);
+ traceSizeMenuDecrease.addActionListener(this);
+ traceSizeMenu.add(traceSizeMenuDecrease);
+ optionsMenu.add(traceSizeMenu);
+ add(optionsMenu);
+
+ }
+ public void actionPerformed(ActionEvent e) {
+ String s = e.getActionCommand();
+ switch (s) {
+ case "increase-trace_width":
+ traceView.increaseTraceWidth();
+ break;
+ case "decrease-trace_width":
+ traceView.decreaseTraceWidth();
+ break;
+ case "exit":
+ System.exit(0);
+ default:
+ System.out.println("Unknown Action Command:" + s);
+ break;
+ }
+ }
+} // class PerfMenuBar
+
+public class JobPerf extends JFrame {
+
+ private List JobExecutionEventlist;
+
+ public JobPerf( TraceViewPanel traceView, SouthToolBar southToolBar) {
+
+ PerfMenuBar menuBar = new PerfMenuBar(traceView);
+ setJMenuBar(menuBar);
+
+ NorthToolBar nToolBar = new NorthToolBar( traceView );
+ add(nToolBar, BorderLayout.NORTH);
+
+ JScrollPane scrollPane = new JScrollPane( traceView );
+ scrollPane.setPreferredSize(new Dimension(800, 400));
+
+ 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(southToolBar, BorderLayout.SOUTH);
+
+ setTitle("JobPerf");
+ setSize(800, 500);
+ add(mainPanel);
+ pack();
+ setVisible(true);
+ setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ setFocusable(true);
+ setVisible(true);
+
+ traceView.repaint();
+ }
+
+ private static void printHelpText() {
+ System.out.println(
+ "----------------------------------------------------------------------\n"
+ + "usage: java jar JobPerf.jar \n"
+ + "----------------------------------------------------------------------\n"
+ );
+ }
+
+ public static void main(String[] args) {
+ int ii = 0;
+ String fileName = "in.csv";
+ while (ii < args.length) {
+ switch (args[ii]) {
+ case "-help" :
+ case "--help" : {
+ printHelpText();
+ System.exit(0);
+ } break;
+ default : {
+ fileName = args[ii];
+ } break;
+ }
+ ++ii;
+ } // while
+
+ SouthToolBar sToolBar = new SouthToolBar();
+ TraceViewPanel traceView = new TraceViewPanel( fileName, sToolBar);
+ JobPerf jobPerf = new JobPerf(traceView, sToolBar);
+
+ } // main
+} // class JobPerf
diff --git a/trick_source/sim_services/FrameLog/FrameLog.cpp b/trick_source/sim_services/FrameLog/FrameLog.cpp
index 2eb4516e..f6980729 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,44 @@ 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,isTopOfFrame,isEndOfFrame,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;
+ int isTrickJob = (tl[ii].trick_job) ? 1 : 0;
+ int isEndOfFrame = (tl[ii].isEndOfFrame) ? 1 : 0;
+ int isTopOfFrame = (tl[ii].isTopOfFrame) ? 1 : 0;
+ fprintf(fp_log,"%f,%d,%d,%f,%f\n", tl[ii].id, isTopOfFrame, isEndOfFrame, 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 +630,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