diff options
Diffstat (limited to 'csit.infra.dash/app/cdash/trending')
-rw-r--r-- | csit.infra.dash/app/cdash/trending/__init__.py | 12 | ||||
-rw-r--r-- | csit.infra.dash/app/cdash/trending/graphs.py | 679 | ||||
-rw-r--r-- | csit.infra.dash/app/cdash/trending/layout.py | 1721 | ||||
-rw-r--r-- | csit.infra.dash/app/cdash/trending/layout.yaml | 201 | ||||
-rw-r--r-- | csit.infra.dash/app/cdash/trending/trending.py | 52 |
5 files changed, 2665 insertions, 0 deletions
diff --git a/csit.infra.dash/app/cdash/trending/__init__.py b/csit.infra.dash/app/cdash/trending/__init__.py new file mode 100644 index 0000000000..c6a5f639fe --- /dev/null +++ b/csit.infra.dash/app/cdash/trending/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# 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. diff --git a/csit.infra.dash/app/cdash/trending/graphs.py b/csit.infra.dash/app/cdash/trending/graphs.py new file mode 100644 index 0000000000..1a507dfeea --- /dev/null +++ b/csit.infra.dash/app/cdash/trending/graphs.py @@ -0,0 +1,679 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# 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. + +"""Implementation of graphs for trending data. +""" + +import logging +import plotly.graph_objects as go +import pandas as pd + +from ..utils.constants import Constants as C +from ..utils.utils import get_color, get_hdrh_latencies +from ..utils.anomalies import classify_anomalies + + +def select_trending_data(data: pd.DataFrame, itm: dict) -> pd.DataFrame: + """Select the data for graphs from the provided data frame. + + :param data: Data frame with data for graphs. + :param itm: Item (in this case job name) which data will be selected from + the input data frame. + :type data: pandas.DataFrame + :type itm: dict + :returns: A data frame with selected data. + :rtype: pandas.DataFrame + """ + + phy = itm["phy"].split("-") + if len(phy) == 4: + topo, arch, nic, drv = phy + if drv == "dpdk": + drv = "" + else: + drv += "-" + drv = drv.replace("_", "-") + else: + return None + + if itm["testtype"] in ("ndr", "pdr"): + test_type = "ndrpdr" + elif itm["testtype"] == "mrr": + test_type = "mrr" + elif itm["testtype"] == "soak": + test_type = "soak" + elif itm["area"] == "hoststack": + test_type = "hoststack" + df = data.loc[( + (data["test_type"] == test_type) & + (data["passed"] == True) + )] + df = df[df.job.str.endswith(f"{topo}-{arch}")] + core = str() if itm["dut"] == "trex" else f"{itm['core']}" + ttype = "ndrpdr" if itm["testtype"] in ("ndr", "pdr") else itm["testtype"] + df = df[df.test_id.str.contains( + f"^.*[.|-]{nic}.*{itm['framesize']}-{core}-{drv}{itm['test']}-{ttype}$", + regex=True + )].sort_values(by="start_time", ignore_index=True) + + return df + + +def graph_trending( + data: pd.DataFrame, + sel: dict, + layout: dict, + normalize: bool=False + ) -> tuple: + """Generate the trending graph(s) - MRR, NDR, PDR and for PDR also Latences + (result_latency_forward_pdr_50_avg). + + :param data: Data frame with test results. + :param sel: Selected tests. + :param layout: Layout of plot.ly graph. + :param normalize: If True, the data is normalized to CPU frquency + Constants.NORM_FREQUENCY. + :type data: pandas.DataFrame + :type sel: dict + :type layout: dict + :type normalize: bool + :returns: Trending graph(s) + :rtype: tuple(plotly.graph_objects.Figure, plotly.graph_objects.Figure) + """ + + if not sel: + return None, None + + + def _generate_trending_traces( + ttype: str, + name: str, + df: pd.DataFrame, + color: str, + nf: float + ) -> list: + """Generate the trending traces for the trending graph. + + :param ttype: Test type (MRR, NDR, PDR). + :param name: The test name to be displayed as the graph title. + :param df: Data frame with test data. + :param color: The color of the trace (samples and trend line). + :param nf: The factor used for normalization of the results to + CPU frequency set to Constants.NORM_FREQUENCY. + :type ttype: str + :type name: str + :type df: pandas.DataFrame + :type color: str + :type nf: float + :returns: Traces (samples, trending line, anomalies) + :rtype: list + """ + + df = df.dropna(subset=[C.VALUE[ttype], ]) + if df.empty: + return list(), list() + + hover = list() + customdata = list() + customdata_samples = list() + name_lst = name.split("-") + for _, row in df.iterrows(): + h_tput, h_band, h_lat = str(), str(), str() + if ttype in ("mrr", "mrr-bandwidth"): + h_tput = ( + f"tput avg [{row['result_receive_rate_rate_unit']}]: " + f"{row['result_receive_rate_rate_avg'] * nf:,.0f}<br>" + f"tput stdev [{row['result_receive_rate_rate_unit']}]: " + f"{row['result_receive_rate_rate_stdev'] * nf:,.0f}<br>" + ) + if pd.notna(row["result_receive_rate_bandwidth_avg"]): + h_band = ( + f"bandwidth avg " + f"[{row['result_receive_rate_bandwidth_unit']}]: " + f"{row['result_receive_rate_bandwidth_avg'] * nf:,.0f}" + "<br>" + f"bandwidth stdev " + f"[{row['result_receive_rate_bandwidth_unit']}]: " + f"{row['result_receive_rate_bandwidth_stdev']* nf:,.0f}" + "<br>" + ) + elif ttype in ("ndr", "ndr-bandwidth"): + h_tput = ( + f"tput [{row['result_ndr_lower_rate_unit']}]: " + f"{row['result_ndr_lower_rate_value'] * nf:,.0f}<br>" + ) + if pd.notna(row["result_ndr_lower_bandwidth_value"]): + h_band = ( + f"bandwidth [{row['result_ndr_lower_bandwidth_unit']}]:" + f" {row['result_ndr_lower_bandwidth_value'] * nf:,.0f}" + "<br>" + ) + elif ttype in ("pdr", "pdr-bandwidth", "latency"): + h_tput = ( + f"tput [{row['result_pdr_lower_rate_unit']}]: " + f"{row['result_pdr_lower_rate_value'] * nf:,.0f}<br>" + ) + if pd.notna(row["result_pdr_lower_bandwidth_value"]): + h_band = ( + f"bandwidth [{row['result_pdr_lower_bandwidth_unit']}]:" + f" {row['result_pdr_lower_bandwidth_value'] * nf:,.0f}" + "<br>" + ) + if pd.notna(row["result_latency_forward_pdr_50_avg"]): + h_lat = ( + f"latency " + f"[{row['result_latency_forward_pdr_50_unit']}]: " + f"{row['result_latency_forward_pdr_50_avg'] / nf:,.0f}" + "<br>" + ) + elif ttype in ("hoststack-cps", "hoststack-rps", + "hoststack-cps-bandwidth", + "hoststack-rps-bandwidth", "hoststack-latency"): + h_tput = ( + f"tput [{row['result_rate_unit']}]: " + f"{row['result_rate_value'] * nf:,.0f}<br>" + ) + h_band = ( + f"bandwidth [{row['result_bandwidth_unit']}]: " + f"{row['result_bandwidth_value'] * nf:,.0f}<br>" + ) + h_lat = ( + f"latency [{row['result_latency_unit']}]: " + f"{row['result_latency_value'] / nf:,.0f}<br>" + ) + elif ttype in ("hoststack-bps", ): + h_band = ( + f"bandwidth [{row['result_bandwidth_unit']}]: " + f"{row['result_bandwidth_value'] * nf:,.0f}<br>" + ) + elif ttype in ("soak", "soak-bandwidth"): + h_tput = ( + f"tput [{row['result_critical_rate_lower_rate_unit']}]: " + f"{row['result_critical_rate_lower_rate_value'] * nf:,.0f}" + "<br>" + ) + if pd.notna(row["result_critical_rate_lower_bandwidth_value"]): + bv = row['result_critical_rate_lower_bandwidth_value'] + h_band = ( + "bandwidth " + f"[{row['result_critical_rate_lower_bandwidth_unit']}]:" + f" {bv * nf:,.0f}" + "<br>" + ) + try: + hosts = f"<br>hosts: {', '.join(row['hosts'])}" + except (KeyError, TypeError): + hosts = str() + hover_itm = ( + f"dut: {name_lst[0]}<br>" + f"infra: {'-'.join(name_lst[1:5])}<br>" + f"test: {'-'.join(name_lst[5:])}<br>" + f"date: {row['start_time'].strftime('%Y-%m-%d %H:%M:%S')}<br>" + f"{h_tput}{h_band}{h_lat}" + f"{row['dut_type']}-ref: {row['dut_version']}<br>" + f"csit-ref: {row['job']}/{row['build']}" + f"{hosts}" + ) + hover.append(hover_itm) + if ttype == "latency": + customdata_samples.append(get_hdrh_latencies(row, name)) + customdata.append({"name": name}) + else: + customdata_samples.append( + {"name": name, "show_telemetry": True} + ) + customdata.append({"name": name}) + + x_axis = df["start_time"].tolist() + if "latency" in ttype: + y_data = [(v / nf) for v in df[C.VALUE[ttype]].tolist()] + else: + y_data = [(v * nf) for v in df[C.VALUE[ttype]].tolist()] + units = df[C.UNIT[ttype]].unique().tolist() + + try: + anomalies, trend_avg, trend_stdev = classify_anomalies( + {k: v for k, v in zip(x_axis, y_data)} + ) + except ValueError as err: + logging.error(err) + return list(), list() + + hover_trend = list() + for avg, stdev, (_, row) in zip(trend_avg, trend_stdev, df.iterrows()): + try: + hosts = f"<br>hosts: {', '.join(row['hosts'])}" + except (KeyError, TypeError): + hosts = str() + hover_itm = ( + f"dut: {name_lst[0]}<br>" + f"infra: {'-'.join(name_lst[1:5])}<br>" + f"test: {'-'.join(name_lst[5:])}<br>" + f"date: {row['start_time'].strftime('%Y-%m-%d %H:%M:%S')}<br>" + f"trend [{row[C.UNIT[ttype]]}]: {avg:,.0f}<br>" + f"stdev [{row[C.UNIT[ttype]]}]: {stdev:,.0f}<br>" + f"{row['dut_type']}-ref: {row['dut_version']}<br>" + f"csit-ref: {row['job']}/{row['build']}" + f"{hosts}" + ) + if ttype == "latency": + hover_itm = hover_itm.replace("[pps]", "[us]") + hover_trend.append(hover_itm) + + traces = [ + go.Scatter( # Samples + x=x_axis, + y=y_data, + name=name, + mode="markers", + marker={ + "size": 5, + "color": color, + "symbol": "circle", + }, + text=hover, + hoverinfo="text", + showlegend=True, + legendgroup=name, + customdata=customdata_samples + ), + go.Scatter( # Trend line + x=x_axis, + y=trend_avg, + name=name, + mode="lines", + line={ + "shape": "linear", + "width": 1, + "color": color, + }, + text=hover_trend, + hoverinfo="text", + showlegend=False, + legendgroup=name, + customdata=customdata + ) + ] + + if anomalies: + anomaly_x = list() + anomaly_y = list() + anomaly_color = list() + hover = list() + for idx, anomaly in enumerate(anomalies): + if anomaly in ("regression", "progression"): + anomaly_x.append(x_axis[idx]) + anomaly_y.append(trend_avg[idx]) + anomaly_color.append(C.ANOMALY_COLOR[anomaly]) + hover_itm = ( + f"dut: {name_lst[0]}<br>" + f"infra: {'-'.join(name_lst[1:5])}<br>" + f"test: {'-'.join(name_lst[5:])}<br>" + f"date: {x_axis[idx].strftime('%Y-%m-%d %H:%M:%S')}<br>" + f"trend [pps]: {trend_avg[idx]:,.0f}<br>" + f"classification: {anomaly}" + ) + if ttype == "latency": + hover_itm = hover_itm.replace("[pps]", "[us]") + hover.append(hover_itm) + anomaly_color.extend([0.0, 0.5, 1.0]) + traces.append( + go.Scatter( + x=anomaly_x, + y=anomaly_y, + mode="markers", + text=hover, + hoverinfo="text", + showlegend=False, + legendgroup=name, + name=name, + customdata=customdata, + marker={ + "size": 15, + "symbol": "circle-open", + "color": anomaly_color, + "colorscale": C.COLORSCALE_LAT \ + if ttype == "latency" else C.COLORSCALE_TPUT, + "showscale": True, + "line": { + "width": 2 + }, + "colorbar": { + "y": 0.5, + "len": 0.8, + "title": "Circles Marking Data Classification", + "titleside": "right", + "tickmode": "array", + "tickvals": [0.167, 0.500, 0.833], + "ticktext": C.TICK_TEXT_LAT \ + if ttype == "latency" else C.TICK_TEXT_TPUT, + "ticks": "", + "ticklen": 0, + "tickangle": -90, + "thickness": 10 + } + } + ) + ) + + return traces, units + + + fig_tput = None + fig_lat = None + fig_band = None + y_units = set() + for idx, itm in enumerate(sel): + df = select_trending_data(data, itm) + if df is None or df.empty: + continue + + if normalize: + phy = itm["phy"].split("-") + topo_arch = f"{phy[0]}-{phy[1]}" if len(phy) == 4 else str() + norm_factor = (C.NORM_FREQUENCY / C.FREQUENCY.get(topo_arch, 1.0)) \ + if topo_arch else 1.0 + else: + norm_factor = 1.0 + + if itm["area"] == "hoststack": + ttype = f"hoststack-{itm['testtype']}" + else: + ttype = itm["testtype"] + + traces, units = _generate_trending_traces( + ttype, + itm["id"], + df, + get_color(idx), + norm_factor + ) + if traces: + if not fig_tput: + fig_tput = go.Figure() + fig_tput.add_traces(traces) + + if ttype in C.TESTS_WITH_BANDWIDTH: + traces, _ = _generate_trending_traces( + f"{ttype}-bandwidth", + itm["id"], + df, + get_color(idx), + norm_factor + ) + if traces: + if not fig_band: + fig_band = go.Figure() + fig_band.add_traces(traces) + + if ttype in C.TESTS_WITH_LATENCY: + traces, _ = _generate_trending_traces( + "latency" if ttype == "pdr" else "hoststack-latency", + itm["id"], + df, + get_color(idx), + norm_factor + ) + if traces: + if not fig_lat: + fig_lat = go.Figure() + fig_lat.add_traces(traces) + + y_units.update(units) + + if fig_tput: + fig_layout = layout.get("plot-trending-tput", dict()) + fig_layout["yaxis"]["title"] = \ + f"Throughput [{'|'.join(sorted(y_units))}]" + fig_tput.update_layout(fig_layout) + if fig_band: + fig_band.update_layout(layout.get("plot-trending-bandwidth", dict())) + if fig_lat: + fig_lat.update_layout(layout.get("plot-trending-lat", dict())) + + return fig_tput, fig_band, fig_lat + + +def graph_tm_trending( + data: pd.DataFrame, + layout: dict, + all_in_one: bool=False + ) -> list: + """Generates one trending graph per test, each graph includes all selected + metrics. + + :param data: Data frame with telemetry data. + :param layout: Layout of plot.ly graph. + :param all_in_one: If True, all telemetry traces are placed in one graph, + otherwise they are split to separate graphs grouped by test ID. + :type data: pandas.DataFrame + :type layout: dict + :type all_in_one: bool + :returns: List of generated graphs together with test names. + list(tuple(plotly.graph_objects.Figure(), str()), tuple(...), ...) + :rtype: list + """ + + if data.empty: + return list() + + def _generate_traces( + data: pd.DataFrame, + test: str, + all_in_one: bool, + color_index: int + ) -> list: + """Generates a trending graph for given test with all metrics. + + :param data: Data frame with telemetry data for the given test. + :param test: The name of the test. + :param all_in_one: If True, all telemetry traces are placed in one + graph, otherwise they are split to separate graphs grouped by + test ID. + :param color_index: The index of the test used if all_in_one is True. + :type data: pandas.DataFrame + :type test: str + :type all_in_one: bool + :type color_index: int + :returns: List of traces. + :rtype: list + """ + traces = list() + metrics = data.tm_metric.unique().tolist() + for idx, metric in enumerate(metrics): + if "-pdr" in test and "='pdr'" not in metric: + continue + if "-ndr" in test and "='ndr'" not in metric: + continue + + df = data.loc[(data["tm_metric"] == metric)] + x_axis = df["start_time"].tolist() + y_data = [float(itm) for itm in df["tm_value"].tolist()] + hover = list() + for i, (_, row) in enumerate(df.iterrows()): + if row["test_type"] == "mrr": + rate = ( + f"mrr avg [{row[C.UNIT['mrr']]}]: " + f"{row[C.VALUE['mrr']]:,.0f}<br>" + f"mrr stdev [{row[C.UNIT['mrr']]}]: " + f"{row['result_receive_rate_rate_stdev']:,.0f}<br>" + ) + elif row["test_type"] == "ndrpdr": + if "-pdr" in test: + rate = ( + f"pdr [{row[C.UNIT['pdr']]}]: " + f"{row[C.VALUE['pdr']]:,.0f}<br>" + ) + elif "-ndr" in test: + rate = ( + f"ndr [{row[C.UNIT['ndr']]}]: " + f"{row[C.VALUE['ndr']]:,.0f}<br>" + ) + else: + rate = str() + else: + rate = str() + hover.append( + f"date: " + f"{row['start_time'].strftime('%Y-%m-%d %H:%M:%S')}<br>" + f"value: {y_data[i]:,.2f}<br>" + f"{rate}" + f"{row['dut_type']}-ref: {row['dut_version']}<br>" + f"csit-ref: {row['job']}/{row['build']}<br>" + ) + if any(y_data): + anomalies, trend_avg, trend_stdev = classify_anomalies( + {k: v for k, v in zip(x_axis, y_data)} + ) + hover_trend = list() + for avg, stdev, (_, row) in \ + zip(trend_avg, trend_stdev, df.iterrows()): + hover_trend.append( + f"date: " + f"{row['start_time'].strftime('%Y-%m-%d %H:%M:%S')}<br>" + f"trend: {avg:,.2f}<br>" + f"stdev: {stdev:,.2f}<br>" + f"{row['dut_type']}-ref: {row['dut_version']}<br>" + f"csit-ref: {row['job']}/{row['build']}" + ) + else: + anomalies = None + if all_in_one: + color = get_color(color_index * len(metrics) + idx) + metric_name = f"{test}<br>{metric}" + else: + color = get_color(idx) + metric_name = metric + + traces.append( + go.Scatter( # Samples + x=x_axis, + y=y_data, + name=metric_name, + mode="markers", + marker={ + "size": 5, + "color": color, + "symbol": "circle", + }, + text=hover, + hoverinfo="text+name", + showlegend=True, + legendgroup=metric_name + ) + ) + if anomalies: + traces.append( + go.Scatter( # Trend line + x=x_axis, + y=trend_avg, + name=metric_name, + mode="lines", + line={ + "shape": "linear", + "width": 1, + "color": color, + }, + text=hover_trend, + hoverinfo="text+name", + showlegend=False, + legendgroup=metric_name + ) + ) + + anomaly_x = list() + anomaly_y = list() + anomaly_color = list() + hover = list() + for idx, anomaly in enumerate(anomalies): + if anomaly in ("regression", "progression"): + anomaly_x.append(x_axis[idx]) + anomaly_y.append(trend_avg[idx]) + anomaly_color.append(C.ANOMALY_COLOR[anomaly]) + hover_itm = ( + f"date: {x_axis[idx].strftime('%Y-%m-%d %H:%M:%S')}" + f"<br>trend: {trend_avg[idx]:,.2f}" + f"<br>classification: {anomaly}" + ) + hover.append(hover_itm) + anomaly_color.extend([0.0, 0.5, 1.0]) + traces.append( + go.Scatter( + x=anomaly_x, + y=anomaly_y, + mode="markers", + text=hover, + hoverinfo="text+name", + showlegend=False, + legendgroup=metric_name, + name=metric_name, + marker={ + "size": 15, + "symbol": "circle-open", + "color": anomaly_color, + "colorscale": C.COLORSCALE_TPUT, + "showscale": True, + "line": { + "width": 2 + }, + "colorbar": { + "y": 0.5, + "len": 0.8, + "title": "Circles Marking Data Classification", + "titleside": "right", + "tickmode": "array", + "tickvals": [0.167, 0.500, 0.833], + "ticktext": C.TICK_TEXT_TPUT, + "ticks": "", + "ticklen": 0, + "tickangle": -90, + "thickness": 10 + } + } + ) + ) + + unique_metrics = set() + for itm in metrics: + unique_metrics.add(itm.split("{", 1)[0]) + return traces, unique_metrics + + tm_trending_graphs = list() + graph_layout = layout.get("plot-trending-telemetry", dict()) + + if all_in_one: + all_traces = list() + + all_metrics = set() + all_tests = list() + for idx, test in enumerate(data.test_name.unique()): + df = data.loc[(data["test_name"] == test)] + traces, metrics = _generate_traces(df, test, all_in_one, idx) + if traces: + all_metrics.update(metrics) + if all_in_one: + all_traces.extend(traces) + all_tests.append(test) + else: + graph = go.Figure() + graph.add_traces(traces) + graph.update_layout(graph_layout) + tm_trending_graphs.append((graph, [test, ], )) + + if all_in_one: + graph = go.Figure() + graph.add_traces(all_traces) + graph.update_layout(graph_layout) + tm_trending_graphs.append((graph, all_tests, )) + + return tm_trending_graphs, list(all_metrics) diff --git a/csit.infra.dash/app/cdash/trending/layout.py b/csit.infra.dash/app/cdash/trending/layout.py new file mode 100644 index 0000000000..da90ae26f9 --- /dev/null +++ b/csit.infra.dash/app/cdash/trending/layout.py @@ -0,0 +1,1721 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# 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. + + +"""Plotly Dash HTML layout override. +""" + +import logging +import pandas as pd +import dash_bootstrap_components as dbc + +from flask import Flask +from dash import dcc +from dash import html +from dash import callback_context, no_update, ALL +from dash import Input, Output, State +from dash.exceptions import PreventUpdate +from yaml import load, FullLoader, YAMLError +from ast import literal_eval +from copy import deepcopy + +from ..utils.constants import Constants as C +from ..utils.control_panel import ControlPanel +from ..utils.trigger import Trigger +from ..utils.telemetry_data import TelemetryData +from ..utils.utils import show_tooltip, label, sync_checklists, gen_new_url, \ + generate_options, get_list_group_items, navbar_trending, \ + show_trending_graph_data +from ..utils.url_processing import url_decode +from .graphs import graph_trending, select_trending_data, graph_tm_trending + + +# Control panel partameters and their default values. +CP_PARAMS = { + "dd-dut-val": str(), + "dd-phy-opt": list(), + "dd-phy-dis": True, + "dd-phy-val": str(), + "dd-area-opt": list(), + "dd-area-dis": True, + "dd-area-val": str(), + "dd-test-opt": list(), + "dd-test-dis": True, + "dd-test-val": str(), + "cl-core-opt": list(), + "cl-core-val": list(), + "cl-core-all-val": list(), + "cl-core-all-opt": C.CL_ALL_DISABLED, + "cl-frmsize-opt": list(), + "cl-frmsize-val": list(), + "cl-frmsize-all-val": list(), + "cl-frmsize-all-opt": C.CL_ALL_DISABLED, + "cl-tsttype-opt": list(), + "cl-tsttype-val": list(), + "cl-tsttype-all-val": list(), + "cl-tsttype-all-opt": C.CL_ALL_DISABLED, + "btn-add-dis": True, + "cl-normalize-val": list() +} + + +class Layout: + """The layout of the dash app and the callbacks. + """ + + def __init__(self, + app: Flask, + data_trending: pd.DataFrame, + html_layout_file: str, + graph_layout_file: str, + tooltip_file: str + ) -> None: + """Initialization: + - save the input parameters, + - read and pre-process the data, + - prepare data for the control panel, + - read HTML layout file, + - read tooltips from the tooltip file. + + :param app: Flask application running the dash application. + :param data_trending: Pandas dataframe with trending data. + :param html_layout_file: Path and name of the file specifying the HTML + layout of the dash application. + :param graph_layout_file: Path and name of the file with layout of + plot.ly graphs. + :param tooltip_file: Path and name of the yaml file specifying the + tooltips. + :type app: Flask + :type data_trending: pandas.DataFrame + :type html_layout_file: str + :type graph_layout_file: str + :type tooltip_file: str + """ + + # Inputs + self._app = app + self._data = data_trending + self._html_layout_file = html_layout_file + self._graph_layout_file = graph_layout_file + self._tooltip_file = tooltip_file + + # Get structure of tests: + tbs = dict() + cols = ["job", "test_id", "test_type", "tg_type"] + for _, row in self._data[cols].drop_duplicates().iterrows(): + lst_job = row["job"].split("-") + dut = lst_job[1] + tbed = "-".join(lst_job[-2:]) + lst_test = row["test_id"].split(".") + if dut == "dpdk": + area = "dpdk" + else: + area = ".".join(lst_test[3:-2]) + suite = lst_test[-2].replace("2n1l-", "").replace("1n1l-", "").\ + replace("2n-", "") + test = lst_test[-1] + nic = suite.split("-")[0] + for drv in C.DRIVERS: + if drv in test: + if drv == "af-xdp": + driver = "af_xdp" + else: + driver = drv + test = test.replace(f"{drv}-", "") + break + else: + driver = "dpdk" + infra = "-".join((tbed, nic, driver)) + lst_test = test.split("-") + framesize = lst_test[0] + core = lst_test[1] if lst_test[1] else "8C" + test = "-".join(lst_test[2: -1]) + + if tbs.get(dut, None) is None: + tbs[dut] = dict() + if tbs[dut].get(area, None) is None: + tbs[dut][area] = dict() + if tbs[dut][area].get(test, None) is None: + tbs[dut][area][test] = dict() + if tbs[dut][area][test].get(infra, None) is None: + tbs[dut][area][test][infra] = { + "core": list(), + "frame-size": list(), + "test-type": list() + } + tst_params = tbs[dut][area][test][infra] + if core.upper() not in tst_params["core"]: + tst_params["core"].append(core.upper()) + if framesize.upper() not in tst_params["frame-size"]: + tst_params["frame-size"].append(framesize.upper()) + if row["test_type"] == "ndrpdr": + if "NDR" not in tst_params["test-type"]: + tst_params["test-type"].extend(("NDR", "PDR")) + elif row["test_type"] == "hoststack": + if row["tg_type"] in ("iperf", "vpp"): + if "BPS" not in tst_params["test-type"]: + tst_params["test-type"].append("BPS") + elif row["tg_type"] == "ab": + if "CPS" not in tst_params["test-type"]: + tst_params["test-type"].extend(("CPS", "RPS")) + else: # MRR, SOAK + if row["test_type"].upper() not in tst_params["test-type"]: + tst_params["test-type"].append(row["test_type"].upper()) + self._spec_tbs = tbs + + # Read from files: + self._html_layout = str() + self._graph_layout = None + self._tooltips = dict() + + try: + with open(self._html_layout_file, "r") as file_read: + self._html_layout = file_read.read() + except IOError as err: + raise RuntimeError( + f"Not possible to open the file {self._html_layout_file}\n{err}" + ) + + try: + with open(self._graph_layout_file, "r") as file_read: + self._graph_layout = load(file_read, Loader=FullLoader) + except IOError as err: + raise RuntimeError( + f"Not possible to open the file {self._graph_layout_file}\n" + f"{err}" + ) + except YAMLError as err: + raise RuntimeError( + f"An error occurred while parsing the specification file " + f"{self._graph_layout_file}\n{err}" + ) + + try: + with open(self._tooltip_file, "r") as file_read: + self._tooltips = load(file_read, Loader=FullLoader) + except IOError as err: + logging.warning( + f"Not possible to open the file {self._tooltip_file}\n{err}" + ) + except YAMLError as err: + logging.warning( + f"An error occurred while parsing the specification file " + f"{self._tooltip_file}\n{err}" + ) + + # Callbacks: + if self._app is not None and hasattr(self, "callbacks"): + self.callbacks(self._app) + + @property + def html_layout(self): + return self._html_layout + + def add_content(self): + """Top level method which generated the web page. + + It generates: + - Store for user input data, + - Navigation bar, + - Main area with control panel and ploting area. + + If no HTML layout is provided, an error message is displayed instead. + + :returns: The HTML div with the whole page. + :rtype: html.Div + """ + + if self.html_layout and self._spec_tbs: + return html.Div( + id="div-main", + className="small", + children=[ + dcc.Store(id="store"), + dcc.Location(id="url", refresh=False), + dbc.Row( + id="row-navbar", + class_name="g-0", + children=[navbar_trending((True, False, False, False))] + ), + dbc.Row( + id="row-main", + class_name="g-0", + children=[ + self._add_ctrl_col(), + self._add_plotting_col() + ] + ), + dbc.Spinner( + dbc.Offcanvas( + class_name="w-50", + id="offcanvas-metadata", + title="Detailed Information", + placement="end", + is_open=False, + children=[ + dbc.Row(id="metadata-tput-lat"), + dbc.Row(id="metadata-hdrh-graph") + ] + ), + delay_show=C.SPINNER_DELAY + ), + dbc.Offcanvas( + class_name="w-75", + id="offcanvas-documentation", + title="Documentation", + placement="end", + is_open=False, + children=html.Iframe( + src=C.URL_DOC_TRENDING, + width="100%", + height="100%" + ) + ) + ] + ) + else: + return html.Div( + dbc.Alert("An Error Occured", color="danger"), + id="div-main-error" + ) + + def _add_ctrl_col(self) -> dbc.Col: + """Add column with controls. It is placed on the left side. + + :returns: Column with the control panel. + :rtype: dbc.Col + """ + return dbc.Col(html.Div(self._add_ctrl_panel(), className="sticky-top")) + + def _add_ctrl_panel(self) -> list: + """Add control panel. + + :returns: Control panel. + :rtype: list + """ + return [ + dbc.Row( + dbc.InputGroup( + [ + dbc.InputGroupText( + show_tooltip(self._tooltips, "help-dut", "DUT") + ), + dbc.Select( + id={"type": "ctrl-dd", "index": "dut"}, + placeholder="Select a Device under Test...", + options=sorted( + [ + {"label": k, "value": k} \ + for k in self._spec_tbs.keys() + ], + key=lambda d: d["label"] + ) + ) + ], + size="sm" + ), + class_name="g-0 p-1" + ), + dbc.Row( + dbc.InputGroup( + [ + dbc.InputGroupText( + show_tooltip(self._tooltips, "help-area", "Area") + ), + dbc.Select( + id={"type": "ctrl-dd", "index": "area"}, + placeholder="Select an Area..." + ) + ], + size="sm" + ), + class_name="g-0 p-1" + ), + dbc.Row( + dbc.InputGroup( + [ + dbc.InputGroupText( + show_tooltip(self._tooltips, "help-test", "Test") + ), + dbc.Select( + id={"type": "ctrl-dd", "index": "test"}, + placeholder="Select a Test..." + ) + ], + size="sm" + ), + class_name="g-0 p-1" + ), + dbc.Row( + dbc.InputGroup( + [ + dbc.InputGroupText( + show_tooltip(self._tooltips, "help-infra", "Infra") + ), + dbc.Select( + id={"type": "ctrl-dd", "index": "phy"}, + placeholder="Select a Physical Test Bed Topology..." + ) + ], + size="sm" + ), + class_name="g-0 p-1" + ), + dbc.Row( + dbc.InputGroup( + [ + dbc.InputGroupText(show_tooltip( + self._tooltips, + "help-framesize", + "Frame Size" + )), + dbc.Col( + dbc.Checklist( + id={"type": "ctrl-cl", "index": "frmsize-all"}, + options=C.CL_ALL_DISABLED, + inline=True, + class_name="ms-2" + ), + width=2 + ), + dbc.Col( + dbc.Checklist( + id={"type": "ctrl-cl", "index": "frmsize"}, + inline=True + ) + ) + ], + style={"align-items": "center"}, + size="sm" + ), + class_name="g-0 p-1" + ), + dbc.Row( + dbc.InputGroup( + [ + dbc.InputGroupText(show_tooltip( + self._tooltips, + "help-cores", + "Number of Cores" + )), + dbc.Col( + dbc.Checklist( + id={"type": "ctrl-cl", "index": "core-all"}, + options=C.CL_ALL_DISABLED, + inline=True, + class_name="ms-2" + ), + width=2 + ), + dbc.Col( + dbc.Checklist( + id={"type": "ctrl-cl", "index": "core"}, + inline=True + ) + ) + ], + style={"align-items": "center"}, + size="sm" + ), + class_name="g-0 p-1" + ), + dbc.Row( + dbc.InputGroup( + [ + dbc.InputGroupText(show_tooltip( + self._tooltips, + "help-ttype", + "Test Type" + )), + dbc.Col( + dbc.Checklist( + id={"type": "ctrl-cl", "index": "tsttype-all"}, + options=C.CL_ALL_DISABLED, + inline=True, + class_name="ms-2" + ), + width=2 + ), + dbc.Col( + dbc.Checklist( + id={"type": "ctrl-cl", "index": "tsttype"}, + inline=True + ) + ) + ], + style={"align-items": "center"}, + size="sm" + ), + class_name="g-0 p-1" + ), + dbc.Row( + dbc.InputGroup( + [ + dbc.InputGroupText(show_tooltip( + self._tooltips, + "help-normalize", + "Normalization" + )), + dbc.Col(dbc.Checklist( + id="normalize", + options=[{ + "value": "normalize", + "label": "Normalize to CPU frequency 2GHz" + }], + value=[], + inline=True, + class_name="ms-2" + )) + ], + style={"align-items": "center"}, + size="sm" + ), + class_name="g-0 p-1" + ), + dbc.Row( + dbc.Button( + id={"type": "ctrl-btn", "index": "add-test"}, + children="Add Selected", + color="info" + ), + class_name="g-0 p-1" + ), + dbc.Row( + dbc.ListGroup( + class_name="overflow-auto p-0", + id="lg-selected", + children=[], + style={"max-height": "20em"}, + flush=True + ), + id="row-card-sel-tests", + class_name="g-0 p-1", + style=C.STYLE_DISABLED, + ), + dbc.Row( + dbc.ButtonGroup([ + dbc.Button( + "Remove Selected", + id={"type": "ctrl-btn", "index": "rm-test"}, + class_name="w-100", + color="info", + disabled=False + ), + dbc.Button( + "Remove All", + id={"type": "ctrl-btn", "index": "rm-test-all"}, + class_name="w-100", + color="info", + disabled=False + ) + ]), + id="row-btns-sel-tests", + class_name="g-0 p-1", + style=C.STYLE_DISABLED, + ), + dbc.Stack( + [ + dbc.Button( + "Add Telemetry Panel", + id={"type": "telemetry-btn", "index": "open"}, + color="info" + ), + dbc.Button("Show URL", id="plot-btn-url", color="info"), + dbc.Modal( + [ + dbc.ModalHeader(dbc.ModalTitle("URL")), + dbc.ModalBody(id="mod-url") + ], + id="plot-mod-url", + size="xl", + is_open=False, + scrollable=True + ) + ], + id="row-btns-add-tm", + class_name="g-0 p-1", + style=C.STYLE_DISABLED, + gap=2 + ) + ] + + def _add_plotting_col(self) -> dbc.Col: + """Add column with plots. It is placed on the right side. + + :returns: Column with plots. + :rtype: dbc.Col + """ + return dbc.Col( + id="col-plotting-area", + children=[ + dbc.Row( + id="plotting-area-trending", + class_name="g-0 p-0", + children=C.PLACEHOLDER + ), + dbc.Row( + id="plotting-area-telemetry", + class_name="g-0 p-0", + children=C.PLACEHOLDER + ) + ], + width=9, + style=C.STYLE_DISABLED, + ) + + @staticmethod + def _plotting_area_trending(graphs: list) -> dbc.Col: + """Generate the plotting area with all its content. + + :param graphs: A list of graphs to be displayed in the trending page. + :type graphs: list + :returns: A collumn with trending graphs (tput and latency) in tabs. + :rtype: dbc.Col + """ + if not graphs: + return C.PLACEHOLDER + + if not graphs[0]: + return C.PLACEHOLDER + + tab_items = [ + dbc.Tab( + children=dcc.Graph( + id={"type": "graph", "index": "tput"}, + figure=graphs[0] + ), + label="Throughput", + tab_id="tab-tput" + ) + ] + + if graphs[1]: + tab_items.append( + dbc.Tab( + children=dcc.Graph( + id={"type": "graph", "index": "bandwidth"}, + figure=graphs[1] + ), + label="Bandwidth", + tab_id="tab-bandwidth" + ) + ) + + if graphs[2]: + tab_items.append( + dbc.Tab( + children=dcc.Graph( + id={"type": "graph", "index": "lat"}, + figure=graphs[2] + ), + label="Latency", + tab_id="tab-lat" + ) + ) + + trending = [ + dbc.Row( + dbc.Tabs( + children=tab_items, + id="tabs", + active_tab="tab-tput", + ), + class_name="g-0 p-0" + ), + dbc.Row( + html.Div( + [ + dbc.Button( + "Download Data", + id="plot-btn-download", + class_name="me-1", + color="info", + style={"padding": "0rem 1rem"} + ), + dcc.Download(id="download-trending-data") + ], + className="d-grid gap-0 d-md-flex justify-content-md-end" + ), + class_name="g-0 p-0" + ) + ] + + return dbc.Col( + children=[ + dbc.Accordion( + dbc.AccordionItem(trending, title="Trending"), + class_name="g-0 p-1", + start_collapsed=False, + always_open=True, + active_item=["item-0", ] + ), + dbc.Modal( + [ + dbc.ModalHeader( + dbc.ModalTitle("Select a Metric"), + close_button=False + ), + dbc.Spinner( + dbc.ModalBody(Layout._get_telemetry_step_1()), + delay_show=2 * C.SPINNER_DELAY + ), + dbc.ModalFooter([ + dbc.Button( + "Select", + id={"type": "telemetry-btn", "index": "select"}, + color="success", + disabled=True + ), + dbc.Button( + "Cancel", + id={"type": "telemetry-btn", "index": "cancel"}, + color="info", + disabled=False + ), + dbc.Button( + "Remove All", + id={"type": "telemetry-btn", "index": "rm-all"}, + color="danger", + disabled=False + ) + ]) + ], + id={"type": "plot-mod-telemetry", "index": 0}, + size="lg", + is_open=False, + scrollable=False, + backdrop="static", + keyboard=False + ), + dbc.Modal( + [ + dbc.ModalHeader( + dbc.ModalTitle("Select Labels"), + close_button=False + ), + dbc.Spinner( + dbc.ModalBody(Layout._get_telemetry_step_2()), + delay_show=2 * C.SPINNER_DELAY + ), + dbc.ModalFooter([ + dbc.Button( + "Back", + id={"type": "telemetry-btn", "index": "back"}, + color="info", + disabled=False + ), + dbc.Button( + "Add Telemetry Panel", + id={"type": "telemetry-btn", "index": "add"}, + color="success", + disabled=True + ), + dbc.Button( + "Cancel", + id={"type": "telemetry-btn", "index": "cancel"}, + color="info", + disabled=False + ) + ]) + ], + id={"type": "plot-mod-telemetry", "index": 1}, + size="xl", + is_open=False, + scrollable=False, + backdrop="static", + keyboard=False + ) + ] + ) + + @staticmethod + def _plotting_area_telemetry(graphs: list) -> dbc.Col: + """Generate the plotting area with telemetry. + + :param graphs: A list of graphs to be displayed in the telemetry page. + :type graphs: list + :returns: A collumn with telemetry trending graphs. + :rtype: dbc.Col + """ + if not graphs: + return C.PLACEHOLDER + + def _plural(iterative): + return "s" if len(iterative) > 1 else str() + + panels = list() + for idx, graph_set in enumerate(graphs): + acc_items = list() + for graph in graph_set[0]: + graph_name = ", ".join(graph[1]) + acc_items.append( + dbc.AccordionItem( + dcc.Graph( + id={"type": "graph-telemetry", "index": graph_name}, + figure=graph[0] + ), + title=(f"Test{_plural(graph[1])}: {graph_name}"), + class_name="g-0 p-0" + ) + ) + panels.append( + dbc.AccordionItem( + [ + dbc.Row( + dbc.Accordion( + children=acc_items, + class_name="g-0 p-0", + start_collapsed=True, + always_open=True, + flush=True + ), + class_name="g-0 p-0" + ), + dbc.Row( + html.Div( + [ + dbc.Button( + "Remove", + id={ + "type": "tm-btn-remove", + "index": idx + }, + class_name="me-1", + color="danger", + style={"padding": "0rem 1rem"} + ), + dbc.Button( + "Download Data", + id={ + "type": "tm-btn-download", + "index": idx + }, + class_name="me-1", + color="info", + style={"padding": "0rem 1rem"} + ) + ], + className=\ + "d-grid gap-0 d-md-flex justify-content-md-end" + ), + class_name="g-0 p-0" + ) + ], + class_name="g-0 p-0", + title=( + f"Metric{_plural(graph_set[1])}: ", + ", ".join(graph_set[1]) + ) + ) + ) + + return dbc.Col( + dbc.Accordion( + panels, + class_name="g-0 p-1", + start_collapsed=True, + always_open=True + ) + ) + + @staticmethod + def _get_telemetry_step_1() -> list: + """Return the content of the modal window used in the step 1 of metrics + selection. + + :returns: A list of dbc rows with 'input' and 'search output'. + :rtype: list + """ + return [ + dbc.Row( + class_name="g-0 p-1", + children=[ + dbc.Input( + id={"type": "telemetry-search-in", "index": 0}, + placeholder="Start typing a metric name...", + type="text" + ) + ] + ), + dbc.Row( + class_name="g-0 p-1", + children=[ + dbc.ListGroup( + class_name="overflow-auto p-0", + id={"type": "telemetry-search-out", "index": 0}, + children=[], + style={"max-height": "14em"}, + flush=True + ) + ] + ) + ] + + @staticmethod + def _get_telemetry_step_2() -> list: + """Return the content of the modal window used in the step 2 of metrics + selection. + + :returns: A list of dbc rows with 'container with dynamic dropdowns' and + 'search output'. + :rtype: list + """ + return [ + dbc.Row( + "Add content here.", + id={"type": "tm-container", "index": 0}, + class_name="g-0 p-1" + ), + dbc.Row( + [ + dbc.Col( + dbc.Checkbox( + id={"type": "cb-all-in-one", "index": 0}, + label="All Metrics in one Graph" + ), + width=6 + ), + dbc.Col( + dbc.Checkbox( + id={"type": "cb-ignore-host", "index": 0}, + label="Ignore Host" + ), + width=6 + ) + ], + class_name="g-0 p-2" + ), + dbc.Row( + dbc.Textarea( + id={"type": "tm-list-metrics", "index": 0}, + rows=20, + size="sm", + wrap="off", + readonly=True + ), + class_name="g-0 p-1" + ) + ] + + def callbacks(self, app): + """Callbacks for the whole application. + + :param app: The application. + :type app: Flask + """ + + @app.callback( + Output("store", "data"), + Output("plotting-area-trending", "children"), + Output("plotting-area-telemetry", "children"), + Output("col-plotting-area", "style"), + Output("row-card-sel-tests", "style"), + Output("row-btns-sel-tests", "style"), + Output("row-btns-add-tm", "style"), + Output("lg-selected", "children"), + Output({"type": "telemetry-search-out", "index": ALL}, "children"), + Output({"type": "plot-mod-telemetry", "index": ALL}, "is_open"), + Output({"type": "telemetry-btn", "index": ALL}, "disabled"), + Output({"type": "tm-container", "index": ALL}, "children"), + Output({"type": "tm-list-metrics", "index": ALL}, "value"), + Output({"type": "ctrl-dd", "index": "dut"}, "value"), + Output({"type": "ctrl-dd", "index": "phy"}, "options"), + Output({"type": "ctrl-dd", "index": "phy"}, "disabled"), + Output({"type": "ctrl-dd", "index": "phy"}, "value"), + Output({"type": "ctrl-dd", "index": "area"}, "options"), + Output({"type": "ctrl-dd", "index": "area"}, "disabled"), + Output({"type": "ctrl-dd", "index": "area"}, "value"), + Output({"type": "ctrl-dd", "index": "test"}, "options"), + Output({"type": "ctrl-dd", "index": "test"}, "disabled"), + Output({"type": "ctrl-dd", "index": "test"}, "value"), + Output({"type": "ctrl-cl", "index": "core"}, "options"), + Output({"type": "ctrl-cl", "index": "core"}, "value"), + Output({"type": "ctrl-cl", "index": "core-all"}, "value"), + Output({"type": "ctrl-cl", "index": "core-all"}, "options"), + Output({"type": "ctrl-cl", "index": "frmsize"}, "options"), + Output({"type": "ctrl-cl", "index": "frmsize"}, "value"), + Output({"type": "ctrl-cl", "index": "frmsize-all"}, "value"), + Output({"type": "ctrl-cl", "index": "frmsize-all"}, "options"), + Output({"type": "ctrl-cl", "index": "tsttype"}, "options"), + Output({"type": "ctrl-cl", "index": "tsttype"}, "value"), + Output({"type": "ctrl-cl", "index": "tsttype-all"}, "value"), + Output({"type": "ctrl-cl", "index": "tsttype-all"}, "options"), + Output({"type": "ctrl-btn", "index": "add-test"}, "disabled"), + Output("normalize", "value"), + + State("store", "data"), + State({"type": "sel-cl", "index": ALL}, "value"), + State({"type": "cb-all-in-one", "index": ALL}, "value"), + State({"type": "cb-ignore-host", "index": ALL}, "value"), + State({"type": "telemetry-search-out", "index": ALL}, "children"), + State({"type": "plot-mod-telemetry", "index": ALL}, "is_open"), + State({"type": "telemetry-btn", "index": ALL}, "disabled"), + State({"type": "tm-container", "index": ALL}, "children"), + State({"type": "tm-list-metrics", "index": ALL}, "value"), + State({"type": "tele-cl", "index": ALL}, "value"), + + Input("url", "href"), + Input({"type": "tm-dd", "index": ALL}, "value"), + + Input("normalize", "value"), + Input({"type": "telemetry-search-in", "index": ALL}, "value"), + Input({"type": "telemetry-btn", "index": ALL}, "n_clicks"), + Input({"type": "tm-btn-remove", "index": ALL}, "n_clicks"), + Input({"type": "ctrl-dd", "index": ALL}, "value"), + Input({"type": "ctrl-cl", "index": ALL}, "value"), + Input({"type": "ctrl-btn", "index": ALL}, "n_clicks"), + + prevent_initial_call=True + ) + def _update_application( + store: dict, + lst_sel: list, + all_in_one: list, + ignore_host: list, + search_out: list, + is_open: list, + tm_btns_disabled: list, + tm_dd: list, + list_metrics: list, + cl_metrics: list, + href: str, + tm_dd_in: list, + *_ + ) -> tuple: + """Update the application when the event is detected. + """ + + if store is None: + store = { + "control-panel": dict(), + "selected-tests": list(), + "trending-graphs": None, + "telemetry-data": dict(), + "selected-metrics": dict(), + "telemetry-panels": list(), + "telemetry-all-in-one": list(), + "telemetry-ignore-host": list(), + "telemetry-graphs": list(), + "url": str() + } + + ctrl_panel = ControlPanel( + CP_PARAMS, + store.get("control-panel", dict()) + ) + store_sel = store["selected-tests"] + tm_data = store["telemetry-data"] + tm_user = store["selected-metrics"] + tm_panels = store["telemetry-panels"] + tm_all_in_one = store["telemetry-all-in-one"] + tm_ignore_host = store["telemetry-ignore-host"] + + plotting_area_telemetry = no_update + on_draw = [False, False] # 0 --> trending, 1 --> telemetry + + # Parse the url: + parsed_url = url_decode(href) + if parsed_url: + url_params = parsed_url["params"] + else: + url_params = None + + if tm_user is None: + # Telemetry user data + # The data provided by user or result of user action + tm_user = { + # List of unique metrics: + "unique_metrics": list(), + # List of metrics selected by user: + "selected_metrics": list(), + # Labels from metrics selected by user (key: label name, + # value: list of all possible values): + "unique_labels": dict(), + # Labels selected by the user (subset of 'unique_labels'): + "selected_labels": dict(), + # All unique metrics with labels (output from the step 1) + # converted from pandas dataframe to dictionary. + "unique_metrics_with_labels": dict(), + # Metrics with labels selected by the user using dropdowns. + "selected_metrics_with_labels": dict() + } + tm = TelemetryData(store_sel) if store_sel else TelemetryData() + + trigger = Trigger(callback_context.triggered) + if trigger.type == "url" and url_params: + telemetry = None + try: + store_sel = literal_eval(url_params["store_sel"][0]) + normalize = literal_eval(url_params["norm"][0]) + telemetry = literal_eval(url_params["telemetry"][0]) + url_p = url_params.get("all-in-one", ["[[None]]"]) + tm_all_in_one = literal_eval(url_p[0]) + url_p = url_params.get("ignore-host", ["[[None]]"]) + tm_ignore_host = literal_eval(url_p[0]) + if not isinstance(telemetry, list): + telemetry = [telemetry, ] + except (KeyError, IndexError, AttributeError, ValueError): + pass + if store_sel: + last_test = store_sel[-1] + test = self._spec_tbs[last_test["dut"]]\ + [last_test["area"]][last_test["test"]][last_test["phy"]] + ctrl_panel.set({ + "dd-dut-val": last_test["dut"], + "dd-area-val": last_test["area"], + "dd-area-opt": [ + {"label": label(v), "value": v} for v in sorted( + self._spec_tbs[last_test["dut"]].keys()) + ], + "dd-area-dis": False, + "dd-test-val": last_test["test"], + "dd-test-opt": generate_options( + self._spec_tbs[last_test["dut"]]\ + [last_test["area"]].keys() + ), + "dd-test-dis": False, + "dd-phy-val": last_test["phy"], + "dd-phy-opt": generate_options( + self._spec_tbs[last_test["dut"]][last_test["area"]]\ + [last_test["test"]].keys() + ), + "dd-phy-dis": False, + "cl-core-opt": generate_options(test["core"]), + "cl-core-val": [last_test["core"].upper(), ], + "cl-core-all-val": list(), + "cl-core-all-opt": C.CL_ALL_ENABLED, + "cl-frmsize-opt": generate_options(test["frame-size"]), + "cl-frmsize-val": [last_test["framesize"].upper(), ], + "cl-frmsize-all-val": list(), + "cl-frmsize-all-opt": C.CL_ALL_ENABLED, + "cl-tsttype-opt": generate_options(test["test-type"]), + "cl-tsttype-val": [last_test["testtype"].upper(), ], + "cl-tsttype-all-val": list(), + "cl-tsttype-all-opt": C.CL_ALL_ENABLED, + "cl-normalize-val": normalize, + "btn-add-dis": False + }) + store["trending-graphs"] = None + store["telemetry-graphs"] = list() + on_draw[0] = True + if telemetry: + tm = TelemetryData(store_sel) + tm.from_dataframe(self._data) + tm_data = tm.to_json() + tm.from_json(tm_data) + tm_panels = telemetry + on_draw[1] = True + elif trigger.type == "normalize": + ctrl_panel.set({"cl-normalize-val": trigger.value}) + store["trending-graphs"] = None + on_draw[0] = True + elif trigger.type == "ctrl-dd": + if trigger.idx == "dut": + try: + dut = self._spec_tbs[trigger.value] + options = [{"label": label(v), "value": v} \ + for v in sorted(dut.keys())] + disabled = False + except KeyError: + options = list() + disabled = True + ctrl_panel.set({ + "dd-dut-val": trigger.value, + "dd-area-val": str(), + "dd-area-opt": options, + "dd-area-dis": disabled, + "dd-test-val": str(), + "dd-test-opt": list(), + "dd-test-dis": True, + "dd-phy-val": str(), + "dd-phy-opt": list(), + "dd-phy-dis": True, + "cl-core-opt": list(), + "cl-core-val": list(), + "cl-core-all-val": list(), + "cl-core-all-opt": C.CL_ALL_DISABLED, + "cl-frmsize-opt": list(), + "cl-frmsize-val": list(), + "cl-frmsize-all-val": list(), + "cl-frmsize-all-opt": C.CL_ALL_DISABLED, + "cl-tsttype-opt": list(), + "cl-tsttype-val": list(), + "cl-tsttype-all-val": list(), + "cl-tsttype-all-opt": C.CL_ALL_DISABLED, + "btn-add-dis": True + }) + if trigger.idx == "area": + try: + dut = ctrl_panel.get("dd-dut-val") + area = self._spec_tbs[dut][trigger.value] + options = generate_options(area.keys()) + disabled = False + except KeyError: + options = list() + disabled = True + ctrl_panel.set({ + "dd-area-val": trigger.value, + "dd-test-val": str(), + "dd-test-opt": options, + "dd-test-dis": disabled, + "dd-phy-val": str(), + "dd-phy-opt": list(), + "dd-phy-dis": True, + "cl-core-opt": list(), + "cl-core-val": list(), + "cl-core-all-val": list(), + "cl-core-all-opt": C.CL_ALL_DISABLED, + "cl-frmsize-opt": list(), + "cl-frmsize-val": list(), + "cl-frmsize-all-val": list(), + "cl-frmsize-all-opt": C.CL_ALL_DISABLED, + "cl-tsttype-opt": list(), + "cl-tsttype-val": list(), + "cl-tsttype-all-val": list(), + "cl-tsttype-all-opt": C.CL_ALL_DISABLED, + "btn-add-dis": True + }) + if trigger.idx == "test": + try: + dut = ctrl_panel.get("dd-dut-val") + area = ctrl_panel.get("dd-area-val") + test = self._spec_tbs[dut][area][trigger.value] + options = generate_options(test.keys()) + disabled = False + except KeyError: + options = list() + disabled = True + ctrl_panel.set({ + "dd-test-val": trigger.value, + "dd-phy-val": str(), + "dd-phy-opt": options, + "dd-phy-dis": disabled, + "cl-core-opt": list(), + "cl-core-val": list(), + "cl-core-all-val": list(), + "cl-core-all-opt": C.CL_ALL_DISABLED, + "cl-frmsize-opt": list(), + "cl-frmsize-val": list(), + "cl-frmsize-all-val": list(), + "cl-frmsize-all-opt": C.CL_ALL_DISABLED, + "cl-tsttype-opt": list(), + "cl-tsttype-val": list(), + "cl-tsttype-all-val": list(), + "cl-tsttype-all-opt": C.CL_ALL_DISABLED, + "btn-add-dis": True + }) + if trigger.idx == "phy": + dut = ctrl_panel.get("dd-dut-val") + area = ctrl_panel.get("dd-area-val") + test = ctrl_panel.get("dd-test-val") + if all((dut, area, test, trigger.value, )): + phy = self._spec_tbs[dut][area][test][trigger.value] + ctrl_panel.set({ + "dd-phy-val": trigger.value, + "cl-core-opt": generate_options(phy["core"]), + "cl-core-val": list(), + "cl-core-all-val": list(), + "cl-core-all-opt": C.CL_ALL_ENABLED, + "cl-frmsize-opt": \ + generate_options(phy["frame-size"]), + "cl-frmsize-val": list(), + "cl-frmsize-all-val": list(), + "cl-frmsize-all-opt": C.CL_ALL_ENABLED, + "cl-tsttype-opt": \ + generate_options(phy["test-type"]), + "cl-tsttype-val": list(), + "cl-tsttype-all-val": list(), + "cl-tsttype-all-opt": C.CL_ALL_ENABLED, + "btn-add-dis": True + }) + elif trigger.type == "ctrl-cl": + param = trigger.idx.split("-")[0] + if "-all" in trigger.idx: + c_sel, c_all, c_id = list(), trigger.value, "all" + else: + c_sel, c_all, c_id = trigger.value, list(), str() + val_sel, val_all = sync_checklists( + options=ctrl_panel.get(f"cl-{param}-opt"), + sel=c_sel, + all=c_all, + id=c_id + ) + ctrl_panel.set({ + f"cl-{param}-val": val_sel, + f"cl-{param}-all-val": val_all, + }) + if all((ctrl_panel.get("cl-core-val"), + ctrl_panel.get("cl-frmsize-val"), + ctrl_panel.get("cl-tsttype-val"), )): + ctrl_panel.set({"btn-add-dis": False}) + else: + ctrl_panel.set({"btn-add-dis": True}) + elif trigger.type == "ctrl-btn": + tm_panels = list() + tm_all_in_one = list() + tm_ignore_host = list() + store["trending-graphs"] = None + store["telemetry-graphs"] = list() + on_draw = [True, True] + if trigger.idx == "add-test": + dut = ctrl_panel.get("dd-dut-val") + phy = ctrl_panel.get("dd-phy-val") + area = ctrl_panel.get("dd-area-val") + test = ctrl_panel.get("dd-test-val") + # Add selected test(s) to the list of tests in store: + if store_sel is None: + store_sel = list() + for core in ctrl_panel.get("cl-core-val"): + for framesize in ctrl_panel.get("cl-frmsize-val"): + for ttype in ctrl_panel.get("cl-tsttype-val"): + if dut == "trex": + core = str() + tid = "-".join(( + dut, + phy.replace('af_xdp', 'af-xdp'), + area, + framesize.lower(), + core.lower(), + test, + ttype.lower() + )) + if tid not in [i["id"] for i in store_sel]: + store_sel.append({ + "id": tid, + "dut": dut, + "phy": phy, + "area": area, + "test": test, + "framesize": framesize.lower(), + "core": core.lower(), + "testtype": ttype.lower() + }) + store_sel = sorted(store_sel, key=lambda d: d["id"]) + if C.CLEAR_ALL_INPUTS: + ctrl_panel.set(ctrl_panel.defaults) + elif trigger.idx == "rm-test" and lst_sel: + new_store_sel = list() + for idx, item in enumerate(store_sel): + if not lst_sel[idx]: + new_store_sel.append(item) + store_sel = new_store_sel + elif trigger.idx == "rm-test-all": + store_sel = list() + elif trigger.type == "telemetry-btn": + if trigger.idx in ("open", "back"): + tm.from_dataframe(self._data) + tm_data = tm.to_json() + tm_user["unique_metrics"] = tm.unique_metrics + tm_user["selected_metrics"] = list() + tm_user["unique_labels"] = dict() + tm_user["selected_labels"] = dict() + search_out = ( + get_list_group_items(tm_user["unique_metrics"], + "tele-cl", False), + ) + is_open = (True, False) + tm_btns_disabled[1], tm_btns_disabled[5] = False, True + elif trigger.idx == "select": + if any(cl_metrics): + tm.from_json(tm_data) + if not tm_user["selected_metrics"]: + tm_user["selected_metrics"] = \ + tm_user["unique_metrics"] + metrics = [a for a, b in \ + zip(tm_user["selected_metrics"], cl_metrics) if b] + tm_user["selected_metrics"] = metrics + tm_user["unique_labels"] = \ + tm.get_selected_labels(metrics) + tm_user["unique_metrics_with_labels"] = \ + tm.unique_metrics_with_labels + list_metrics[0] = tm.str_metrics + tm_dd[0] = _get_dd_container(tm_user["unique_labels"]) + if list_metrics[0]: + tm_btns_disabled[1] = True + tm_btns_disabled[4] = False + is_open = (False, True) + else: + is_open = (True, False) + elif trigger.idx == "add": + tm.from_json(tm_data) + tm_panels.append(tm_user["selected_metrics_with_labels"]) + tm_all_in_one.append(all_in_one) + tm_ignore_host.append(ignore_host) + is_open = (False, False) + tm_btns_disabled[1], tm_btns_disabled[5] = True, True + on_draw = [True, True] + elif trigger.idx == "cancel": + is_open = (False, False) + tm_btns_disabled[1], tm_btns_disabled[5] = True, True + elif trigger.idx == "rm-all": + tm_panels = list() + tm_all_in_one = list() + tm_ignore_host = list() + tm_user = None + is_open = (False, False) + tm_btns_disabled[1], tm_btns_disabled[5] = True, True + plotting_area_telemetry = C.PLACEHOLDER + elif trigger.type == "telemetry-search-in": + tm.from_metrics(tm_user["unique_metrics"]) + tm_user["selected_metrics"] = \ + tm.search_unique_metrics(trigger.value) + search_out = (get_list_group_items( + tm_user["selected_metrics"], + type="tele-cl", + colorize=False + ), ) + is_open = (True, False) + elif trigger.type == "tm-dd": + tm.from_metrics_with_labels( + tm_user["unique_metrics_with_labels"] + ) + selected = dict() + previous_itm = None + for itm in tm_dd_in: + if itm is None: + show_new = True + elif isinstance(itm, str): + show_new = False + selected[itm] = list() + elif isinstance(itm, list): + if previous_itm is not None: + selected[previous_itm] = itm + show_new = True + previous_itm = itm + tm_dd[0] = _get_dd_container( + tm_user["unique_labels"], + selected, + show_new + ) + sel_metrics = tm.filter_selected_metrics_by_labels(selected) + tm_user["selected_metrics_with_labels"] = sel_metrics.to_dict() + if not sel_metrics.empty: + list_metrics[0] = tm.metrics_to_str(sel_metrics) + tm_btns_disabled[5] = False + else: + list_metrics[0] = str() + elif trigger.type == "tm-btn-remove": + del tm_panels[trigger.idx] + del tm_all_in_one[trigger.idx] + del tm_ignore_host[trigger.idx] + del store["telemetry-graphs"][trigger.idx] + tm.from_json(tm_data) + on_draw = [True, True] + + new_url_params = { + "store_sel": store_sel, + "norm": ctrl_panel.get("cl-normalize-val") + } + if tm_panels: + new_url_params["telemetry"] = tm_panels + new_url_params["all-in-one"] = tm_all_in_one + new_url_params["ignore-host"] = tm_ignore_host + + if on_draw[0]: # Trending + if store_sel: + lg_selected = get_list_group_items(store_sel, "sel-cl") + if store["trending-graphs"]: + graphs = store["trending-graphs"] + else: + graphs = graph_trending( + self._data, + store_sel, + self._graph_layout, + bool(ctrl_panel.get("cl-normalize-val")) + ) + if graphs and graphs[0]: + store["trending-graphs"] = graphs + plotting_area_trending = \ + Layout._plotting_area_trending(graphs) + + # Telemetry + start_idx = len(store["telemetry-graphs"]) + end_idx = len(tm_panels) + if not end_idx: + plotting_area_telemetry = C.PLACEHOLDER + elif on_draw[1] and (end_idx >= start_idx): + if len(tm_all_in_one) != end_idx: + tm_all_in_one = [[None], ] * end_idx + if len(tm_ignore_host) != end_idx: + tm_ignore_host = [[None], ] * end_idx + for idx in range(start_idx, end_idx): + store["telemetry-graphs"].append(graph_tm_trending( + tm.select_tm_trending_data( + tm_panels[idx], + ignore_host=bool(tm_ignore_host[idx][0]) + ), + self._graph_layout, + bool(tm_all_in_one[idx][0]) + )) + plotting_area_telemetry = \ + Layout._plotting_area_telemetry( + store["telemetry-graphs"] + ) + col_plotting_area = C.STYLE_ENABLED + row_card_sel_tests = C.STYLE_ENABLED + row_btns_sel_tests = C.STYLE_ENABLED + row_btns_add_tm = C.STYLE_ENABLED + else: + plotting_area_trending = no_update + plotting_area_telemetry = C.PLACEHOLDER + col_plotting_area = C.STYLE_DISABLED + row_card_sel_tests = C.STYLE_DISABLED + row_btns_sel_tests = C.STYLE_DISABLED + row_btns_add_tm = C.STYLE_DISABLED + lg_selected = no_update + store_sel = list() + tm_panels = list() + tm_all_in_one = list() + tm_ignore_host = list() + tm_user = None + else: + plotting_area_trending = no_update + col_plotting_area = no_update + row_card_sel_tests = no_update + row_btns_sel_tests = no_update + row_btns_add_tm = no_update + lg_selected = no_update + + store["url"] = gen_new_url(parsed_url, new_url_params) + store["control-panel"] = ctrl_panel.panel + store["selected-tests"] = store_sel + store["telemetry-data"] = tm_data + store["selected-metrics"] = tm_user + store["telemetry-panels"] = tm_panels + store["telemetry-all-in-one"] = tm_all_in_one + store["telemetry-ignore-host"] = tm_ignore_host + ret_val = [ + store, + plotting_area_trending, + plotting_area_telemetry, + col_plotting_area, + row_card_sel_tests, + row_btns_sel_tests, + row_btns_add_tm, + lg_selected, + search_out, + is_open, + tm_btns_disabled, + tm_dd, + list_metrics + ] + ret_val.extend(ctrl_panel.values) + return ret_val + + @app.callback( + Output("plot-mod-url", "is_open"), + Output("mod-url", "children"), + State("store", "data"), + State("plot-mod-url", "is_open"), + Input("plot-btn-url", "n_clicks") + ) + def toggle_plot_mod_url(store, is_open, n_clicks): + """Toggle the modal window with url. + """ + if not store: + raise PreventUpdate + + if n_clicks: + return not is_open, store.get("url", str()) + return is_open, store["url"] + + def _get_dd_container( + all_labels: dict, + selected_labels: dict=dict(), + show_new=True + ) -> list: + """Generate a container with dropdown selection boxes depenting on + the input data. + + :param all_labels: A dictionary with unique labels and their + possible values. + :param selected_labels: A dictionalry with user selected lables and + their values. + :param show_new: If True, a dropdown selection box to add a new + label is displayed. + :type all_labels: dict + :type selected_labels: dict + :type show_new: bool + :returns: A list of dbc rows with dropdown selection boxes. + :rtype: list + """ + + def _row( + id: str, + lopts: list=list(), + lval: str=str(), + vopts: list=list(), + vvals: list=list() + ) -> dbc.Row: + """Generates a dbc row with dropdown boxes. + + :param id: A string added to the dropdown ID. + :param lopts: A list of options for 'label' dropdown. + :param lval: Value of 'label' dropdown. + :param vopts: A list of options for 'value' dropdown. + :param vvals: A list of values for 'value' dropdown. + :type id: str + :type lopts: list + :type lval: str + :type vopts: list + :type vvals: list + :returns: dbc row with dropdown boxes. + :rtype: dbc.Row + """ + children = list() + if lopts: + children.append( + dbc.Col( + width=6, + children=[ + dcc.Dropdown( + id={ + "type": "tm-dd", + "index": f"label-{id}" + }, + placeholder="Select a label...", + optionHeight=20, + multi=False, + options=lopts, + value=lval if lval else None + ) + ] + ) + ) + if vopts: + children.append( + dbc.Col( + width=6, + children=[ + dcc.Dropdown( + id={ + "type": "tm-dd", + "index": f"value-{id}" + }, + placeholder="Select a value...", + optionHeight=20, + multi=True, + options=vopts, + value=vvals if vvals else None + ) + ] + ) + ) + + return dbc.Row(class_name="g-0 p-1", children=children) + + container = list() + + # Display rows with items in 'selected_labels'; label on the left, + # values on the right: + keys_left = list(all_labels.keys()) + for idx, label in enumerate(selected_labels.keys()): + container.append(_row( + id=idx, + lopts=deepcopy(keys_left), + lval=label, + vopts=all_labels[label], + vvals=selected_labels[label] + )) + keys_left.remove(label) + + # Display row with dd with labels on the left, right side is empty: + if show_new and keys_left: + container.append(_row(id="new", lopts=keys_left)) + + return container + + @app.callback( + Output("metadata-tput-lat", "children"), + Output("metadata-hdrh-graph", "children"), + Output("offcanvas-metadata", "is_open"), + Input({"type": "graph", "index": ALL}, "clickData"), + prevent_initial_call=True + ) + def _show_metadata_from_graphs(graph_data: dict) -> tuple: + """Generates the data for the offcanvas displayed when a particular + point in a graph is clicked on. + + :param graph_data: The data from the clicked point in the graph. + :type graph_data: dict + :returns: The data to be displayed on the offcanvas and the + information to show the offcanvas. + :rtype: tuple(list, list, bool) + """ + + trigger = Trigger(callback_context.triggered) + if not trigger.value: + raise PreventUpdate + + return show_trending_graph_data( + trigger, graph_data, self._graph_layout) + + @app.callback( + Output("download-trending-data", "data"), + State("store", "data"), + Input("plot-btn-download", "n_clicks"), + Input({"type": "tm-btn-download", "index": ALL}, "n_clicks"), + prevent_initial_call=True + ) + def _download_data(store: list, *_) -> dict: + """Download the data + + :param store_sel: List of tests selected by user stored in the + browser. + :type store_sel: list + :returns: dict of data frame content (base64 encoded) and meta data + used by the Download component. + :rtype: dict + """ + + if not store: + raise PreventUpdate + if not store["selected-tests"]: + raise PreventUpdate + + df = pd.DataFrame() + + trigger = Trigger(callback_context.triggered) + if not trigger.value: + raise PreventUpdate + + if trigger.type == "plot-btn-download": + data = list() + for itm in store["selected-tests"]: + sel_data = select_trending_data(self._data, itm) + if sel_data is None: + continue + data.append(sel_data) + df = pd.concat(data, ignore_index=True, copy=False) + file_name = C.TREND_DOWNLOAD_FILE_NAME + elif trigger.type == "tm-btn-download": + tm = TelemetryData(store["selected-tests"]) + tm.from_json(store["telemetry-data"]) + df = tm.select_tm_trending_data( + store["telemetry-panels"][trigger.idx] + ) + file_name = C.TELEMETRY_DOWNLOAD_FILE_NAME + else: + raise PreventUpdate + + return dcc.send_data_frame(df.to_csv, file_name) + + @app.callback( + Output("offcanvas-documentation", "is_open"), + Input("btn-documentation", "n_clicks"), + State("offcanvas-documentation", "is_open") + ) + def toggle_offcanvas_documentation(n_clicks, is_open): + if n_clicks: + return not is_open + return is_open diff --git a/csit.infra.dash/app/cdash/trending/layout.yaml b/csit.infra.dash/app/cdash/trending/layout.yaml new file mode 100644 index 0000000000..e4fcd29260 --- /dev/null +++ b/csit.infra.dash/app/cdash/trending/layout.yaml @@ -0,0 +1,201 @@ +plot-trending-tput: + autosize: True + showlegend: False + yaxis: + showticklabels: True + tickformat: ".3s" + title: "Throughput [pps|cps|rps|bps]" + hoverformat: ".5s" + gridcolor: "rgb(238, 238, 238)" + linecolor: "rgb(238, 238, 238)" + showline: True + zeroline: False + tickcolor: "rgb(238, 238, 238)" + linewidth: 1 + showgrid: True + xaxis: + title: 'Date [MMDD]' + type: "date" + autorange: True + fixedrange: False + showgrid: True + gridcolor: "rgb(238, 238, 238)" + showline: True + linecolor: "rgb(238, 238, 238)" + zeroline: False + linewidth: 1 + showticklabels: True + tickcolor: "rgb(238, 238, 238)" + tickmode: "auto" + tickformat: "%m%d" + margin: + r: 20 + b: 0 + t: 5 + l: 70 + paper_bgcolor: "#fff" + plot_bgcolor: "#fff" + hoverlabel: + namelength: -1 + +plot-trending-bandwidth: + autosize: True + showlegend: False + yaxis: + showticklabels: True + tickformat: ".3s" + title: "Bandwidth [bps]" + hoverformat: ".5s" + gridcolor: "rgb(238, 238, 238)" + linecolor: "rgb(238, 238, 238)" + showline: True + zeroline: False + tickcolor: "rgb(238, 238, 238)" + linewidth: 1 + showgrid: True + xaxis: + title: 'Date [MMDD]' + type: "date" + autorange: True + fixedrange: False + showgrid: True + gridcolor: "rgb(238, 238, 238)" + showline: True + linecolor: "rgb(238, 238, 238)" + zeroline: False + linewidth: 1 + showticklabels: True + tickcolor: "rgb(238, 238, 238)" + tickmode: "auto" + tickformat: "%m%d" + margin: + r: 20 + b: 0 + t: 5 + l: 70 + paper_bgcolor: "#fff" + plot_bgcolor: "#fff" + hoverlabel: + namelength: -1 + +plot-trending-lat: + autosize: True + showlegend: False + yaxis: + showticklabels: True + tickformat: ".3s" + title: "Average Latency at 50% PDR [us]" + hoverformat: ".5s" + gridcolor: "rgb(238, 238, 238)" + linecolor: "rgb(238, 238, 238)" + showline: True + zeroline: False + tickcolor: "rgb(238, 238, 238)" + linewidth: 1 + showgrid: True + xaxis: + title: 'Date [MMDD]' + type: "date" + autorange: True + fixedrange: False + showgrid: True + gridcolor: "rgb(238, 238, 238)" + showline: True + linecolor: "rgb(238, 238, 238)" + zeroline: False + linewidth: 1 + showticklabels: True + tickcolor: "rgb(238, 238, 238)" + tickmode: "auto" + tickformat: "%m%d" + margin: + r: 20 + b: 0 + t: 5 + l: 70 + paper_bgcolor: "#fff" + plot_bgcolor: "#fff" + hoverlabel: + namelength: -1 + +plot-hdrh-latency: + showlegend: True + legend: + traceorder: "normal" + orientation: "h" + xanchor: "left" + yanchor: "top" + x: 0 + y: -0.25 + bgcolor: "rgba(255, 255, 255, 0)" + bordercolor: "rgba(255, 255, 255, 0)" + xaxis: + type: "log" + title: "Percentile [%]" + autorange: True + gridcolor: "rgb(230, 230, 230)" + linecolor: "rgb(220, 220, 220)" + linewidth: 1 + showgrid: True + showline: True + showticklabels: True + tickcolor: "rgb(220, 220, 220)" + tickvals: [1, 2, 1e1, 20, 1e2, 1e3, 1e4, 1e5, 1e6] + ticktext: [0, 50, 90, 95, 99, 99.9, 99.99, 99.999, 99.9999] + yaxis: + title: "One-Way Latency per Direction [us]" + gridcolor: "rgb(230, 230, 230)" + linecolor: "rgb(220, 220, 220)" + linewidth: 1 + showgrid: True + showline: True + showticklabels: True + tickcolor: "rgb(220, 220, 220)" + autosize: True + paper_bgcolor: "white" + plot_bgcolor: "white" + +plot-trending-telemetry: + autosize: True + showlegend: True + yaxis: + showticklabels: True + tickformat: ".3s" + title: "Metric" + hoverformat: ".5s" + gridcolor: "rgb(238, 238, 238)" + linecolor: "rgb(238, 238, 238)" + showline: True + zeroline: False + tickcolor: "rgb(238, 238, 238)" + linewidth: 1 + showgrid: True + xaxis: + title: 'Date [MMDD]' + type: "date" + autorange: True + fixedrange: False + showgrid: True + gridcolor: "rgb(238, 238, 238)" + showline: True + linecolor: "rgb(238, 238, 238)" + zeroline: False + linewidth: 1 + showticklabels: True + tickcolor: "rgb(238, 238, 238)" + tickmode: "auto" + tickformat: "%m%d" + margin: + r: 20 + b: 0 + t: 5 + l: 70 + paper_bgcolor: "#fff" + plot_bgcolor: "#fff" + hoverlabel: + namelength: 50 + legend: + orientation: "h" + y: -0.2 + font: + size: 12 diff --git a/csit.infra.dash/app/cdash/trending/trending.py b/csit.infra.dash/app/cdash/trending/trending.py new file mode 100644 index 0000000000..257e3de625 --- /dev/null +++ b/csit.infra.dash/app/cdash/trending/trending.py @@ -0,0 +1,52 @@ +# Copyright (c) 2024 Cisco and/or its affiliates. +# 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. + +"""Instantiate the Trending Dash application. +""" +import dash +import pandas as pd + +from ..utils.constants import Constants as C +from .layout import Layout + + +def init_trending( + server, + data_trending: pd.DataFrame + ) -> dash.Dash: + """Create a Plotly Dash dashboard. + + :param server: Flask server. + :type server: Flask + :returns: Dash app server. + :rtype: Dash + """ + + dash_app = dash.Dash( + server=server, + routes_pathname_prefix=C.TREND_ROUTES_PATHNAME_PREFIX, + external_stylesheets=C.EXTERNAL_STYLESHEETS, + title=C.TREND_TITLE + ) + + layout = Layout( + app=dash_app, + data_trending=data_trending, + html_layout_file=C.HTML_LAYOUT_FILE, + graph_layout_file=C.TREND_GRAPH_LAYOUT_FILE, + tooltip_file=C.TOOLTIP_FILE + ) + dash_app.index_string = layout.html_layout + dash_app.layout = layout.add_content() + + return dash_app.server |