diff --git a/lib/scripts/launch-fedsd.sh b/lib/scripts/launch-fedsd.sh new file mode 100755 index 0000000000..e09c60fc1e --- /dev/null +++ b/lib/scripts/launch-fedsd.sh @@ -0,0 +1,103 @@ +#!/bin/bash + +#============================================================================ +# Description: Visualize federated trace data for RTI-federate interactions. +# Authors: Chadlia Jerad +# Edward A. Lee +# Usage: Usage: fedsd -r [rti.csv] -f [fed.csv ...] +#============================================================================ + +#============================================================================ +# Preamble +#============================================================================ + +# Copied from build.sh FIXME: How to avoid copying + +# Find the directory in which this script resides in a way that is compatible +# with MacOS, which has a `readlink` implementation that does not support the +# necessary `-f` flag to canonicalize by following every symlink in every +# component of the given name recursively. +# This solution, adapted from an example written by Geoff Nixon, is POSIX- +# compliant and robust to symbolic links. If a chain of more than 1000 links +# is encountered, we return. +find_dir() ( + start_dir=$PWD + cd "$(dirname "$1")" + link=$(readlink "$(basename "$1")") + count=0 + while [ "${link}" ]; do + if [[ "${count}" -lt 1000 ]]; then + cd "$(dirname "${link}")" + link=$(readlink "$(basename "$1")") + ((count++)) + else + return + fi + done + real_path="$PWD/$(basename "$1")" + cd "${start_dir}" + echo `dirname "${real_path}"` +) + +# Report fatal error and exit. +function fatal_error() { + 1>&2 echo -e "\e[1mfedsd: \e[31mfatal error: \e[0m$1" + exit 1 +} + +abs_path="$(find_dir "$0")" + +if [[ "${abs_path}" ]]; then + base=`dirname $(dirname ${abs_path})` +else + fatal_error "Unable to determine absolute path to $0." +fi + +# Get the lft files +lft_files_list=$@ + +if [ -z "$lft_files_list" ] +then + echo "Usage: fedsd [lft files]" + exit 1 +fi + +# Initialize variables +csv_files_list='' +extension='.csv' +rti_csv_file='' + +# Iterate over the lft file list to: +# - First, transform into csv +# - Second, construct the csv fiel name +# - Then construct the csv file list +# The csv file list does include the rti, it is put in a separate variable +for each_lft_file in $lft_files_list + do + # Tranform to csv + trace_to_csv $each_lft_file + # Get the file name + csv=${each_lft_file%.*} + if [ $csv == 'rti' ] + then + # Set the rti csv file + rti_csv_file='rti.csv' + else + # Construct the csv file name and add it to the list + csv_files_list="$csv$extension $csv_files_list" + fi + done + +# echo $lft_files_list +# echo $rti_csv_file +# echo $csv_files_list + +# FIXME: Check that python3 is in the path. +if [ $rti_csv_file == '' ] +then + # FIXME: Support the case where no rti file is given + python3 "${base}/util/tracing/visualization/fedsd.py" "-f" $csv_files_list +else + echo Building the communication diagram for the following trace files: $lft_files_list in trace_svg.html + python3 "${base}/util/tracing/visualization/fedsd.py" "-r" "$rti_csv_file" "-f" $csv_files_list +fi diff --git a/org.lflang/src/lib/c/reactor-c b/org.lflang/src/lib/c/reactor-c index d43e973780..4d6bb55496 160000 --- a/org.lflang/src/lib/c/reactor-c +++ b/org.lflang/src/lib/c/reactor-c @@ -1 +1 @@ -Subproject commit d43e9737804f2d984d52a99cac20d8e57adad543 +Subproject commit 4d6bb5549640b57ac25b26b6ebb4ecfdfad256e6 diff --git a/org.lflang/src/org/lflang/TargetProperty.java b/org.lflang/src/org/lflang/TargetProperty.java index 1c580b603e..3f38360d67 100644 --- a/org.lflang/src/org/lflang/TargetProperty.java +++ b/org.lflang/src/org/lflang/TargetProperty.java @@ -580,8 +580,7 @@ public enum TargetProperty { }), /** - * Directive to generate a Dockerfile. This is either a boolean, - * true or false, or a dictionary of options. + * Directive to enable tracing. */ TRACING("tracing", UnionType.TRACING_UNION, Arrays.asList(Target.C, Target.CCPP, Target.CPP, Target.Python), diff --git a/org.lflang/src/org/lflang/federated/launcher/FedLauncherGenerator.java b/org.lflang/src/org/lflang/federated/launcher/FedLauncherGenerator.java index 4b4d748058..3ad08e2450 100644 --- a/org.lflang/src/org/lflang/federated/launcher/FedLauncherGenerator.java +++ b/org.lflang/src/org/lflang/federated/launcher/FedLauncherGenerator.java @@ -316,6 +316,9 @@ private String getRtiCommand(List federates, boolean isRemote) if (targetConfig.auth) { commands.add(" -a \\"); } + if (targetConfig.tracing != null) { + commands.add(" -t \\"); + } commands.addAll(List.of( " -n "+federates.size()+" \\", " -c "+targetConfig.clockSync.toString()+" \\" diff --git a/util/tracing/README.md b/util/tracing/README.md index d189260ef7..28d8db23d7 100644 --- a/util/tracing/README.md +++ b/util/tracing/README.md @@ -3,12 +3,18 @@ This directory contains the source code for utilities that are standalone executables for post-processing tracing data created by the tracing function in Lingua Franca. +Utilities for visualizing the data are contained in the [visualization](visualization/README.md) +directory. + * trace\_to\_csv: Creates a comma-separated values text file from a binary trace file. The resulting file is suitable for analyzing in spreadsheet programs such as Excel. * trace\_to\_chrome: Creates a JSON file suitable for importing into Chrome's trace visualizer. Point Chrome to chrome://tracing/ and load the resulting file. +* trace\_to\_influxdb: A preliminary implementation that takes a binary trace file + and uploads its data into [InfluxDB](https://en.wikipedia.org/wiki/InfluxDB). + ## Installing ``` diff --git a/util/tracing/makefile b/util/tracing/makefile index eb21e42e62..407b153235 100644 --- a/util/tracing/makefile +++ b/util/tracing/makefile @@ -28,6 +28,8 @@ install: trace_to_csv trace_to_chrome trace_to_influxdb mv trace_to_csv ../../bin mv trace_to_chrome ../../bin mv trace_to_influxdb ../../bin + ln -f -s ../lib/scripts/launch-fedsd.sh ../../bin/fedsd + chmod +x ../../bin/fedsd clean: rm -f *.o diff --git a/util/tracing/trace_to_chrome.c b/util/tracing/trace_to_chrome.c index d39f53171e..92eeee71dd 100644 --- a/util/tracing/trace_to_chrome.c +++ b/util/tracing/trace_to_chrome.c @@ -43,6 +43,12 @@ THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. /** Maximum thread ID seen. */ int max_thread_id = 0; +/** File containing the trace binary data. */ +FILE* trace_file = NULL; + +/** File for writing the output data. */ +FILE* output_file = NULL; + /** * Print a usage message. */ @@ -62,17 +68,23 @@ bool physical_time_only = false; /** * Read a trace in the specified file and write it to the specified json file. + * @param trace_file An open trace file. + * @param output_file An open output .json file. * @return The number of records read or 0 upon seeing an EOF. */ -size_t read_and_write_trace() { +size_t read_and_write_trace(FILE* trace_file, FILE* output_file) { int trace_length = read_trace(trace_file); if (trace_length == 0) return 0; // Write each line. for (int i = 0; i < trace_length; i++) { char* reaction_name = "\"UNKNOWN\""; - if (trace[i].reaction_number >= 0) { + + // Ignore federated trace events. + if (trace[i].event_type > federated) continue; + + if (trace[i].dst_id >= 0) { reaction_name = (char*)malloc(4); - snprintf(reaction_name, 4, "%d", trace[i].reaction_number); + snprintf(reaction_name, 4, "%d", trace[i].dst_id); } // printf("DEBUG: Reactor's self struct pointer: %p\n", trace[i].pointer); int reactor_index; @@ -113,7 +125,7 @@ size_t read_and_write_trace() { } // Default thread id is the worker number. - int thread_id = trace[i].worker; + int thread_id = trace[i].src_id; char* args; asprintf(&args, "{" @@ -182,7 +194,7 @@ size_t read_and_write_trace() { phase = "E"; break; default: - fprintf(stderr, "WARNING: Unrecognized event type %d: %s", + fprintf(stderr, "WARNING: Unrecognized event type %d: %s\n", trace[i].event_type, trace_event_names[trace[i].event_type]); pid = PID_FOR_UNKNOWN_EVENT; phase = "i"; @@ -206,8 +218,8 @@ size_t read_and_write_trace() { ); free(args); - if (trace[i].worker > max_thread_id) { - max_thread_id = trace[i].worker; + if (trace[i].src_id > max_thread_id) { + max_thread_id = trace[i].src_id; } // If the event is reaction_starts and physical_time_only is not set, // then also generate an instantaneous @@ -217,13 +229,13 @@ size_t read_and_write_trace() { pid = reactor_index + 1; reaction_name = (char*)malloc(4); char name[13]; - snprintf(name, 13, "reaction %d", trace[i].reaction_number); + snprintf(name, 13, "reaction %d", trace[i].dst_id); // NOTE: If the reactor has more than 1024 timers and actions, then // there will be a collision of thread IDs here. - thread_id = 1024 + trace[i].reaction_number; - if (trace[i].reaction_number > max_reaction_number) { - max_reaction_number = trace[i].reaction_number; + thread_id = 1024 + trace[i].dst_id; + if (trace[i].dst_id > max_reaction_number) { + max_reaction_number = trace[i].dst_id; } fprintf(output_file, "{" @@ -253,8 +265,9 @@ size_t read_and_write_trace() { /** * Write metadata events, which provide names in the renderer. + * @param output_file An open output .json file. */ -void write_metadata_events() { +void write_metadata_events(FILE* output_file) { // Thread 0 is the main thread. fprintf(output_file, "{" "\"name\": \"thread_name\", " @@ -416,13 +429,22 @@ int main(int argc, char* argv[]) { usage(); exit(0); } - open_files(filename, "json"); + + // Open the trace file. + trace_file = open_file(filename, "r"); + + // Construct the name of the csv output file and open it. + char* root = root_name(filename); + char json_filename[strlen(root) + 6]; + strcpy(json_filename, root); + strcat(json_filename, ".json"); + output_file = open_file(json_filename, "w"); if (read_header(trace_file) >= 0) { // Write the opening bracket into the json file. fprintf(output_file, "{ \"traceEvents\": [\n"); - while (read_and_write_trace() != 0) {}; - write_metadata_events(); + while (read_and_write_trace(trace_file, output_file) != 0) {}; + write_metadata_events(output_file); fprintf(output_file, "]}\n"); } } diff --git a/util/tracing/trace_to_csv.c b/util/tracing/trace_to_csv.c index a2bcfd91a2..707cc5f43b 100644 --- a/util/tracing/trace_to_csv.c +++ b/util/tracing/trace_to_csv.c @@ -37,6 +37,15 @@ THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #define MAX_NUM_REACTIONS 64 // Maximum number of reactions reported in summary stats. #define MAX_NUM_WORKERS 64 +/** File containing the trace binary data. */ +FILE* trace_file = NULL; + +/** File for writing the output data. */ +FILE* output_file = NULL; + +/** File for writing summary statistics. */ +FILE* summary_file = NULL; + /** Size of the stats table is object_table_size plus twice MAX_NUM_WORKERS. */ int table_size; @@ -69,7 +78,7 @@ typedef struct reaction_stats_t { */ typedef struct summary_stats_t { trace_event_t event_type; // Use reaction_ends for reactions. - char* description; // Description in the reaction table (e.g. reactor name). + const char* description; // Description in the reaction table (e.g. reactor name). int occurrences; // Number of occurrences of this description. int num_reactions_seen; reaction_stats_t reactions[MAX_NUM_REACTIONS]; @@ -94,11 +103,6 @@ size_t read_and_write_trace() { if (trace_length == 0) return 0; // Write each line. for (int i = 0; i < trace_length; i++) { - char* reaction_name = "none"; - if (trace[i].reaction_number >= 0) { - reaction_name = (char*)malloc(4); - snprintf(reaction_name, 4, "%d", trace[i].reaction_number); - } // printf("DEBUG: reactor self struct pointer: %p\n", trace[i].pointer); int object_instance = -1; char* reactor_name = get_object_description(trace[i].pointer, &object_instance); @@ -110,11 +114,11 @@ size_t read_and_write_trace() { if (trigger_name == NULL) { trigger_name = "NO TRIGGER"; } - fprintf(output_file, "%s, %s, %s, %d, %lld, %d, %lld, %s, %lld\n", + fprintf(output_file, "%s, %s, %d, %d, %lld, %d, %lld, %s, %lld\n", trace_event_names[trace[i].event_type], reactor_name, - reaction_name, - trace[i].worker, + trace[i].src_id, + trace[i].dst_id, trace[i].logical_time - start_time, trace[i].microstep, trace[i].physical_time - start_time, @@ -125,33 +129,41 @@ size_t read_and_write_trace() { if (trace[i].physical_time > latest_time) { latest_time = trace[i].physical_time; } - if (summary_stats[object_instance] == NULL) { - summary_stats[object_instance] = (summary_stats_t*)calloc(1, sizeof(summary_stats_t)); + if (object_instance >= 0 && summary_stats[NUM_EVENT_TYPES + object_instance] == NULL) { + summary_stats[NUM_EVENT_TYPES + object_instance] = (summary_stats_t*)calloc(1, sizeof(summary_stats_t)); } - if (trigger_instance >= 0 && summary_stats[trigger_instance] == NULL) { - summary_stats[trigger_instance] = (summary_stats_t*)calloc(1, sizeof(summary_stats_t)); + if (trigger_instance >= 0 && summary_stats[NUM_EVENT_TYPES + trigger_instance] == NULL) { + summary_stats[NUM_EVENT_TYPES + trigger_instance] = (summary_stats_t*)calloc(1, sizeof(summary_stats_t)); } - summary_stats_t* stats; + summary_stats_t* stats = NULL; interval_t exec_time; reaction_stats_t* rstats; int index; + // Count of event type. + if (summary_stats[trace[i].event_type] == NULL) { + summary_stats[trace[i].event_type] = (summary_stats_t*)calloc(1, sizeof(summary_stats_t)); + } + summary_stats[trace[i].event_type]->event_type = trace[i].event_type; + summary_stats[trace[i].event_type]->description = trace_event_names[trace[i].event_type]; + summary_stats[trace[i].event_type]->occurrences++; + switch(trace[i].event_type) { case reaction_starts: case reaction_ends: // This code relies on the mutual exclusion of reactions in a reactor // and the ordering of reaction_starts and reaction_ends events. - if (trace[i].reaction_number >= MAX_NUM_REACTIONS) { + if (trace[i].dst_id >= MAX_NUM_REACTIONS) { fprintf(stderr, "WARNING: Too many reactions. Not all will be shown in summary file.\n"); continue; } - stats = summary_stats[object_instance]; + stats = summary_stats[NUM_EVENT_TYPES + object_instance]; stats->description = reactor_name; - if (trace[i].reaction_number >= stats->num_reactions_seen) { - stats->num_reactions_seen = trace[i].reaction_number + 1; + if (trace[i].dst_id >= stats->num_reactions_seen) { + stats->num_reactions_seen = trace[i].dst_id + 1; } - rstats = &stats->reactions[trace[i].reaction_number]; + rstats = &stats->reactions[trace[i].dst_id]; if (trace[i].event_type == reaction_starts) { rstats->latest_start_time = trace[i].physical_time; } else { @@ -172,19 +184,19 @@ size_t read_and_write_trace() { // No trigger. Do not report. continue; } - stats = summary_stats[trigger_instance]; + stats = summary_stats[NUM_EVENT_TYPES + trigger_instance]; stats->description = trigger_name; break; case user_event: // Although these are not exec times and not reactions, // commandeer the first entry in the reactions array to track values. - stats = summary_stats[object_instance]; + stats = summary_stats[NUM_EVENT_TYPES + object_instance]; stats->description = reactor_name; break; case user_value: // Although these are not exec times and not reactions, // commandeer the first entry in the reactions array to track values. - stats = summary_stats[object_instance]; + stats = summary_stats[NUM_EVENT_TYPES + object_instance]; stats->description = reactor_name; rstats = &stats->reactions[0]; rstats->occurrences++; @@ -205,7 +217,7 @@ size_t read_and_write_trace() { // Use the reactions array to store data. // There will be two entries per worker, one for waits on the // reaction queue and one for waits while advancing time. - index = trace[i].worker * 2; + index = trace[i].src_id * 2; // Even numbered indices are used for waits on reaction queue. // Odd numbered indices for waits for time advancement. if (trace[i].event_type == scheduler_advancing_time_starts @@ -216,10 +228,10 @@ size_t read_and_write_trace() { fprintf(stderr, "WARNING: Too many workers. Not all will be shown in summary file.\n"); continue; } - stats = summary_stats[object_table_size + index]; + stats = summary_stats[NUM_EVENT_TYPES + object_table_size + index]; if (stats == NULL) { stats = (summary_stats_t*)calloc(1, sizeof(summary_stats_t)); - summary_stats[object_table_size + index] = stats; + summary_stats[NUM_EVENT_TYPES + object_table_size + index] = stats; } // num_reactions_seen here will be used to store the number of // entries in the reactions array, which is twice the number of workers. @@ -244,10 +256,15 @@ size_t read_and_write_trace() { } } break; + default: + // No special summary statistics for the rest. + break; } // Common stats across event types. - stats->occurrences++; - stats->event_type = trace[i].event_type; + if (stats != NULL) { + stats->occurrences++; + stats->event_type = trace[i].event_type; + } } return trace_length; } @@ -261,11 +278,22 @@ void write_summary_file() { fprintf(summary_file, "End time:, %lld\n", latest_time); fprintf(summary_file, "Total time:, %lld\n", latest_time - start_time); + fprintf(summary_file, "\nTotal Event Occurrences\n"); + for (int i = 0; i < NUM_EVENT_TYPES; i++) { + summary_stats_t* stats = summary_stats[i]; + if (stats != NULL) { + fprintf(summary_file, "%s, %d\n", + stats->description, + stats->occurrences + ); + } + } + // First pass looks for reaction invocations. // First print a header. fprintf(summary_file, "\nReaction Executions\n"); fprintf(summary_file, "Reactor, Reaction, Occurrences, Total Time, Pct Total Time, Avg Time, Max Time, Min Time\n"); - for (int i = 0; i < table_size; i++) { + for (int i = NUM_EVENT_TYPES; i < table_size; i++) { summary_stats_t* stats = summary_stats[i]; if (stats != NULL && stats->num_reactions_seen > 0) { for (int j = 0; j < stats->num_reactions_seen; j++) { @@ -288,7 +316,7 @@ void write_summary_file() { // Next pass looks for calls to schedule. bool first = true; - for (int i = 0; i < table_size; i++) { + for (int i = NUM_EVENT_TYPES; i < table_size; i++) { summary_stats_t* stats = summary_stats[i]; if (stats != NULL && stats->event_type == schedule_called && stats->occurrences > 0) { if (first) { @@ -302,7 +330,7 @@ void write_summary_file() { // Next pass looks for user-defined events. first = true; - for (int i = 0; i < table_size; i++) { + for (int i = NUM_EVENT_TYPES; i < table_size; i++) { summary_stats_t* stats = summary_stats[i]; if (stats != NULL && (stats->event_type == user_event || stats->event_type == user_value) @@ -329,7 +357,7 @@ void write_summary_file() { // Next pass looks for wait events. first = true; - for (int i = 0; i < table_size; i++) { + for (int i = NUM_EVENT_TYPES; i < table_size; i++) { summary_stats_t* stats = summary_stats[i]; if (stats != NULL && ( stats->event_type == worker_wait_ends @@ -369,15 +397,34 @@ int main(int argc, char* argv[]) { usage(); exit(0); } - open_files(argv[1], "csv"); + // Open the trace file. + trace_file = open_file(argv[1], "r"); + if (trace_file == NULL) exit(1); + + // Construct the name of the csv output file and open it. + char* root = root_name(argv[1]); + char csv_filename[strlen(root) + 5]; + strcpy(csv_filename, root); + strcat(csv_filename, ".csv"); + output_file = open_file(csv_filename, "w"); + if (output_file == NULL) exit(1); + + // Construct the name of the summary output file and open it. + char summary_filename[strlen(root) + 13]; + strcpy(summary_filename, root); + strcat(summary_filename, "_summary.csv"); + summary_file = open_file(summary_filename, "w"); + if (summary_file == NULL) exit(1); + + free(root); if (read_header() >= 0) { // Allocate an array for summary statistics. - table_size = object_table_size + (MAX_NUM_WORKERS * 2); + table_size = NUM_EVENT_TYPES + object_table_size + (MAX_NUM_WORKERS * 2); summary_stats = (summary_stats_t**)calloc(table_size, sizeof(summary_stats_t*)); // Write a header line into the CSV file. - fprintf(output_file, "Event, Reactor, Reaction, Worker, Elapsed Logical Time, Microstep, Elapsed Physical Time, Trigger, Extra Delay\n"); + fprintf(output_file, "Event, Reactor, Source, Destination, Elapsed Logical Time, Microstep, Elapsed Physical Time, Trigger, Extra Delay\n"); while (read_and_write_trace() != 0) {}; write_summary_file(); diff --git a/util/tracing/trace_to_influxdb.c b/util/tracing/trace_to_influxdb.c index d8281abd01..a99ae003ec 100644 --- a/util/tracing/trace_to_influxdb.c +++ b/util/tracing/trace_to_influxdb.c @@ -117,6 +117,9 @@ THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #define MAX_NUM_REACTIONS 64 // Maximum number of reactions reported in summary stats. #define MAX_NUM_WORKERS 64 +/** File containing the trace binary data. */ +FILE* trace_file = NULL; + /** Struct identifying the influx client. */ influx_client_t influx_client; influx_v2_client_t influx_v2_client; @@ -151,10 +154,14 @@ size_t read_and_write_trace() { if (trace_length == 0) return 0; // Write each line. for (int i = 0; i < trace_length; i++) { + + // Ignore federated traces. + if (trace[i].event_type > federated) continue; + char* reaction_name = "none"; - if (trace[i].reaction_number >= 0) { + if (trace[i].dst_id >= 0) { reaction_name = (char*)malloc(4); - snprintf(reaction_name, 4, "%d", trace[i].reaction_number); + snprintf(reaction_name, 4, "%d", trace[i].dst_id); } // printf("DEBUG: reactor self struct pointer: %p\n", trace[i].pointer); int object_instance = -1; @@ -176,7 +183,7 @@ size_t read_and_write_trace() { INFLUX_MEAS(trace_event_names[trace[i].event_type]), INFLUX_TAG("Reactor", reactor_name), INFLUX_TAG("Reaction", reaction_name), - INFLUX_F_INT("Worker", trace[i].worker), + INFLUX_F_INT("Worker", trace[i].src_id), INFLUX_F_INT("Logical Time", trace[i].logical_time), INFLUX_F_INT("Microstep", trace[i].microstep), INFLUX_F_STR("Trigger Name", trigger_name), @@ -259,7 +266,8 @@ int main(int argc, char* argv[]) { exit(1); } - open_files(filename, NULL); + // Open the trace file. + trace_file = open_file(filename, "r"); if (read_header() >= 0) { size_t num_records = 0, result; diff --git a/util/tracing/trace_util.c b/util/tracing/trace_util.c index f688b560d4..0f97b3bdb9 100644 --- a/util/tracing/trace_util.c +++ b/util/tracing/trace_util.c @@ -37,15 +37,6 @@ THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. /** Buffer for reading object descriptions. Size limit is BUFFER_SIZE bytes. */ char buffer[BUFFER_SIZE]; -/** File containing the trace binary data. */ -FILE* trace_file = NULL; - -/** File for writing the output data. */ -FILE* output_file = NULL; - -/** File for writing summary statistics. */ -FILE* summary_file = NULL; - /** Buffer for reading trace records. */ trace_record_t trace[TRACE_BUFFER_CAPACITY]; @@ -60,6 +51,13 @@ char* top_level = NULL; object_description_t* object_table; int object_table_size = 0; +typedef struct open_file_t open_file_t; +typedef struct open_file_t { + FILE* file; + open_file_t* next; +} open_file_t; +open_file_t* _open_files = NULL; + /** * Function to be invoked upon exiting. */ @@ -68,82 +66,57 @@ void termination() { for (int i = 0; i < object_table_size; i++) { free(object_table[i].description); } - if (trace_file != NULL) { - fclose(trace_file); - } - if (output_file != NULL) { - fclose(output_file); - } - if (summary_file != NULL) { - fclose(summary_file); + while (_open_files != NULL) { + fclose(_open_files->file); + open_file_t* tmp = _open_files->next; + free(_open_files); + _open_files = tmp; } printf("Done!\n"); } -/** - * Open the trace file and the output file using the given filename. - * This leaves the FILE* pointers in the global variables trace_file and output_file. - * If the extension if "csv", then it also opens a summary_file. - * The filename argument can include path information. - * It can include the ".lft" extension or not. - * The output file will have the same path and name except that the - * extension will be given by the second argument. - * The summary_file, if opened, will have the filename with "_summary.csv" appended. - * @param filename The file name. - * @param output_file_extension The extension to put on the output file name (e.g. "csv"). - * @return A pointer to the file. - */ -void open_files(char* filename, char* output_file_extension) { - // Open the input file for reading. - size_t length = strlen(filename); - if (length > 4 && strcmp(&filename[length - 4], ".lft") == 0) { - // The filename includes the .lft extension. - length -= 4; - } - char trace_file_name[length + 4]; - strncpy(trace_file_name, filename, length); - trace_file_name[length] = 0; - strcat(trace_file_name, ".lft"); - trace_file = fopen(trace_file_name, "r"); - if (trace_file == NULL) { - fprintf(stderr, "No trace file named %s.\n", trace_file_name); +const char PATH_SEPARATOR = +#ifdef _WIN32 + '\\'; +#else + '/'; +#endif + +char* root_name(const char* path) { + if (path == NULL) return NULL; + + // Remove any path. + char* last_separator = strrchr(path, PATH_SEPARATOR); + if (last_separator != NULL) path = last_separator + 1; + + // Allocate and copy name without extension. + char* last_period = strrchr(path, '.'); + size_t length = (last_period == NULL) ? + strlen(path) : last_period - path; + char* result = (char*)malloc(length + 1); + if (result == NULL) return NULL; + strncpy(result, path, length); + result[length] = '\0'; + + return result; +} + +FILE* open_file(const char* path, const char* mode) { + FILE* result = fopen(path, mode); + if (result == NULL) { + fprintf(stderr, "No file named %s.\n", path); usage(); exit(2); } - - // Open the output file for writing. - if (output_file_extension) { - char output_file_name[length + strlen(output_file_extension) + 1]; - strncpy(output_file_name, filename, length); - output_file_name[length] = 0; - strcat(output_file_name, "."); - strcat(output_file_name, output_file_extension); - output_file = fopen(output_file_name, "w"); - if (output_file == NULL) { - fprintf(stderr, "Could not create output file named %s.\n", output_file_name); - usage(); - exit(2); - } - - if (strcmp("csv", output_file_extension) == 0) { - // Also open a summary_file. - char *suffix = "_summary.csv"; - char summary_file_name[length + strlen(suffix) + 1]; - strncpy(summary_file_name, filename, length); - summary_file_name[length] = 0; - strcat(summary_file_name, suffix); - summary_file = fopen(summary_file_name, "w"); - if (summary_file == NULL) { - fprintf(stderr, "Could not create summary file named %s.\n", summary_file_name); - usage(); - exit(2); - } - } - } - - if (atexit(termination) != 0) { - fprintf(stderr, "WARNING: Failed to register termination function!"); + open_file_t* record = (open_file_t*)malloc(sizeof(open_file_t)); + if (record == NULL) { + fprintf(stderr, "Out of memory.\n"); + exit(3); } + record->file = result; + record->next = _open_files; + _open_files = record; + return result; } /** diff --git a/util/tracing/trace_util.h b/util/tracing/trace_util.h index 56eb9fe3e8..dab1f5e989 100644 --- a/util/tracing/trace_util.h +++ b/util/tracing/trace_util.h @@ -36,10 +36,10 @@ THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. /** Macro to use when access to trace file fails. */ #define _LF_TRACE_FAILURE(trace_file) \ do { \ - fprintf(stderr, "WARNING: Access to trace file failed.\n"); \ + fprintf(stderr, "ERROR: Access to trace file failed.\n"); \ fclose(trace_file); \ trace_file = NULL; \ - return -1; \ + exit(1); \ } while(0) /** Buffer for reading object descriptions. Size limit is BUFFER_SIZE bytes. */ @@ -73,18 +73,23 @@ extern int object_table_size; extern char* top_level; /** - * Open the trace file and the output file using the given filename. - * This leaves the FILE* pointers in the global variables trace_file and output_file. - * If the extension if "csv", then it also opens a summary_file. - * The filename argument can include path information. - * It can include the ".lft" extension or not. - * The output file will have the same path and name except that the - * extension will be given by the second argument. - * The summary_file, if opened, will have the filename with "_summary.csv" appended. - * @param filename The file name. - * @param output_file_extension The extension to put on the output file name (e.g. "csv"). + * @brief Return the root file name from the given path. + * Given a path to a file, this function returns a dynamically + * allocated string (which you must free) that points to the root + * filename without the preceding path and without the file extension. + * @param path The path including the full filename. + * @return The root name of the file or NULL for failure. */ -void open_files(char* filename, char* output_file_extension); +char* root_name(const char* path); + +/** + * @brief Open the specified file for reading or writing. + * This function records the file for closing at termination. + * @param path The path to the file. + * @param mode "r" for reading and "w" for writing. + * @return A pointer to the open file or NULL for failure. + */ +FILE* open_file(const char* path, const char* mode); /** * Get the description of the object pointed to by the specified pointer. diff --git a/util/tracing/visualization/.gitignore b/util/tracing/visualization/.gitignore new file mode 100644 index 0000000000..ba0430d26c --- /dev/null +++ b/util/tracing/visualization/.gitignore @@ -0,0 +1 @@ +__pycache__/ \ No newline at end of file diff --git a/util/tracing/visualization/README.md b/util/tracing/visualization/README.md new file mode 100644 index 0000000000..82861e9b2a --- /dev/null +++ b/util/tracing/visualization/README.md @@ -0,0 +1,25 @@ +# Trace sequence diagram visualiser + +This is a 1st iteration of a prototyping tool for constructing a sequence diagram +out of the traces. +It operates over the csv files generated by `trace_to_csv`. + +# Running + +Once the `.lft` files collected and tranformed into `csv` files, run: +``` +$ python3 sd_gen.py -r -f ... +``` + +The output is an html file with the svg in it. + +# Current problems + +- The collected traces are not complete. They need to be checked for correcteness as well. +- All arrows are horizontal and can be duplicated. Need further processing to derive the connections +for that. +- The scale needs exploration + + + + diff --git a/util/tracing/visualization/fedsd.py b/util/tracing/visualization/fedsd.py new file mode 100644 index 0000000000..3eb13d11bc --- /dev/null +++ b/util/tracing/visualization/fedsd.py @@ -0,0 +1,314 @@ +''' +Define arrows: + (x1, y1) ==> (x2, y2), when unique result (this arrow will be tilted) + (x1, y1) --> (x2, y2), when a possible result (could be not tilted)? +If not arrow, then triangle with text + +In the dataframe, each row will be marked with one op these values: + - 'arrow': draw a non-dashed arrow + - 'dot': draw a dot only + - 'marked': marked, not to be drawn + - 'pending': pending + - 'adv': for reporting logical time advancing, draw a simple dash +''' + +# Styles to determine appearance: +css_style = ' \ +' + +#!/usr/bin/env python3 +import argparse # For arguments parsing +import pandas as pd # For csv manipulation +from os.path import exists +from pathlib import Path +import math +import fedsd_helper as fhlp + +# Define the arguments to pass in the command line +parser = argparse.ArgumentParser(description='Set of the csv trace files to render.') +parser.add_argument('-r','--rti', type=str, default="rti.csv", + help='RTI csv trace file.') +parser.add_argument('-f','--federates', nargs='+', action='append', + help='List of the federates csv trace files.') + + +''' Clock synchronization error ''' +''' FIXME: There should be a value for each communicating pair ''' +clock_sync_error = 0 + +''' Bound on the network latency ''' +''' FIXME: There should be a value for each communicating pair ''' +network_latency = 100000000 # That is 100us + + +def load_and_process_csv_file(csv_file) : + ''' + Loads and processes the csv entries, based on the type of the actor (if RTI + or federate). + + Args: + * csv_file: String file name + Returns: + * The processed dataframe. + ''' + # Load tracepoints, rename the columns and clean non useful data + df = pd.read_csv(csv_file) + df.columns = ['event', 'reactor', 'self_id', 'partner_id', 'logical_time', 'microstep', 'physical_time', 't', 'ed'] + df = df.drop(columns=['reactor', 't', 'ed']) + + # Remove all the lines that do not contain communication information + # which boils up to having 'RTI' in the 'event' column + df = df[df['event'].str.contains('Sending|Receiving|Scheduler advancing time ends') == True] + df = df.astype({'self_id': 'int', 'partner_id': 'int'}) + + # Add an inout column to set the arrow direction + df['inout'] = df['event'].apply(lambda e: 'in' if 'Receiving' in e else 'out') + + # Prune event names + df['event'] = df['event'].apply(lambda e: fhlp.prune_event_name[e]) + return df + + +if __name__ == '__main__': + args = parser.parse_args() + + # Check if the RTI trace file exists + if (not exists(args.rti)): + print('Error: No RTI csv trace file! Specify with -r argument.') + exit(1) + + # The RTI and each of the federates have a fixed x coordinate. They will be + # saved in a dict + x_coor = {} + actors = [] + actors_names = {} + padding = 50 + spacing = 200 # Spacing between federates + + ############################################################################ + #### RTI trace processing + ############################################################################ + trace_df = load_and_process_csv_file(args.rti) + x_coor[-1] = padding * 2 + actors.append(-1) + actors_names[-1] = "RTI" + # Temporary use + trace_df['x1'] = x_coor[-1] + + ############################################################################ + #### Federates trace processing + ############################################################################ + # Loop over the given list of federates trace files + if (args.federates) : + for fed_trace in args.federates[0]: + if (not exists(fed_trace)): + print('Warning: Trace file ' + fed_trace + ' does not exist! Will resume though') + continue + fed_df = load_and_process_csv_file(fed_trace) + if (not fed_df.empty): + # Get the federate id number + fed_id = fed_df.iloc[-1]['self_id'] + # Add to the list of sequence diagram actors and add the name + actors.append(fed_id) + actors_names[fed_id] = Path(fed_trace).stem + # Derive the x coordinate of the actor + x_coor[fed_id] = (padding * 2) + (spacing * (len(actors)-1)) + fed_df['x1'] = x_coor[fed_id] + # Append into trace_df + trace_df = trace_df.append(fed_df, sort=False, ignore_index=True) + fed_df = fed_df[0:0] + + # Sort all traces by physical time and then reset the index + trace_df = trace_df.sort_values(by=['physical_time']) + trace_df = trace_df.reset_index(drop=True) + + # FIXME: For now, we need to remove the rows with negative physical time values... + # Until the reason behinf such values is investigated. The negative physical + # time is when federates are still in the process of joining + # trace_df = trace_df[trace_df['physical_time'] >= 0] + + # Add the Y column and initialize it with the padding value + trace_df['y1'] = math.ceil(padding * 3 / 2) # Or set a small shift + + ############################################################################ + #### Compute the 'y1' coordinates + ############################################################################ + ppt = 0 # Previous physical time + cpt = 0 # Current physical time + py = 0 # Previous y + min = 15 # Minimum spacing between events when time has not advanced. + scale = 1 # Will probably be set manually + first_pass = True + for index, row in trace_df.iterrows(): + if (not first_pass) : + cpt = row['physical_time'] + # print('cpt = '+str(cpt)+' and ppt = '+str(ppt)) + # From the email: + # Y = T_previous + min + log10(1 + (T - T_previous)*scale) + # But rather think it should be: + if (cpt != ppt) : + py = math.ceil(py + min + (1 + math.log10(cpt - ppt) * scale)) + trace_df.at[index, 'y1'] = py + + ppt = row['physical_time'] + py = trace_df.at[index, 'y1'] + first_pass = False + + ############################################################################ + #### Derive arrows that match sided communications + ############################################################################ + # Intialize all rows as pending to be matched + trace_df['arrow'] = 'pending' + trace_df['x2'] = -1 + trace_df['y2'] = -1 + + # Iterate and check possible sides + for index in trace_df.index: + # If the tracepoint is pending, proceed to look for a match + if (trace_df.at[index,'arrow'] == 'pending') : + # Look for a match only if it is not about advancing time + if (trace_df.at[index,'event'] == 'AdvLT') : + trace_df.at[index,'arrow'] = 'adv' + continue + self_id = trace_df.at[index,'self_id'] + partner_id = trace_df.at[index,'partner_id'] + event = trace_df.at[index,'event'] + logical_time = trace_df.at[index, 'logical_time'] + microstep = trace_df.at[index, 'microstep'] + inout = trace_df.at[index, 'inout'] + + # Match tracepoints + matching_df = trace_df[\ + (trace_df['inout'] != inout) & \ + (trace_df['self_id'] == partner_id) & \ + (trace_df['partner_id'] == self_id) & \ + (trace_df['arrow'] == 'pending') & \ + (trace_df['event'] == event) & \ + (trace_df['logical_time'] == logical_time) & \ + (trace_df['microstep'] == microstep) \ + ] + + if (matching_df.empty) : + # If no matching receiver, than set the arrow to 'dot', + # meaning that only a dot will be rendered + trace_df.loc[index, 'arrow'] = 'dot' + else: + # If there is one or more matching rows, then consider + # the first one, since it is an out -> in arrow, and + # since it is the closet in time + # FIXME: What other possible choices to consider? + if (inout == 'out'): + matching_index = matching_df.index[0] + matching_row = matching_df.loc[matching_index] + trace_df.at[index, 'x2'] = matching_row['x1'] + trace_df.at[index, 'y2'] = matching_row['y1'] + else: + matching_index = matching_df.index[-1] + matching_row = matching_df.loc[matching_index] + trace_df.at[index, 'x2'] = trace_df.at[index, 'x1'] + trace_df.at[index, 'y2'] = trace_df.at[index, 'y1'] + trace_df.at[index, 'x1'] = matching_row['x1'] + trace_df.at[index, 'y1'] = matching_row['y1'] + + # Mark it, so not to consider it anymore + trace_df.at[matching_index, 'arrow'] = 'marked' + + trace_df.at[index, 'arrow'] = 'arrow' + + ############################################################################ + #### Write to svg file + ############################################################################ + svg_width = padding * 2 + (len(actors) - 1) * spacing + padding * 2 + 200 + svg_height = padding + trace_df.iloc[-1]['y1'] + + with open('trace_svg.html', 'w', encoding='utf-8') as f: + # Print header + f.write('\n') + f.write('\n') + f.write('\n\n') + + f.write('\n') + + f.write(css_style) + + # Print the circles and the names + for key in x_coor: + title = actors_names[key] + if (key == -1): + f.write(fhlp.svg_string_comment('RTI Actor and line')) + center = 15 + else: + f.write(fhlp.svg_string_comment('Federate '+str(key)+': ' + title + ' Actor and line')) + center = 5 + f.write(fhlp.svg_string_draw_line(x_coor[key], math.ceil(padding/2), x_coor[key], svg_height, False)) + f.write('\t\n') + f.write('\t'+title+'\n') + + # Now, we need to iterate over the traces to draw the lines + f.write(fhlp.svg_string_comment('Draw interactions')) + for index, row in trace_df.iterrows(): + # For time labels, display them on the left for the RTI, right for everthing else. + anchor = 'start' + if (row['self_id'] < 0): + anchor = 'end' + + # formatted physical time. + # FIXME: Using microseconds is hardwired here. + physical_time = f'{int(row["physical_time"]/1000):,}' + + if (row['event'] in {'FED_ID', 'ACK', 'REJECT', 'ADR_RQ', 'ADR_AD', 'MSG', 'P2P_MSG'}): + label = row['event'] + elif (row['logical_time'] == -1678240241788173894) : + # FIXME: This isn't right. NEVER == -9223372036854775808. + label = row['event'] + '(NEVER)' + else: + label = row['event'] + '(' + f'{int(row["logical_time"]):,}' + ', ' + str(row['microstep']) + ')' + + if (row['arrow'] == 'arrow'): + f.write(fhlp.svg_string_draw_arrow(row['x1'], row['y1'], row['x2'], row['y2'], label, row['event'])) + f.write(fhlp.svg_string_draw_side_label(row['x1'], row['y1'], physical_time, anchor)) + elif (row['arrow'] == 'dot'): + if (row['inout'] == 'in'): + label = "(in) from " + str(row['partner_id']) + ' ' + label + else : + label = "(out) to " + str(row['partner_id']) + ' ' + label + + if (anchor == 'end'): + f.write(fhlp.svg_string_draw_side_label(row['x1'], row['y1'], physical_time, anchor)) + f.write(fhlp.svg_string_draw_dot(row['x1'], row['y1'], label)) + else: + f.write(fhlp.svg_string_draw_dot_with_time(row['x1'], row['y1'], physical_time, label)) + + elif (row['arrow'] == 'marked'): + f.write(fhlp.svg_string_draw_side_label(row['x1'], row['y1'], physical_time, anchor)) + + elif (row['arrow'] == 'adv'): + f.write(fhlp.svg_string_draw_adv(row['x1'], row['y1'], label)) + + f.write('\n\n\n') + + # Print footer + f.write('\n') + f.write('\n') + + # Write to a csv file, just to double check + trace_df.to_csv('all.csv', index=True) \ No newline at end of file diff --git a/util/tracing/visualization/fedsd_helper.py b/util/tracing/visualization/fedsd_helper.py new file mode 100644 index 0000000000..804341f117 --- /dev/null +++ b/util/tracing/visualization/fedsd_helper.py @@ -0,0 +1,241 @@ +import math + +# Disctionary for pruning event names. Usefule for tracepoint matching and +# communication rendering +prune_event_name = { + "Sending ACK": "ACK", + "Sending TIMESTAMP": "TIMESTAMP", + "Sending NET": "NET", + "Sending LTC": "LTC", + "Sending STOP_REQ": "STOP_REQ", + "Sending STOP_REQ_REP": "STOP_REQ_REP", + "Sending STOP_GRN": "STOP_GRN", + "Sending FED_ID": "FED_ID", + "Sending PTAG": "PTAG", + "Sending TAG": "TAG", + "Sending REJECT": "REJECT", + "Sending RESIGN": "RESIGN", + "Sending PORT_ABS": "ABS", + "Sending CLOSE_RQ": "CLOSE_RQ", + "Sending TAGGED_MSG": "T_MSG", + "Sending P2P_TAGGED_MSG": "P2P_T_MSG", + "Sending MSG": "MSG", + "Sending P2P_MSG": "P2P_MSG", + "Sending ADR_AD": "ADR_AD", + "Sending ADR_QR": "ADR_QR", + "Receiving ACK": "ACK", + "Receiving TIMESTAMP": "TIMESTAMP", + "Receiving NET": "NET", + "Receiving LTC": "LTC", + "Receiving STOP_REQ": "STOP_REQ", + "Receiving STOP_REQ_REP": "STOP_REQ_REP", + "Receiving STOP_GRN": "STOP_GRN", + "Receiving FED_ID": "FED_ID", + "Receiving PTAG": "PTAG", + "Receiving TAG": "TAG", + "Receiving REJECT": "REJECT", + "Receiving RESIGN": "RESIGN", + "Receiving PORT_ABS": "ABS", + "Receiving CLOSE_RQ": "CLOSE_RQ", + "Receiving TAGGED_MSG": "T_MSG", + "Receiving P2P_TAGGED_MSG": "P2P_T_MSG", + "Receiving MSG": "MSG", + "Receiving P2P_MSG": "P2P_MSG", + "Receiving ADR_AD": "ADR_AD", + "Receiving ADR_QR": "ADR_QR", + "Receiving UNIDENTIFIED": "UNIDENTIFIED", + "Scheduler advancing time ends": "AdvLT" +} + +prune_event_name.setdefault(" ", "UNIDENTIFIED") + +################################################################################ +### Routines to write to csv file +################################################################################ + +def svg_string_draw_line(x1, y1, x2, y2, type=''): + ''' + Constructs the svg html string to draw a line from (x1, y1) to (x2, y2). + + Args: + * x1: Int X coordinate of the source point + * y1: Int Y coordinate of the source point + * x2: Int X coordinate of the sink point + * y2: Int Y coordinate of the sink point + * type: The type of the message (for styling) + Returns: + * String: the svg string of the lineĀ© + ''' + str_line = '\t\n' + return str_line + + +def svg_string_draw_arrow_head(x1, y1, x2, y2, type='') : + ''' + Constructs the svg html string to draw the arrow end + + Args: + * x1: Int X coordinate of the source point + * y1: Int Y coordinate of the source point + * x2: Int X coordinate of the sink point + * y2: Int Y coordinate of the sink point + * type: The type (for styling) + Returns: + * String: the svg string of the triangle + ''' + + rotation = - math.ceil(math.atan((x2-x1)/(y2-y1)) * 180 / 3.14) - 90 + style = '' + if (type): + style = ' class="'+type+'"' + + str_line = '' + if (x1 > x2) : + str_line = '\t\n' + else : + str_line = '\t\n' + + return str_line + + +def svg_string_draw_label(x1, y1, x2, y2, label) : + ''' + Computes the rotation angle of the text and then constructs the svg string. + + Args: + * x1: Int X coordinate of the source point + * y1: Int Y coordinate of the source point + * x2: Int X coordinate of the sink point + * y2: Int Y coordinate of the sink point + * label: The label to draw + Returns: + * String: the svg string of the text + ''' + # FIXME: Need further improvement, based of the position of the arrows + # FIXME: Rotation value is not that accurate. + if (x2 < x1) : + # Left-going arrow. + rotation = - math.ceil(math.atan((x2-x1)/(y2-y1)) * 180 / 3.14) - 90 + str_line = '\t'+label+'\n' + else : + # Right-going arrow. + rotation = - math.ceil(math.atan((x1-x2)/(y1-y2)) * 180 / 3.14) + 90 + str_line = '\t'+label+'\n' + #print('rot = '+str(rotation)+' x1='+str(x1)+' y1='+str(y1)+' x2='+str(x2)+' y2='+str(y2)) + return str_line + + +def svg_string_draw_arrow(x1, y1, x2, y2, label, type=''): + ''' + Constructs the svg html string to draw the arrow from (x1, y1) to (x2, y2). + The arrow end is constructed, together with the label + + Args: + * x1: Int X coordinate of the source point + * y1: Int Y coordinate of the source point + * x2: Int X coordinate of the sink point + * y2: Int Y coordinate of the sink point + * label: String Label to draw on top of the arrow + * type: The type of the message + Returns: + * String: the svg string of the arrow + ''' + str_line1 = svg_string_draw_line(x1, y1, x2, y2, type) + str_line2 = svg_string_draw_arrow_head(x1, y1, x2, y2, type) + str_line3 = svg_string_draw_label(x1, y1, x2, y2, label) + return str_line1 + str_line2 + str_line3 + +def svg_string_draw_side_label(x, y, label, anchor="start") : + ''' + Put a label to the right of the x, y point, + unless x is small, in which case put it to the left. + + Args: + * x: Int X coordinate of the source point + * y: Int Y coordinate of the source point + * label: Label to put by the point. + * anchor: One of "start", "middle", or "end" to specify the text-anchor. + Returns: + * String: the svg string of the text + ''' + offset = 5 + if (anchor == 'end'): + offset = -5 + elif (anchor == 'middle'): + offset = 0 + str_line = '\t'+label+'\n' + + return str_line + +def svg_string_comment(comment): + ''' + Constructs the svg html string to write a comment into an svg file. + + Args: + * comment: String Comment to add + Returns: + * String: the svg string of the comment + ''' + str_line = '\n\t\n' + return str_line + + +def svg_string_draw_dot(x, y, label) : + ''' + Constructs the svg html string to draw at a dot. + + Args: + * x: Int X coordinate of the dot + * y: Int Y coordinate of the dot + * label: String to draw + Returns: + * String: the svg string of the triangle + ''' + str_line = '' + str_line = '\t\n' + str_line = str_line + '\t'+label+'\n' + return str_line + +def svg_string_draw_dot_with_time(x, y, time, label) : + ''' + Constructs the svg html string to draw at a dot with a prefixed physical time. + + Args: + * x: Int X coordinate of the dot + * y: Int Y coordinate of the dot + * time: The time + * label: String to draw + Returns: + * String: the svg string of the triangle + ''' + str_line = '' + str_line = '\t\n' + str_line = str_line + '\t '+time+': '+label+'\n' + return str_line + +def svg_string_draw_adv(x, y, label) : + ''' + Constructs the svg html string to draw at a dash, meaning that logical time is advancing there. + + Args: + * x: Int X coordinate of the dash + * y: Int Y coordinate of the dash + * label: String to draw + Returns: + * String: the svg string of the triangle + ''' + str_line1 = svg_string_draw_line(x-5, y, x+5, y, "ADV") + str_line2 = svg_string_draw_side_label(x, y, label) + return str_line1 + str_line2 \ No newline at end of file