From f855471bb851b53784aa9eb91e02c57c967c133e Mon Sep 17 00:00:00 2001 From: iwatake Date: Sun, 28 Jul 2024 14:47:45 +0900 Subject: [PATCH] feat: add topic analysis report (#179) * feat: add topic analysis report Signed-off-by: takeshi.iwanari * fix filename Signed-off-by: takeshi.iwanari --------- Signed-off-by: takeshi.iwanari --- .../analyze_path/add_path_to_architecture.py | 4 +- report/analyze_topic/analyze_topic.py | 248 ++++++++++++++++++ .../make_report_analyze_topic.py | 124 +++++++++ .../analyze_topic/template_topic_detail.html | 89 +++++++ .../analyze_topic/template_topic_index.html | 43 +++ report/report_analysis/analyze_all.py | 2 + report/report_analysis/make_html_analysis.py | 20 +- report/report_analysis/make_report.sh | 5 + .../template_html_analysis.html | 10 + 9 files changed, 541 insertions(+), 4 deletions(-) create mode 100644 report/analyze_topic/analyze_topic.py create mode 100644 report/analyze_topic/make_report_analyze_topic.py create mode 100644 report/analyze_topic/template_topic_detail.html create mode 100644 report/analyze_topic/template_topic_index.html diff --git a/report/analyze_path/add_path_to_architecture.py b/report/analyze_path/add_path_to_architecture.py index d59bdacc..2fc31bbc 100644 --- a/report/analyze_path/add_path_to_architecture.py +++ b/report/analyze_path/add_path_to_architecture.py @@ -272,13 +272,13 @@ def add_path_to_architecture(args, arch: Architecture): child.append(terminal_node) arch.add_path(target_path_name, PathStructValue(target_path_name, child)) - arch.export(args.architecture_file_path, force=True) - if args.use_latest_message: # convert_context_type_to_use_latest_message(args.architecture_file_path, # args.architecture_file_path) convert_context_type_to_use_latest_message(arch) + arch.export(args.architecture_file_path, force=True) + _logger.info('<<< Add Path: Finish >>>') return arch diff --git a/report/analyze_topic/analyze_topic.py b/report/analyze_topic/analyze_topic.py new file mode 100644 index 00000000..43f721bf --- /dev/null +++ b/report/analyze_topic/analyze_topic.py @@ -0,0 +1,248 @@ +# Copyright 2022 Tier IV, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Script to analyze topics +""" +from __future__ import annotations +from enum import Enum +import sys +import os +from pathlib import Path +import argparse +from distutils.util import strtobool +import logging +import math +import yaml +import numpy as np +import pandas as pd +from bokeh.plotting import figure +from caret_analyze import Architecture, Application, Lttng +from caret_analyze.runtime.communication import Communication +from caret_analyze.runtime.callback import CallbackBase, CallbackType +from caret_analyze.plot import Plot, PlotBase +sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/..') +from common.utils import create_logger, make_destination_dir, read_trace_data, export_graph, trail_df +from common.utils import round_yaml, get_callback_legend +from common.utils import ComponentManager + +# Suppress log for CARET +from logging import getLogger, FATAL +logger = getLogger() +logger.setLevel(FATAL) + +# Suppress log for Bokeh "BokehUserWarning: out of range integer may result in loss of precision" +import warnings +warnings.simplefilter("ignore") + +_logger: logging.Logger = None + + +class Metrics(Enum): + FREQUENCY = 1 + PERIOD = 2 + LATENCY = 3 + + +def get_comm_plot(comm: Communication, metrics: Metrics): + # todo: create_communication_frequency_plot doesn't work (it's stuck and consumes too much memory) + if metrics == Metrics.FREQUENCY: + return Plot.create_frequency_timeseries_plot([comm.publisher, comm.subscription]) + elif metrics == Metrics.PERIOD: + return Plot.create_period_timeseries_plot([comm.publisher, comm.subscription]) + elif metrics == Metrics.LATENCY: + return Plot.create_latency_timeseries_plot(comm) + + +class StatsComm(): + """Statistics of comm""" + def __init__(self, topic_name, publish_node_name, subscribe_node_name): + self.topic_name = topic_name + self.publish_node_name = publish_node_name + self.subscribe_node_name = subscribe_node_name + self.filename = '' + self.avg = '---' + self.std = '---' + self.min = '---' + self.max = '---' + self.p50 = '---' + self.p95 = '---' + self.p99 = '---' + + def calculate(self, data: pd.DataFrame): + """Calculate stats""" + if len(data) > 1: + self.avg = round(float(data.mean()), 3) + self.std = round(float(data.std()), 3) + self.p50 = round(float(np.quantile(data, 0.5)), 3) + self.p95 = round(float(np.quantile(data, 0.95)), 3) + self.p99 = round(float(np.quantile(data, 0.99)), 3) + if len(data) > 0: + self.min = round(float(data.min()), 3) + self.max = round(float(data.max()), 3) + + +def create_stats_for_comm(comm: Communication, index: int, metrics: Metrics, dest_dir: str, xaxis_type: str, with_graph: bool) -> StatsComm: + stats_comm = StatsComm(comm.topic_name, comm.publish_node_name, comm.subscribe_node_name) + try: + timeseries_plot = get_comm_plot(comm, metrics) + if with_graph: + figure = timeseries_plot.figure(xaxis_type=xaxis_type) + figure.y_range.start = 0 + graph_filename = metrics.name + comm.topic_name.replace('/', '_') + '_' + str(index) + graph_filefilename_suffix = comm.subscribe_node_name.replace('/', '_') + graph_filename = graph_filename + graph_filefilename_suffix[:120-len(graph_filename)] # avoid too long file name + stats_comm.filename = graph_filename + export_graph(figure, dest_dir, graph_filename, with_png=False, logger=_logger) + df_comm = timeseries_plot.to_dataframe(xaxis_type=xaxis_type) + df_comm = df_comm.iloc[:, 1] # get metrics value only (use value of publish. df=|time|pub|time|sub|) + df_comm = trail_df(df_comm, end_strip_num=2) # remove the last data because freq becomes small + stats_comm.calculate(df_comm) + except: + _logger.info(f'This comm is invalid: {comm.topic_name}: {comm.publish_node_name} -> {comm.subscribe_node_name}') + return None + return stats_comm + + +def analyze_comms(topic_name: str, comm_list: list[Communication], dest_dir: str, xaxis_type: str, threshold_freq_not_display: int=300) -> dict[str, list[StatsComm]]: + """Analyze topic (communications)""" + _logger.info(f'Processing {topic_name}') + + stats_dict: dict[str, list[StatsComm]] = {} + skip_list = [] + + for metrics in Metrics: + for index, comm in enumerate(comm_list): + with_graph = True + if comm in skip_list and metrics != Metrics.FREQUENCY: + with_graph = False + stats_comm = create_stats_for_comm(comm, index, metrics, dest_dir, xaxis_type, with_graph) + if stats_comm is None: + continue + if metrics == Metrics.FREQUENCY: + freq = stats_comm.avg + if isinstance(freq, int) or isinstance(freq, float): + if freq >= threshold_freq_not_display: + _logger.info(f'{comm.topic_name} is not displayed in graph') + skip_list.append(comm) + stats_dict.setdefault(metrics.name, []) + stats_dict[metrics.name].append(stats_comm) + return stats_dict + + +def analyze_topic(app: Application, topic_name: str, dest_dir: str, xaxis_type: str): + """Analyze a topic""" + try: + comm_list: list[Communication] = app.get_communications(topic_name) + except: + _logger.info(f'No communication for {topic_name}') + return + if not comm_list: + return + + make_destination_dir(dest_dir, False, _logger) + stats_dict = analyze_comms(topic_name, comm_list, dest_dir, xaxis_type) + if not stats_dict: + return + for metrics_name, stats_list in stats_dict.items(): + stats_var_list = [] + for stats in stats_list: + stats_var_list.append(vars(stats)) + stat_file_path = f"{dest_dir}/stats_{metrics_name}.yaml" + with open(stat_file_path, 'w', encoding='utf-8') as f_yaml: + yaml.safe_dump(stats_var_list, f_yaml, encoding='utf-8', allow_unicode=True, sort_keys=False) + round_yaml(stat_file_path) + + +def analyze_component(app: Application, topic_name_list: list[str], dest_dir: str, xaxis_type: str): + """Analyze a component""" + make_destination_dir(dest_dir, False, _logger) + for topic_name in topic_name_list: + topic_dest_dir = f"{dest_dir}/{topic_name.replace('/', '_').lstrip('_')}" + analyze_topic(app, topic_name, topic_dest_dir, xaxis_type) + + +def create_component_topic_dict(arch: Architecture) -> dict[str, list[str]]: + dict_component_name_topic: dict[str, list[str]] = {} + for topic_name in arch.topic_names: + is_match = False + for component_name, _ in ComponentManager().component_dict.items(): + if ComponentManager().check_if_target(component_name, topic_name): + dict_component_name_topic.setdefault(component_name, []) + dict_component_name_topic[component_name].append(topic_name) + is_match = True + break + if not is_match: + dict_component_name_topic.setdefault('other', []) + dict_component_name_topic['other'].append(topic_name) + return dict_component_name_topic + + +def analyze(args, lttng: Lttng, arch: Architecture, app: Application, dest_dir: str): + """Analyze topics""" + global _logger + if _logger is None: + _logger = create_logger(__name__, logging.DEBUG if args.verbose else logging.INFO) + _logger.info('<<< Analyze Topic: Start >>>') + make_destination_dir(dest_dir, args.force, _logger) + arch.export(dest_dir + '/architecture.yaml', force=True) + ComponentManager().initialize(args.component_list_json, _logger) + dict_component_name_topic = create_component_topic_dict(arch) + + for component_name, topic_name_list in dict_component_name_topic.items(): + analyze_component(app, topic_name_list, f'{dest_dir}/{component_name}', 'sim_time' if args.sim_time else 'system_time') + + _logger.info('<<< Analyze Topic: Finish >>>') + + +def parse_arg(): + """Parse arguments""" + parser = argparse.ArgumentParser( + description='Script to analyze node callback functions') + parser.add_argument('trace_data', nargs=1, type=str) + parser.add_argument('dest_dir', nargs=1, type=str) + parser.add_argument('--component_list_json', type=str, default='') + parser.add_argument('--start_strip', type=float, default=0.0, + help='Start strip [sec] to load trace data') + parser.add_argument('--end_strip', type=float, default=0.0, + help='End strip [sec] to load trace data') + parser.add_argument('--sim_time', type=strtobool, default=False) + parser.add_argument('-f', '--force', action='store_true', default=False, + help='Overwrite report directory') + parser.add_argument('-v', '--verbose', action='store_true', default=False) + args = parser.parse_args() + return args + + +def main(): + """Main function""" + global _logger + args = parse_arg() + _logger = create_logger(__name__, logging.DEBUG if args.verbose else logging.INFO) + + _logger.debug(f'trace_data: {args.trace_data[0]}') + _logger.debug(f'dest_dir: {args.dest_dir[0]}') + _logger.debug(f'component_list_json: {args.component_list_json}') + _logger.debug(f'start_strip: {args.start_strip}, end_strip: {args.end_strip}') + _logger.debug(f'sim_time: {args.sim_time}') + + lttng = read_trace_data(args.trace_data[0], args.start_strip, args.end_strip, False) + arch = Architecture('lttng', str(args.trace_data[0])) + app = Application(arch, lttng) + + dest_dir = args.dest_dir[0] + analyze(args, lttng, arch, app, dest_dir + '/analyze_topic') + + +if __name__ == '__main__': + main() diff --git a/report/analyze_topic/make_report_analyze_topic.py b/report/analyze_topic/make_report_analyze_topic.py new file mode 100644 index 00000000..47ef5c88 --- /dev/null +++ b/report/analyze_topic/make_report_analyze_topic.py @@ -0,0 +1,124 @@ +# Copyright 2022 Tier IV, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Script to make report page +""" +import glob +import argparse +from pathlib import Path +import sys +import yaml +import flask + +app = flask.Flask(__name__) + + +def render_index_page(dest_path: Path, title: str, sub_title: str, topic_name_list: list[str], topic_html_list: list[str]): + """Render html page""" + template_path = f'{Path(__file__).resolve().parent}/template_topic_index.html' + with app.app_context(): + with open(template_path, 'r', encoding='utf-8') as f_html: + template_string = f_html.read() + rendered = flask.render_template_string( + template_string, + title=title, + sub_title=sub_title, + topic_name_list=topic_name_list, + topic_html_list=topic_html_list, + ) + with open(dest_path, 'w', encoding='utf-8') as f_html: + f_html.write(rendered) + + + +def render_detail_page(dest_path: Path, data_path: str, title: str, sub_title: str, stats_freq: list[dict], stats_period: list[dict], stats_latency: list[dict], node_html_dict: dict[str, Path]): + """Render html page""" + template_path = f'{Path(__file__).resolve().parent}/template_topic_detail.html' + with app.app_context(): + with open(template_path, 'r', encoding='utf-8') as f_html: + template_string = f_html.read() + rendered = flask.render_template_string( + template_string, + title=title, + sub_title=sub_title, + data_path=data_path, + stats_list=[stats_freq, stats_period, stats_latency], + metrics_list=['Frequency', 'Period', 'Latency'], + metrics_unit_list=['[Hz]', '[ms]', '[ms]'], + node_html_dict=node_html_dict, + ) + with open(dest_path, 'w', encoding='utf-8') as f_html: + f_html.write(rendered) + + +def make_pages_for_component(path_component: Path, report_name: str, node_html_dict: dict[str, Path]): + topic_path_list = sorted([d for d in path_component.iterdir() if d.is_dir()]) + topic_html_list = [topic_path.name + '.html' for topic_path in topic_path_list] + topic_name_list = [] + for i, topic_path in enumerate(topic_path_list): + with open(topic_path.joinpath('stats_FREQUENCY.yaml'), 'r', encoding='utf-8') as f_yaml_freq, \ + open(topic_path.joinpath('stats_PERIOD.yaml'), 'r', encoding='utf-8') as f_yaml_period, \ + open(topic_path.joinpath('stats_LATENCY.yaml'), 'r', encoding='utf-8') as f_yaml_latency: + stats_freq = yaml.safe_load(f_yaml_freq) + stats_period = yaml.safe_load(f_yaml_period) + stats_latency = yaml.safe_load(f_yaml_latency) + if len(stats_freq) == 0: + continue + topic_name = stats_freq[0]['topic_name'] + topic_name_list.append(topic_name) + render_detail_page(path_component.joinpath(topic_html_list[i]), topic_path.name, f'Topic: {topic_name}', report_name, stats_freq, stats_period, stats_latency, node_html_dict) + + render_index_page(path_component.joinpath('index.html'), f'Topic: {path_component.name}', report_name, topic_name_list, topic_html_list) + + +def create_node_html_dict(dest_dir: Path) -> dict[str, str]: + node_dir = dest_dir.parent.joinpath('analyze_node') + node_html_dict: dict[str, str] = {} + node_html_path = glob.glob(f'{node_dir}/**/index_*.html', recursive=True) + for path in node_html_path: + path = Path(path) + node_name = path.name.lstrip('index').rstrip('.html') + node_html_dict[node_name] = Path('../../').joinpath(path.absolute().relative_to(dest_dir.absolute().parent)) + return node_html_dict + + +def make_report(dest_dir: str): + """Make report page""" + dest_dir = Path(dest_dir) + report_name = dest_dir.parent.name + node_html_dict = create_node_html_dict(dest_dir) + + path_component_list = [d for d in dest_dir.iterdir() if d.is_dir()] + for path_component in path_component_list: + make_pages_for_component(path_component, report_name, node_html_dict) + + +def parse_arg(): + """Parse arguments""" + parser = argparse.ArgumentParser( + description='Script to make report page') + parser.add_argument('dest_dir', nargs=1, type=str) + args = parser.parse_args() + return args + + +def main(): + """main function""" + args = parse_arg() + + dest_dir = args.dest_dir[0] + make_report(dest_dir + '/analyze_topic') + +if __name__ == '__main__': + main() diff --git a/report/analyze_topic/template_topic_detail.html b/report/analyze_topic/template_topic_detail.html new file mode 100644 index 00000000..beb640eb --- /dev/null +++ b/report/analyze_topic/template_topic_detail.html @@ -0,0 +1,89 @@ + + + + + + + + {{ title }}, {{ sub_title }} + + + +
+

{{ title }}

+

{{ sub_title }}

+ +

Back to Top, Back to Topic List

+
+ + {% for metrics in metrics_list %} +

{{ metrics }}

+ {% set stats = stats_list[loop.index0] %} + {% set metrics_unit = metrics_unit_list[loop.index0] %} + + + + + + + + + + + + + {% for item in stats %} + {%set publish_node_name = item['publish_node_name'] %} + {%set subscribe_node_name = item['subscribe_node_name'] %} + {%set publish_node_name_for_dict = item['publish_node_name'] | replace('/', '_') %} + {%set subscribe_node_name_for_dict = item['subscribe_node_name'] | replace('/', '_') %} + + + + {% if publish_node_name_for_dict in node_html_dict %} + + {% else %} + + {% endif %} + {% if subscribe_node_name_for_dict in node_html_dict %} + + {% else %} + + {% endif %} + + + + + + {% endfor %} +
#TopicPublisherSubscriberAvg {{ metrics_unit }}Min {{ metrics_unit }}Max {{ metrics_unit }}99%ile {{ metrics_unit }}
{{ loop.index0 }}{{ item['topic_name'] }}{{ publish_node_name }}{{ publish_node_name }}{{ subscribe_node_name }}{{ subscribe_node_name }}{{ '%.3f' % item['avg']|float }}{{ '%.3f' % item['min']|float }}{{ '%.3f' % item['max']|float }}{{ '%.3f' % item['p99']|float }}
+ + {% for item in stats %} + {% if item['filename'] != '' %} +

{{ loop.index0 }}. {{ item['topic_name'] }}: {{ item['publish_node_name'] }} -> {{ item['subscribe_node_name'] }}

+ + {% endif %} + {% endfor %} + +
+ + {% endfor %} +
+ + + + + diff --git a/report/analyze_topic/template_topic_index.html b/report/analyze_topic/template_topic_index.html new file mode 100644 index 00000000..3bec4f07 --- /dev/null +++ b/report/analyze_topic/template_topic_index.html @@ -0,0 +1,43 @@ + + + + + + + + {{ title }}, {{ sub_title }} + + + +
+

{{ title }}

+

{{ sub_title }}

+ +

Back to Top

+
+ +

Topic List

+ +
+ + + + + diff --git a/report/report_analysis/analyze_all.py b/report/report_analysis/analyze_all.py index 78418801..ed7e91f3 100644 --- a/report/report_analysis/analyze_all.py +++ b/report/report_analysis/analyze_all.py @@ -26,6 +26,7 @@ from common.utils import create_logger, read_trace_data from analyze_node import analyze_node from analyze_path import add_path_to_architecture, analyze_path +from analyze_topic import analyze_topic from find_valid_duration import find_valid_duration @@ -122,6 +123,7 @@ def main(): if not args.is_path_analysis_only: analyze_node.analyze(args, lttng, arch, app, args.dest_dir + '/analyze_node') + analyze_topic.analyze(args, lttng, arch, app, args.dest_dir + '/analyze_topic') if __name__ == '__main__': diff --git a/report/report_analysis/make_html_analysis.py b/report/report_analysis/make_html_analysis.py index 3a405d56..c2ca83b1 100644 --- a/report/report_analysis/make_html_analysis.py +++ b/report/report_analysis/make_html_analysis.py @@ -30,7 +30,7 @@ def render_page(title, sub_title, destination_path, template_path, component_list, stats_node_dict, - stats_path, note_text_top, note_text_bottom, num_back): + stats_path, component_list_for_topic, note_text_top, note_text_bottom, num_back): """Render html page""" with app.app_context(): with open(template_path, 'r', encoding='utf-8') as f_html: @@ -42,6 +42,7 @@ def render_page(title, sub_title, destination_path, template_path, component_lis component_list=component_list, stats_node_dict=stats_node_dict, stats_path=stats_path, + component_list_for_topic=component_list_for_topic, note_text_top=note_text_top, note_text_bottom=note_text_bottom, link_back='../' * num_back + 'index.html' if num_back > 0 else '' @@ -106,6 +107,19 @@ def find_latency_topk(component_name, stats_node, numk=20) -> None: stats_node['latency_topk'] = callback_latency_list +def get_component_list_for_topic(report_dir: str) -> list[str]: + """Create component name list in topic analysis""" + component_list = list(ComponentManager().component_dict.keys()) + component_list.append('other') + remove_list = [] + for component_name in component_list: + if not os.path.isfile(Path(report_dir).joinpath('analyze_topic').joinpath(component_name).joinpath('index.html')): + remove_list.append(component_name) + for remove_item in remove_list: + component_list.remove(remove_item) + return component_list + + def make_report(args, index_filename: str='index'): """Make report page""" trace_data_dir = args.trace_data[0].rstrip('/') @@ -119,6 +133,8 @@ def make_report(args, index_filename: str='index'): stats_path = get_stats_path(dest_dir) + component_list_for_topic = get_component_list_for_topic(dest_dir) + note_text_top, note_text_bottom = read_note_text(trace_data_dir, dest_dir, args.note_text_top, args.note_text_bottom) destination_path = f'{dest_dir}/{index_filename}.html' @@ -126,7 +142,7 @@ def make_report(args, index_filename: str='index'): title = 'Analysis report' sub_title = dest_dir.split('/')[-1] render_page(title, sub_title, destination_path, template_path, component_list, stats_node_dict, - stats_path, note_text_top, note_text_bottom, args.num_back) + stats_path, component_list_for_topic, note_text_top, note_text_bottom, args.num_back) def parse_arg(): diff --git a/report/report_analysis/make_report.sh b/report/report_analysis/make_report.sh index f952d3f0..06247dde 100644 --- a/report/report_analysis/make_report.sh +++ b/report/report_analysis/make_report.sh @@ -54,6 +54,7 @@ if ${use_python}; then python3 "${script_path}"/analyze_node/make_report_analyze_node.py "${report_dir_name}" python3 "${script_path}"/analyze_path/make_report_analyze_path.py "${report_dir_name}" python3 "${script_path}"/track_path/make_report_track_path.py "${report_dir_name}" "${report_store_dir}" --relpath_from_report_store_dir="${relpath_from_report_store_dir}" + python3 "${script_path}"/analyze_topic/make_report_analyze_topic.py "${report_dir_name}" python3 "${script_path}"/report_analysis/make_html_analysis.py "${trace_data}" "${report_dir_name}" --note_text_top "${note_text_top}" --note_text_bottom "${note_text_bottom}" --num_back 3 else # Path analysis @@ -68,6 +69,10 @@ else python3 "${script_path}"/analyze_node/analyze_node.py "${trace_data}" "${report_dir_name}" --component_list_json="${component_list_json}" --start_strip "${start_strip}" --end_strip "${end_strip}" --sim_time "${sim_time}" -f -v python3 "${script_path}"/analyze_node/make_report_analyze_node.py "${report_dir_name}" + # Topic analysis + python3 "${script_path}"/analyze_topic/analyze_topic.py "${trace_data}" "${report_dir_name}" --component_list_json="${component_list_json}" --start_strip "${start_strip}" --end_strip "${end_strip}" --sim_time "${sim_time}" -f -v + python3 "${script_path}"/analyze_topic/make_report_analyze_topic.py "${report_dir_name}" + # Make top page python3 "${script_path}"/report_analysis/make_html_analysis.py "${trace_data}" "${report_dir_name}" --note_text_top "${note_text_top}" --note_text_bottom "${note_text_bottom}" --num_back 3 fi diff --git a/report/report_analysis/template_html_analysis.html b/report/report_analysis/template_html_analysis.html index ff9c8234..d350e12b 100644 --- a/report/report_analysis/template_html_analysis.html +++ b/report/report_analysis/template_html_analysis.html @@ -49,6 +49,16 @@

Node Analysis Report

{% endfor %} +

Topic Analysis Report

+

+ This report shows detailed information (Frequency, Period and Latency) for communications +

+ +

Track of Response Time