aboutsummaryrefslogtreecommitdiffstats
path: root/csit.infra.dash/app/cdash/trending
diff options
context:
space:
mode:
authorpmikus <peter.mikus@protonmail.ch>2022-10-05 08:58:31 +0200
committerpmikus <peter.mikus@protonmail.ch>2022-10-05 08:58:31 +0200
commitaf8e703eb180e46ca65ff0c165a21f2261896548 (patch)
treee477719c9010ca3e8ed3ffa63ffe293a2734d358 /csit.infra.dash/app/cdash/trending
parent4d095b586bc4e249ab4e30e1a3f17b310f52a229 (diff)
fix(cdash): Rename
Signed-off-by: pmikus <peter.mikus@protonmail.ch> Change-Id: Ia6dff2674a28b42ebfbe91230587f1e175ae1137
Diffstat (limited to 'csit.infra.dash/app/cdash/trending')
-rw-r--r--csit.infra.dash/app/cdash/trending/__init__.py12
-rw-r--r--csit.infra.dash/app/cdash/trending/graphs.py407
-rw-r--r--csit.infra.dash/app/cdash/trending/layout.py1367
-rw-r--r--csit.infra.dash/app/cdash/trending/layout.yaml129
-rw-r--r--csit.infra.dash/app/cdash/trending/trending.py49
5 files changed, 1964 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..5692432123
--- /dev/null
+++ b/csit.infra.dash/app/cdash/trending/__init__.py
@@ -0,0 +1,12 @@
+# Copyright (c) 2022 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..6f1ec84825
--- /dev/null
+++ b/csit.infra.dash/app/cdash/trending/graphs.py
@@ -0,0 +1,407 @@
+# Copyright (c) 2022 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.
+
+"""
+"""
+
+import plotly.graph_objects as go
+import pandas as pd
+
+import hdrh.histogram
+import hdrh.codec
+
+from ..utils.constants import Constants as C
+from ..utils.utils import classify_anomalies, get_color
+
+
+def _get_hdrh_latencies(row: pd.Series, name: str) -> dict:
+ """Get the HDRH latencies from the test data.
+
+ :param row: A row fron the data frame with test data.
+ :param name: The test name to be displayed as the graph title.
+ :type row: pandas.Series
+ :type name: str
+ :returns: Dictionary with HDRH latencies.
+ :rtype: dict
+ """
+
+ latencies = {"name": name}
+ for key in C.LAT_HDRH:
+ try:
+ latencies[key] = row[key]
+ except KeyError:
+ return None
+
+ return latencies
+
+
+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: str
+ :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
+
+ core = str() if itm["dut"] == "trex" else f"{itm['core']}"
+ ttype = "ndrpdr" if itm["testtype"] in ("ndr", "pdr") else itm["testtype"]
+ dut_v100 = "none" if itm["dut"] == "trex" else itm["dut"]
+ dut_v101 = itm["dut"]
+
+ df = data.loc[(
+ (
+ (
+ (data["version"] == "1.0.0") &
+ (data["dut_type"].str.lower() == dut_v100)
+ ) |
+ (
+ (data["version"] == "1.0.1") &
+ (data["dut_type"].str.lower() == dut_v101)
+ )
+ ) &
+ (data["test_type"] == ttype) &
+ (data["passed"] == True)
+ )]
+ df = df[df.job.str.endswith(f"{topo}-{arch}")]
+ 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 _generate_trending_traces(ttype: str, name: str, df: pd.DataFrame,
+ color: str, norm_factor: 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 norm_factor: 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 norm_factor: float
+ :returns: Traces (samples, trending line, anomalies)
+ :rtype: list
+ """
+
+ df = df.dropna(subset=[C.VALUE[ttype], ])
+ if df.empty:
+ return list()
+
+ x_axis = df["start_time"].tolist()
+ if ttype == "pdr-lat":
+ y_data = [(itm / norm_factor) for itm in df[C.VALUE[ttype]].tolist()]
+ else:
+ y_data = [(itm * norm_factor) for itm in df[C.VALUE[ttype]].tolist()]
+
+ anomalies, trend_avg, trend_stdev = classify_anomalies(
+ {k: v for k, v in zip(x_axis, y_data)}
+ )
+
+ hover = list()
+ customdata = list()
+ customdata_samples = list()
+ for idx, (_, row) in enumerate(df.iterrows()):
+ d_type = "trex" if row["dut_type"] == "none" else row["dut_type"]
+ hover_itm = (
+ f"date: {row['start_time'].strftime('%Y-%m-%d %H:%M:%S')}<br>"
+ f"<prop> [{row[C.UNIT[ttype]]}]: {y_data[idx]:,.0f}<br>"
+ f"<stdev>"
+ f"{d_type}-ref: {row['dut_version']}<br>"
+ f"csit-ref: {row['job']}/{row['build']}<br>"
+ f"hosts: {', '.join(row['hosts'])}"
+ )
+ if ttype == "mrr":
+ stdev = (
+ f"stdev [{row['result_receive_rate_rate_unit']}]: "
+ f"{row['result_receive_rate_rate_stdev']:,.0f}<br>"
+ )
+ else:
+ stdev = ""
+ hover_itm = hover_itm.replace(
+ "<prop>", "latency" if ttype == "pdr-lat" else "average"
+ ).replace("<stdev>", stdev)
+ hover.append(hover_itm)
+ if ttype == "pdr-lat":
+ 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})
+
+ hover_trend = list()
+ for avg, stdev, (_, row) in zip(trend_avg, trend_stdev, df.iterrows()):
+ d_type = "trex" if row["dut_type"] == "none" else row["dut_type"]
+ hover_itm = (
+ f"date: {row['start_time'].strftime('%Y-%m-%d %H:%M:%S')}<br>"
+ f"trend [pps]: {avg:,.0f}<br>"
+ f"stdev [pps]: {stdev:,.0f}<br>"
+ f"{d_type}-ref: {row['dut_version']}<br>"
+ f"csit-ref: {row['job']}/{row['build']}<br>"
+ f"hosts: {', '.join(row['hosts'])}"
+ )
+ if ttype == "pdr-lat":
+ 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+name",
+ 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+name",
+ 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"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 == "pdr-lat":
+ 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+name",
+ showlegend=False,
+ legendgroup=name,
+ name=name,
+ customdata=customdata,
+ marker={
+ "size": 15,
+ "symbol": "circle-open",
+ "color": anomaly_color,
+ "colorscale": C.COLORSCALE_LAT \
+ if ttype == "pdr-lat" 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 == "pdr-lat" else C.TICK_TEXT_TPUT,
+ "ticks": "",
+ "ticklen": 0,
+ "tickangle": -90,
+ "thickness": 10
+ }
+ }
+ )
+ )
+
+ return traces
+
+
+def graph_trending(data: pd.DataFrame, sel:dict, layout: dict,
+ normalize: bool) -> 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
+
+ fig_tput = None
+ fig_lat = None
+ 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[topo_arch]) \
+ if topo_arch else 1.0
+ else:
+ norm_factor = 1.0
+ traces = _generate_trending_traces(itm["testtype"], itm["id"], df,
+ get_color(idx), norm_factor)
+ if traces:
+ if not fig_tput:
+ fig_tput = go.Figure()
+ fig_tput.add_traces(traces)
+
+ if itm["testtype"] == "pdr":
+ traces = _generate_trending_traces("pdr-lat", itm["id"], df,
+ get_color(idx), norm_factor)
+ if traces:
+ if not fig_lat:
+ fig_lat = go.Figure()
+ fig_lat.add_traces(traces)
+
+ if fig_tput:
+ fig_tput.update_layout(layout.get("plot-trending-tput", dict()))
+ if fig_lat:
+ fig_lat.update_layout(layout.get("plot-trending-lat", dict()))
+
+ return fig_tput, fig_lat
+
+
+def graph_hdrh_latency(data: dict, layout: dict) -> go.Figure:
+ """Generate HDR Latency histogram graphs.
+
+ :param data: HDRH data.
+ :param layout: Layout of plot.ly graph.
+ :type data: dict
+ :type layout: dict
+ :returns: HDR latency Histogram.
+ :rtype: plotly.graph_objects.Figure
+ """
+
+ fig = None
+
+ traces = list()
+ for idx, (lat_name, lat_hdrh) in enumerate(data.items()):
+ try:
+ decoded = hdrh.histogram.HdrHistogram.decode(lat_hdrh)
+ except (hdrh.codec.HdrLengthException, TypeError):
+ continue
+ previous_x = 0.0
+ prev_perc = 0.0
+ xaxis = list()
+ yaxis = list()
+ hovertext = list()
+ for item in decoded.get_recorded_iterator():
+ # The real value is "percentile".
+ # For 100%, we cut that down to "x_perc" to avoid
+ # infinity.
+ percentile = item.percentile_level_iterated_to
+ x_perc = min(percentile, C.PERCENTILE_MAX)
+ xaxis.append(previous_x)
+ yaxis.append(item.value_iterated_to)
+ hovertext.append(
+ f"<b>{C.GRAPH_LAT_HDRH_DESC[lat_name]}</b><br>"
+ f"Direction: {('W-E', 'E-W')[idx % 2]}<br>"
+ f"Percentile: {prev_perc:.5f}-{percentile:.5f}%<br>"
+ f"Latency: {item.value_iterated_to}uSec"
+ )
+ next_x = 100.0 / (100.0 - x_perc)
+ xaxis.append(next_x)
+ yaxis.append(item.value_iterated_to)
+ hovertext.append(
+ f"<b>{C.GRAPH_LAT_HDRH_DESC[lat_name]}</b><br>"
+ f"Direction: {('W-E', 'E-W')[idx % 2]}<br>"
+ f"Percentile: {prev_perc:.5f}-{percentile:.5f}%<br>"
+ f"Latency: {item.value_iterated_to}uSec"
+ )
+ previous_x = next_x
+ prev_perc = percentile
+
+ traces.append(
+ go.Scatter(
+ x=xaxis,
+ y=yaxis,
+ name=C.GRAPH_LAT_HDRH_DESC[lat_name],
+ mode="lines",
+ legendgroup=C.GRAPH_LAT_HDRH_DESC[lat_name],
+ showlegend=bool(idx % 2),
+ line=dict(
+ color=get_color(int(idx/2)),
+ dash="solid",
+ width=1 if idx % 2 else 2
+ ),
+ hovertext=hovertext,
+ hoverinfo="text"
+ )
+ )
+ if traces:
+ fig = go.Figure()
+ fig.add_traces(traces)
+ layout_hdrh = layout.get("plot-hdrh-latency", None)
+ if lat_hdrh:
+ fig.update_layout(layout_hdrh)
+
+ return fig
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..14493ff2d9
--- /dev/null
+++ b/csit.infra.dash/app/cdash/trending/layout.py
@@ -0,0 +1,1367 @@
+# Copyright (c) 2022 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 datetime import datetime
+from copy import deepcopy
+from json import loads, JSONDecodeError
+from ast import literal_eval
+
+from ..utils.constants import Constants as C
+from ..utils.utils import show_tooltip, label, sync_checklists, list_tests, \
+ gen_new_url, generate_options
+from ..utils.url_processing import url_decode
+from ..data.data import Data
+from .graphs import graph_trending, graph_hdrh_latency, \
+ select_trending_data
+
+
+class Layout:
+ """The layout of the dash app and the callbacks.
+ """
+
+ def __init__(self, app: Flask, html_layout_file: str,
+ graph_layout_file: str, data_spec_file: str, tooltip_file: str,
+ time_period: str=None) -> 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 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 data_spec_file: Path and name of the file specifying the data to
+ be read from parquets for this application.
+ :param tooltip_file: Path and name of the yaml file specifying the
+ tooltips.
+ :param time_period: It defines the time period for data read from the
+ parquets in days from now back to the past.
+ :type app: Flask
+ :type html_layout_file: str
+ :type graph_layout_file: str
+ :type data_spec_file: str
+ :type tooltip_file: str
+ :type time_period: int
+ """
+
+ # Inputs
+ self._app = app
+ self._html_layout_file = html_layout_file
+ self._graph_layout_file = graph_layout_file
+ self._data_spec_file = data_spec_file
+ self._tooltip_file = tooltip_file
+ self._time_period = time_period
+
+ # Read the data:
+ data_mrr = Data(
+ data_spec_file=self._data_spec_file,
+ debug=True
+ ).read_trending_mrr(days=self._time_period)
+
+ data_ndrpdr = Data(
+ data_spec_file=self._data_spec_file,
+ debug=True
+ ).read_trending_ndrpdr(days=self._time_period)
+
+ self._data = pd.concat([data_mrr, data_ndrpdr], ignore_index=True)
+
+ data_time_period = \
+ (datetime.utcnow() - self._data["start_time"].min()).days
+ if self._time_period > data_time_period:
+ self._time_period = data_time_period
+
+
+ # Get structure of tests:
+ tbs = dict()
+ for _, row in self._data[["job", "test_id"]].drop_duplicates().\
+ iterrows():
+ lst_job = row["job"].split("-")
+ dut = lst_job[1]
+ ttype = lst_job[3]
+ 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(infra, None) is None:
+ tbs[dut][infra] = dict()
+ if tbs[dut][infra].get(area, None) is None:
+ tbs[dut][infra][area] = dict()
+ if tbs[dut][infra][area].get(test, None) is None:
+ tbs[dut][infra][area][test] = dict()
+ tbs[dut][infra][area][test]["core"] = list()
+ tbs[dut][infra][area][test]["frame-size"] = list()
+ tbs[dut][infra][area][test]["test-type"] = list()
+ if core.upper() not in tbs[dut][infra][area][test]["core"]:
+ tbs[dut][infra][area][test]["core"].append(core.upper())
+ if framesize.upper() not in \
+ tbs[dut][infra][area][test]["frame-size"]:
+ tbs[dut][infra][area][test]["frame-size"].append(
+ framesize.upper())
+ if ttype == "mrr":
+ if "MRR" not in tbs[dut][infra][area][test]["test-type"]:
+ tbs[dut][infra][area][test]["test-type"].append("MRR")
+ elif ttype == "ndrpdr":
+ if "NDR" not in tbs[dut][infra][area][test]["test-type"]:
+ tbs[dut][infra][area][test]["test-type"].extend(
+ ("NDR", "PDR"))
+ self._spec_tbs = tbs
+
+ # Read from files:
+ self._html_layout = ""
+ 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
+
+ @property
+ def spec_tbs(self):
+ return self._spec_tbs
+
+ @property
+ def data(self):
+ return self._data
+
+ @property
+ def layout(self):
+ return self._graph_layout
+
+ @property
+ def time_period(self):
+ return self._time_period
+
+ 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=[
+ dbc.Row(
+ id="row-navbar",
+ class_name="g-0",
+ children=[
+ self._add_navbar(),
+ ]
+ ),
+ dcc.Loading(
+ dbc.Offcanvas(
+ class_name="w-50",
+ id="offcanvas-metadata",
+ title="Throughput And Latency",
+ placement="end",
+ is_open=False,
+ children=[
+ dbc.Row(id="metadata-tput-lat"),
+ dbc.Row(id="metadata-hdrh-graph"),
+ ]
+ )
+ ),
+ dbc.Row(
+ id="row-main",
+ class_name="g-0",
+ children=[
+ dcc.Store(id="selected-tests"),
+ dcc.Store(id="control-panel"),
+ dcc.Location(id="url", refresh=False),
+ self._add_ctrl_col(),
+ self._add_plotting_col(),
+ ]
+ )
+ ]
+ )
+ else:
+ return html.Div(
+ id="div-main-error",
+ children=[
+ dbc.Alert(
+ [
+ "An Error Occured",
+ ],
+ color="danger",
+ ),
+ ]
+ )
+
+ def _add_navbar(self):
+ """Add nav element with navigation panel. It is placed on the top.
+
+ :returns: Navigation bar.
+ :rtype: dbc.NavbarSimple
+ """
+ return dbc.NavbarSimple(
+ id="navbarsimple-main",
+ children=[
+ dbc.NavItem(
+ dbc.NavLink(
+ C.TREND_TITLE,
+ disabled=True,
+ external_link=True,
+ href="#"
+ )
+ )
+ ],
+ brand=C.BRAND,
+ brand_href="/",
+ brand_external_link=True,
+ class_name="p-2",
+ fluid=True,
+ )
+
+ 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(
+ children=self._add_ctrl_panel(),
+ className="sticky-top"
+ )
+ ])
+
+ def _add_plotting_col(self) -> dbc.Col:
+ """Add column with plots and tables. It is placed on the right side.
+
+ :returns: Column with tables.
+ :rtype: dbc.Col
+ """
+ return dbc.Col(
+ id="col-plotting-area",
+ children=[
+ dcc.Loading(
+ children=[
+ dbc.Row( # Throughput
+ id="row-graph-tput",
+ class_name="g-0 p-2",
+ children=[
+ C.PLACEHOLDER
+ ]
+ ),
+ dbc.Row( # Latency
+ id="row-graph-lat",
+ class_name="g-0 p-2",
+ children=[
+ C.PLACEHOLDER
+ ]
+ ),
+ dbc.Row( # Download
+ id="row-btn-download",
+ class_name="g-0 p-2",
+ children=[
+ C.PLACEHOLDER
+ ]
+ )
+ ]
+ )
+ ],
+ width=9,
+ )
+
+ def _add_ctrl_panel(self) -> dbc.Row:
+ """Add control panel.
+
+ :returns: Control panel.
+ :rtype: dbc.Row
+ """
+ return [
+ dbc.Row(
+ class_name="g-0 p-1",
+ children=[
+ dbc.InputGroup(
+ [
+ dbc.InputGroupText(
+ children=show_tooltip(self._tooltips,
+ "help-dut", "DUT")
+ ),
+ dbc.Select(
+ id="dd-ctrl-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",
+ ),
+ ]
+ ),
+ dbc.Row(
+ class_name="g-0 p-1",
+ children=[
+ dbc.InputGroup(
+ [
+ dbc.InputGroupText(
+ children=show_tooltip(self._tooltips,
+ "help-infra", "Infra")
+ ),
+ dbc.Select(
+ id="dd-ctrl-phy",
+ placeholder=(
+ "Select a Physical Test Bed "
+ "Topology..."
+ )
+ )
+ ],
+ size="sm",
+ ),
+ ]
+ ),
+ dbc.Row(
+ class_name="g-0 p-1",
+ children=[
+ dbc.InputGroup(
+ [
+ dbc.InputGroupText(
+ children=show_tooltip(self._tooltips,
+ "help-area", "Area")
+ ),
+ dbc.Select(
+ id="dd-ctrl-area",
+ placeholder="Select an Area...",
+ disabled=True,
+ ),
+ ],
+ size="sm",
+ ),
+ ]
+ ),
+ dbc.Row(
+ class_name="g-0 p-1",
+ children=[
+ dbc.InputGroup(
+ [
+ dbc.InputGroupText(
+ children=show_tooltip(self._tooltips,
+ "help-test", "Test")
+ ),
+ dbc.Select(
+ id="dd-ctrl-test",
+ placeholder="Select a Test...",
+ disabled=True,
+ ),
+ ],
+ size="sm",
+ ),
+ ]
+ ),
+ dbc.Row(
+ class_name="g-0 p-1",
+ children=[
+ dbc.Label(
+ children=show_tooltip(self._tooltips,
+ "help-framesize", "Frame Size"),
+ ),
+ dbc.Col(
+ children=[
+ dbc.Checklist(
+ id="cl-ctrl-framesize-all",
+ options=C.CL_ALL_DISABLED,
+ inline=True,
+ switch=False
+ ),
+ ],
+ width=3
+ ),
+ dbc.Col(
+ children=[
+ dbc.Checklist(
+ id="cl-ctrl-framesize",
+ inline=True,
+ switch=False
+ )
+ ]
+ )
+ ]
+ ),
+ dbc.Row(
+ class_name="g-0 p-1",
+ children=[
+ dbc.Label(
+ children=show_tooltip(self._tooltips,
+ "help-cores", "Number of Cores"),
+ ),
+ dbc.Col(
+ children=[
+ dbc.Checklist(
+ id="cl-ctrl-core-all",
+ options=C.CL_ALL_DISABLED,
+ inline=False,
+ switch=False
+ )
+ ],
+ width=3
+ ),
+ dbc.Col(
+ children=[
+ dbc.Checklist(
+ id="cl-ctrl-core",
+ inline=True,
+ switch=False
+ )
+ ]
+ )
+ ]
+ ),
+ dbc.Row(
+ class_name="g-0 p-1",
+ children=[
+ dbc.Label(
+ children=show_tooltip(self._tooltips,
+ "help-ttype", "Test Type"),
+ ),
+ dbc.Col(
+ children=[
+ dbc.Checklist(
+ id="cl-ctrl-testtype-all",
+ options=C.CL_ALL_DISABLED,
+ inline=True,
+ switch=False
+ ),
+ ],
+ width=3
+ ),
+ dbc.Col(
+ children=[
+ dbc.Checklist(
+ id="cl-ctrl-testtype",
+ inline=True,
+ switch=False
+ )
+ ]
+ )
+ ]
+ ),
+ dbc.Row(
+ class_name="g-0 p-1",
+ children=[
+ dbc.Label(
+ children=show_tooltip(self._tooltips,
+ "help-normalize", "Normalize"),
+ ),
+ dbc.Col(
+ children=[
+ dbc.Checklist(
+ id="cl-ctrl-normalize",
+ options=[{
+ "value": "normalize",
+ "label": (
+ "Normalize results to CPU "
+ "frequency 2GHz"
+ )
+ }],
+ value=[],
+ inline=True,
+ switch=False
+ ),
+ ]
+ )
+ ]
+ ),
+ dbc.Row(
+ class_name="g-0 p-1",
+ children=[
+ dbc.ButtonGroup(
+ [
+ dbc.Button(
+ id="btn-ctrl-add",
+ children="Add Selected",
+ color="info"
+ )
+ ]
+ )
+ ]
+ ),
+ dbc.Row(
+ id="row-card-sel-tests",
+ class_name="g-0 p-1",
+ style=C.STYLE_DISABLED,
+ children=[
+ dbc.Label("Selected tests"),
+ dbc.Checklist(
+ class_name="overflow-auto",
+ id="cl-selected",
+ options=[],
+ inline=False,
+ style={"max-height": "12em"},
+ )
+ ],
+ ),
+ dbc.Row(
+ id="row-btns-sel-tests",
+ class_name="g-0 p-1",
+ style=C.STYLE_DISABLED,
+ children=[
+ dbc.ButtonGroup(
+ children=[
+ dbc.Button(
+ id="btn-sel-remove",
+ children="Remove Selected",
+ class_name="w-100",
+ color="info",
+ disabled=False
+ ),
+ dbc.Button(
+ id="btn-sel-remove-all",
+ children="Remove All",
+ class_name="w-100",
+ color="info",
+ disabled=False
+ ),
+ ]
+ )
+ ]
+ ),
+ ]
+
+ class ControlPanel:
+ """A class representing the control panel.
+ """
+
+ def __init__(self, panel: dict) -> None:
+ """Initialisation of the control pannel by default values. If
+ particular values are provided (parameter "panel") they are set
+ afterwards.
+
+ :param panel: Custom values to be set to the control panel.
+ :param default: Default values to be set to the control panel.
+ :type panel: dict
+ :type defaults: dict
+ """
+
+ # Defines also the order of keys
+ self._defaults = {
+ "dd-ctrl-dut-value": str(),
+ "dd-ctrl-phy-options": list(),
+ "dd-ctrl-phy-disabled": True,
+ "dd-ctrl-phy-value": str(),
+ "dd-ctrl-area-options": list(),
+ "dd-ctrl-area-disabled": True,
+ "dd-ctrl-area-value": str(),
+ "dd-ctrl-test-options": list(),
+ "dd-ctrl-test-disabled": True,
+ "dd-ctrl-test-value": str(),
+ "cl-ctrl-core-options": list(),
+ "cl-ctrl-core-value": list(),
+ "cl-ctrl-core-all-value": list(),
+ "cl-ctrl-core-all-options": C.CL_ALL_DISABLED,
+ "cl-ctrl-framesize-options": list(),
+ "cl-ctrl-framesize-value": list(),
+ "cl-ctrl-framesize-all-value": list(),
+ "cl-ctrl-framesize-all-options": C.CL_ALL_DISABLED,
+ "cl-ctrl-testtype-options": list(),
+ "cl-ctrl-testtype-value": list(),
+ "cl-ctrl-testtype-all-value": list(),
+ "cl-ctrl-testtype-all-options": C.CL_ALL_DISABLED,
+ "btn-ctrl-add-disabled": True,
+ "cl-normalize-value": list(),
+ "cl-selected-options": list()
+ }
+
+ self._panel = deepcopy(self._defaults)
+ if panel:
+ for key in self._defaults:
+ self._panel[key] = panel[key]
+
+ @property
+ def defaults(self) -> dict:
+ return self._defaults
+
+ @property
+ def panel(self) -> dict:
+ return self._panel
+
+ def set(self, kwargs: dict) -> None:
+ """Set the values of the Control panel.
+
+ :param kwargs: key - value pairs to be set.
+ :type kwargs: dict
+ :raises KeyError: If the key in kwargs is not present in the Control
+ panel.
+ """
+ for key, val in kwargs.items():
+ if key in self._panel:
+ self._panel[key] = val
+ else:
+ raise KeyError(f"The key {key} is not defined.")
+
+ def get(self, key: str) -> any:
+ """Returns the value of a key from the Control panel.
+
+ :param key: The key which value should be returned.
+ :type key: str
+ :returns: The value of the key.
+ :rtype: any
+ :raises KeyError: If the key in kwargs is not present in the Control
+ panel.
+ """
+ return self._panel[key]
+
+ def values(self) -> tuple:
+ """Returns the values from the Control panel as a list.
+
+ :returns: The values from the Control panel.
+ :rtype: list
+ """
+ return tuple(self._panel.values())
+
+ def callbacks(self, app):
+ """Callbacks for the whole application.
+
+ :param app: The application.
+ :type app: Flask
+ """
+
+ def _generate_plotting_area(figs: tuple, url: str) -> tuple:
+ """Generate the plotting area with all its content.
+
+ :param figs: Figures to be placed in the plotting area.
+ :param utl: The URL to be placed in the plotting area bellow the
+ tables.
+ :type figs: tuple of plotly.graph_objects.Figure
+ :type url: str
+ :returns: tuple of elements to be shown in the plotting area.
+ :rtype: tuple(dcc.Graph, dcc.Graph, list(dbc.Col, dbc.Col))
+ """
+
+ (fig_tput, fig_lat) = figs
+
+ row_fig_tput = C.PLACEHOLDER
+ row_fig_lat = C.PLACEHOLDER
+ row_btn_dwnld = C.PLACEHOLDER
+
+ if fig_tput:
+ row_fig_tput = [
+ dcc.Graph(
+ id={"type": "graph", "index": "tput"},
+ figure=fig_tput
+ )
+ ]
+ row_btn_dwnld = [
+ dbc.Col( # Download
+ width=2,
+ children=[
+ dcc.Loading(children=[
+ dbc.Button(
+ id="btn-download-data",
+ children=show_tooltip(self._tooltips,
+ "help-download", "Download Data"),
+ class_name="me-1",
+ color="info"
+ ),
+ dcc.Download(id="download-data")
+ ]),
+ ]
+ ),
+ dbc.Col( # Show URL
+ width=10,
+ children=[
+ dbc.InputGroup(
+ class_name="me-1",
+ children=[
+ dbc.InputGroupText(
+ style=C.URL_STYLE,
+ children=show_tooltip(self._tooltips,
+ "help-url", "URL", "input-url")
+ ),
+ dbc.Input(
+ id="input-url",
+ readonly=True,
+ type="url",
+ style=C.URL_STYLE,
+ value=url
+ )
+ ]
+ )
+ ]
+ )
+ ]
+ if fig_lat:
+ row_fig_lat = [
+ dcc.Graph(
+ id={"type": "graph", "index": "lat"},
+ figure=fig_lat
+ )
+ ]
+
+ return row_fig_tput, row_fig_lat, row_btn_dwnld
+
+ @app.callback(
+ Output("control-panel", "data"), # Store
+ Output("selected-tests", "data"), # Store
+ Output("row-graph-tput", "children"),
+ Output("row-graph-lat", "children"),
+ Output("row-btn-download", "children"),
+ Output("row-card-sel-tests", "style"),
+ Output("row-btns-sel-tests", "style"),
+ Output("dd-ctrl-dut", "value"),
+ Output("dd-ctrl-phy", "options"),
+ Output("dd-ctrl-phy", "disabled"),
+ Output("dd-ctrl-phy", "value"),
+ Output("dd-ctrl-area", "options"),
+ Output("dd-ctrl-area", "disabled"),
+ Output("dd-ctrl-area", "value"),
+ Output("dd-ctrl-test", "options"),
+ Output("dd-ctrl-test", "disabled"),
+ Output("dd-ctrl-test", "value"),
+ Output("cl-ctrl-core", "options"),
+ Output("cl-ctrl-core", "value"),
+ Output("cl-ctrl-core-all", "value"),
+ Output("cl-ctrl-core-all", "options"),
+ Output("cl-ctrl-framesize", "options"),
+ Output("cl-ctrl-framesize", "value"),
+ Output("cl-ctrl-framesize-all", "value"),
+ Output("cl-ctrl-framesize-all", "options"),
+ Output("cl-ctrl-testtype", "options"),
+ Output("cl-ctrl-testtype", "value"),
+ Output("cl-ctrl-testtype-all", "value"),
+ Output("cl-ctrl-testtype-all", "options"),
+ Output("btn-ctrl-add", "disabled"),
+ Output("cl-ctrl-normalize", "value"),
+ Output("cl-selected", "options"), # User selection
+ State("control-panel", "data"), # Store
+ State("selected-tests", "data"), # Store
+ State("cl-selected", "value"), # User selection
+ Input("dd-ctrl-dut", "value"),
+ Input("dd-ctrl-phy", "value"),
+ Input("dd-ctrl-area", "value"),
+ Input("dd-ctrl-test", "value"),
+ Input("cl-ctrl-core", "value"),
+ Input("cl-ctrl-core-all", "value"),
+ Input("cl-ctrl-framesize", "value"),
+ Input("cl-ctrl-framesize-all", "value"),
+ Input("cl-ctrl-testtype", "value"),
+ Input("cl-ctrl-testtype-all", "value"),
+ Input("cl-ctrl-normalize", "value"),
+ Input("btn-ctrl-add", "n_clicks"),
+ Input("btn-sel-remove", "n_clicks"),
+ Input("btn-sel-remove-all", "n_clicks"),
+ Input("url", "href")
+ )
+ def _update_ctrl_panel(cp_data: dict, store_sel: list, list_sel: list,
+ dd_dut: str, dd_phy: str, dd_area: str, dd_test: str, cl_core: list,
+ cl_core_all: list, cl_framesize: list, cl_framesize_all: list,
+ cl_testtype: list, cl_testtype_all: list, cl_normalize: list,
+ btn_add: int, btn_remove: int,
+ btn_remove_all: int, href: str) -> tuple:
+ """Update the application when the event is detected.
+
+ :param cp_data: Current status of the control panel stored in
+ browser.
+ :param store_sel: List of tests selected by user stored in the
+ browser.
+ :param list_sel: List of tests selected by the user shown in the
+ checklist.
+ :param dd_dut: Input - DUTs.
+ :param dd_phy: Input - topo- arch-nic-driver.
+ :param dd_area: Input - Tested area.
+ :param dd_test: Input - Test.
+ :param cl_core: Input - Number of cores.
+ :param cl_core_all: Input - All numbers of cores.
+ :param cl_framesize: Input - Frame sizes.
+ :param cl_framesize_all: Input - All frame sizes.
+ :param cl_testtype: Input - Test type (NDR, PDR, MRR).
+ :param cl_testtype_all: Input - All test types.
+ :param cl_normalize: Input - Normalize the results.
+ :param btn_add: Input - Button "Add Selected" tests.
+ :param btn_remove: Input - Button "Remove selected" tests.
+ :param btn_remove_all: Input - Button "Remove All" tests.
+ :param href: Input - The URL provided by the browser.
+ :type cp_data: dict
+ :type store_sel: list
+ :type list_sel: list
+ :type dd_dut: str
+ :type dd_phy: str
+ :type dd_area: str
+ :type dd_test: str
+ :type cl_core: list
+ :type cl_core_all: list
+ :type cl_framesize: list
+ :type cl_framesize_all: list
+ :type cl_testtype: list
+ :type cl_testtype_all: list
+ :type cl_normalize: list
+ :type btn_add: int
+ :type btn_remove: int
+ :type btn_remove_all: int
+ :type href: str
+ :returns: New values for web page elements.
+ :rtype: tuple
+ """
+
+ ctrl_panel = self.ControlPanel(cp_data)
+ norm = cl_normalize
+
+ # Parse the url:
+ parsed_url = url_decode(href)
+ if parsed_url:
+ url_params = parsed_url["params"]
+ else:
+ url_params = None
+
+ row_fig_tput = no_update
+ row_fig_lat = no_update
+ row_btn_dwnld = no_update
+ row_card_sel_tests = no_update
+ row_btns_sel_tests = no_update
+
+ trigger_id = callback_context.triggered[0]["prop_id"].split(".")[0]
+
+ if trigger_id == "dd-ctrl-dut":
+ try:
+ options = \
+ generate_options(sorted(self.spec_tbs[dd_dut].keys()))
+ disabled = False
+ except KeyError:
+ options = list()
+ disabled = True
+ ctrl_panel.set({
+ "dd-ctrl-dut-value": dd_dut,
+ "dd-ctrl-phy-value": str(),
+ "dd-ctrl-phy-options": options,
+ "dd-ctrl-phy-disabled": disabled,
+ "dd-ctrl-area-value": str(),
+ "dd-ctrl-area-options": list(),
+ "dd-ctrl-area-disabled": True,
+ "dd-ctrl-test-value": str(),
+ "dd-ctrl-test-options": list(),
+ "dd-ctrl-test-disabled": True,
+ "cl-ctrl-core-options": list(),
+ "cl-ctrl-core-value": list(),
+ "cl-ctrl-core-all-value": list(),
+ "cl-ctrl-core-all-options": C.CL_ALL_DISABLED,
+ "cl-ctrl-framesize-options": list(),
+ "cl-ctrl-framesize-value": list(),
+ "cl-ctrl-framesize-all-value": list(),
+ "cl-ctrl-framesize-all-options": C.CL_ALL_DISABLED,
+ "cl-ctrl-testtype-options": list(),
+ "cl-ctrl-testtype-value": list(),
+ "cl-ctrl-testtype-all-value": list(),
+ "cl-ctrl-testtype-all-options": C.CL_ALL_DISABLED,
+ })
+ elif trigger_id == "dd-ctrl-phy":
+ try:
+ dut = ctrl_panel.get("dd-ctrl-dut-value")
+ phy = self.spec_tbs[dut][dd_phy]
+ options = [{"label": label(v), "value": v} \
+ for v in sorted(phy.keys())]
+ disabled = False
+ except KeyError:
+ options = list()
+ disabled = True
+ ctrl_panel.set({
+ "dd-ctrl-phy-value": dd_phy,
+ "dd-ctrl-area-value": str(),
+ "dd-ctrl-area-options": options,
+ "dd-ctrl-area-disabled": disabled,
+ "dd-ctrl-test-value": str(),
+ "dd-ctrl-test-options": list(),
+ "dd-ctrl-test-disabled": True,
+ "cl-ctrl-core-options": list(),
+ "cl-ctrl-core-value": list(),
+ "cl-ctrl-core-all-value": list(),
+ "cl-ctrl-core-all-options": C.CL_ALL_DISABLED,
+ "cl-ctrl-framesize-options": list(),
+ "cl-ctrl-framesize-value": list(),
+ "cl-ctrl-framesize-all-value": list(),
+ "cl-ctrl-framesize-all-options": C.CL_ALL_DISABLED,
+ "cl-ctrl-testtype-options": list(),
+ "cl-ctrl-testtype-value": list(),
+ "cl-ctrl-testtype-all-value": list(),
+ "cl-ctrl-testtype-all-options": C.CL_ALL_DISABLED,
+ })
+ elif trigger_id == "dd-ctrl-area":
+ try:
+ dut = ctrl_panel.get("dd-ctrl-dut-value")
+ phy = ctrl_panel.get("dd-ctrl-phy-value")
+ area = self.spec_tbs[dut][phy][dd_area]
+ options = generate_options(sorted(area.keys()))
+ disabled = False
+ except KeyError:
+ options = list()
+ disabled = True
+ ctrl_panel.set({
+ "dd-ctrl-area-value": dd_area,
+ "dd-ctrl-test-value": str(),
+ "dd-ctrl-test-options": options,
+ "dd-ctrl-test-disabled": disabled,
+ "cl-ctrl-core-options": list(),
+ "cl-ctrl-core-value": list(),
+ "cl-ctrl-core-all-value": list(),
+ "cl-ctrl-core-all-options": C.CL_ALL_DISABLED,
+ "cl-ctrl-framesize-options": list(),
+ "cl-ctrl-framesize-value": list(),
+ "cl-ctrl-framesize-all-value": list(),
+ "cl-ctrl-framesize-all-options": C.CL_ALL_DISABLED,
+ "cl-ctrl-testtype-options": list(),
+ "cl-ctrl-testtype-value": list(),
+ "cl-ctrl-testtype-all-value": list(),
+ "cl-ctrl-testtype-all-options": C.CL_ALL_DISABLED,
+ })
+ elif trigger_id == "dd-ctrl-test":
+ dut = ctrl_panel.get("dd-ctrl-dut-value")
+ phy = ctrl_panel.get("dd-ctrl-phy-value")
+ area = ctrl_panel.get("dd-ctrl-area-value")
+ if all((dut, phy, area, dd_test, )):
+ test = self.spec_tbs[dut][phy][area][dd_test]
+ ctrl_panel.set({
+ "dd-ctrl-test-value": dd_test,
+ "cl-ctrl-core-options": \
+ generate_options(sorted(test["core"])),
+ "cl-ctrl-core-value": list(),
+ "cl-ctrl-core-all-value": list(),
+ "cl-ctrl-core-all-options": C.CL_ALL_ENABLED,
+ "cl-ctrl-framesize-options": \
+ generate_options(sorted(test["frame-size"])),
+ "cl-ctrl-framesize-value": list(),
+ "cl-ctrl-framesize-all-value": list(),
+ "cl-ctrl-framesize-all-options": C.CL_ALL_ENABLED,
+ "cl-ctrl-testtype-options": \
+ generate_options(sorted(test["test-type"])),
+ "cl-ctrl-testtype-value": list(),
+ "cl-ctrl-testtype-all-value": list(),
+ "cl-ctrl-testtype-all-options": C.CL_ALL_ENABLED,
+ })
+ elif trigger_id == "cl-ctrl-core":
+ val_sel, val_all = sync_checklists(
+ options=ctrl_panel.get("cl-ctrl-core-options"),
+ sel=cl_core,
+ all=list(),
+ id=""
+ )
+ ctrl_panel.set({
+ "cl-ctrl-core-value": val_sel,
+ "cl-ctrl-core-all-value": val_all,
+ })
+ elif trigger_id == "cl-ctrl-core-all":
+ val_sel, val_all = sync_checklists(
+ options = ctrl_panel.get("cl-ctrl-core-options"),
+ sel=list(),
+ all=cl_core_all,
+ id="all"
+ )
+ ctrl_panel.set({
+ "cl-ctrl-core-value": val_sel,
+ "cl-ctrl-core-all-value": val_all,
+ })
+ elif trigger_id == "cl-ctrl-framesize":
+ val_sel, val_all = sync_checklists(
+ options = ctrl_panel.get("cl-ctrl-framesize-options"),
+ sel=cl_framesize,
+ all=list(),
+ id=""
+ )
+ ctrl_panel.set({
+ "cl-ctrl-framesize-value": val_sel,
+ "cl-ctrl-framesize-all-value": val_all,
+ })
+ elif trigger_id == "cl-ctrl-framesize-all":
+ val_sel, val_all = sync_checklists(
+ options = ctrl_panel.get("cl-ctrl-framesize-options"),
+ sel=list(),
+ all=cl_framesize_all,
+ id="all"
+ )
+ ctrl_panel.set({
+ "cl-ctrl-framesize-value": val_sel,
+ "cl-ctrl-framesize-all-value": val_all,
+ })
+ elif trigger_id == "cl-ctrl-testtype":
+ val_sel, val_all = sync_checklists(
+ options = ctrl_panel.get("cl-ctrl-testtype-options"),
+ sel=cl_testtype,
+ all=list(),
+ id=""
+ )
+ ctrl_panel.set({
+ "cl-ctrl-testtype-value": val_sel,
+ "cl-ctrl-testtype-all-value": val_all,
+ })
+ elif trigger_id == "cl-ctrl-testtype-all":
+ val_sel, val_all = sync_checklists(
+ options = ctrl_panel.get("cl-ctrl-testtype-options"),
+ sel=list(),
+ all=cl_testtype_all,
+ id="all"
+ )
+ ctrl_panel.set({
+ "cl-ctrl-testtype-value": val_sel,
+ "cl-ctrl-testtype-all-value": val_all,
+ })
+ elif trigger_id == "btn-ctrl-add":
+ _ = btn_add
+ dut = ctrl_panel.get("dd-ctrl-dut-value")
+ phy = ctrl_panel.get("dd-ctrl-phy-value")
+ area = ctrl_panel.get("dd-ctrl-area-value")
+ test = ctrl_panel.get("dd-ctrl-test-value")
+ cores = ctrl_panel.get("cl-ctrl-core-value")
+ framesizes = ctrl_panel.get("cl-ctrl-framesize-value")
+ testtypes = ctrl_panel.get("cl-ctrl-testtype-value")
+ # Add selected test to the list of tests in store:
+ if all((dut, phy, area, test, cores, framesizes, testtypes)):
+ if store_sel is None:
+ store_sel = list()
+ for core in cores:
+ for framesize in framesizes:
+ for ttype in testtypes:
+ 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 [itm["id"] for itm 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"])
+ row_card_sel_tests = C.STYLE_ENABLED
+ row_btns_sel_tests = C.STYLE_ENABLED
+ if C.CLEAR_ALL_INPUTS:
+ ctrl_panel.set(ctrl_panel.defaults)
+ elif trigger_id == "btn-sel-remove-all":
+ _ = btn_remove_all
+ row_fig_tput = C.PLACEHOLDER
+ row_fig_lat = C.PLACEHOLDER
+ row_btn_dwnld = C.PLACEHOLDER
+ row_card_sel_tests = C.STYLE_DISABLED
+ row_btns_sel_tests = C.STYLE_DISABLED
+ store_sel = list()
+ ctrl_panel.set({"cl-selected-options": list()})
+ elif trigger_id == "btn-sel-remove":
+ _ = btn_remove
+ if list_sel:
+ new_store_sel = list()
+ for item in store_sel:
+ if item["id"] not in list_sel:
+ new_store_sel.append(item)
+ store_sel = new_store_sel
+ elif trigger_id == "url":
+ if url_params:
+ try:
+ store_sel = literal_eval(url_params["store_sel"][0])
+ norm = literal_eval(url_params["norm"][0])
+ except (KeyError, IndexError):
+ pass
+ if store_sel:
+ row_card_sel_tests = C.STYLE_ENABLED
+ row_btns_sel_tests = C.STYLE_ENABLED
+ last_test = store_sel[-1]
+ test = self.spec_tbs[last_test["dut"]]\
+ [last_test["phy"]][last_test["area"]]\
+ [last_test["test"]]
+ ctrl_panel.set({
+ "dd-ctrl-dut-value": last_test["dut"],
+ "dd-ctrl-phy-value": last_test["phy"],
+ "dd-ctrl-phy-options": generate_options(sorted(
+ self.spec_tbs[last_test["dut"]].keys())),
+ "dd-ctrl-phy-disabled": False,
+ "dd-ctrl-area-value": last_test["area"],
+ "dd-ctrl-area-options": [
+ {"label": label(v), "value": v} \
+ for v in sorted(
+ self.spec_tbs[last_test["dut"]]\
+ [last_test["phy"]].keys())
+ ],
+ "dd-ctrl-area-disabled": False,
+ "dd-ctrl-test-value": last_test["test"],
+ "dd-ctrl-test-options": generate_options(sorted(
+ self.spec_tbs[last_test["dut"]]\
+ [last_test["phy"]]\
+ [last_test["area"]].keys())),
+ "dd-ctrl-test-disabled": False,
+ "cl-ctrl-core-options": generate_options(sorted(
+ test["core"])),
+ "cl-ctrl-core-value": [last_test["core"].upper(), ],
+ "cl-ctrl-core-all-value": list(),
+ "cl-ctrl-core-all-options": C.CL_ALL_ENABLED,
+ "cl-ctrl-framesize-options": generate_options(
+ sorted(test["frame-size"])),
+ "cl-ctrl-framesize-value": \
+ [last_test["framesize"].upper(), ],
+ "cl-ctrl-framesize-all-value": list(),
+ "cl-ctrl-framesize-all-options": C.CL_ALL_ENABLED,
+ "cl-ctrl-testtype-options": generate_options(sorted(
+ test["test-type"])),
+ "cl-ctrl-testtype-value": \
+ [last_test["testtype"].upper(), ],
+ "cl-ctrl-testtype-all-value": list(),
+ "cl-ctrl-testtype-all-options": C.CL_ALL_ENABLED
+ })
+
+ if trigger_id in ("btn-ctrl-add", "url", "btn-sel-remove",
+ "cl-ctrl-normalize"):
+ if store_sel:
+ row_fig_tput, row_fig_lat, row_btn_dwnld = \
+ _generate_plotting_area(
+ graph_trending(self.data, store_sel, self.layout,
+ bool(norm)),
+ gen_new_url(
+ parsed_url,
+ {
+ "store_sel": store_sel,
+ "norm": norm
+ }
+ )
+ )
+ ctrl_panel.set({
+ "cl-selected-options": list_tests(store_sel)
+ })
+ else:
+ row_fig_tput = C.PLACEHOLDER
+ row_fig_lat = C.PLACEHOLDER
+ row_btn_dwnld = C.PLACEHOLDER
+ row_card_sel_tests = C.STYLE_DISABLED
+ row_btns_sel_tests = C.STYLE_DISABLED
+ store_sel = list()
+ ctrl_panel.set({"cl-selected-options": list()})
+
+ if ctrl_panel.get("cl-ctrl-core-value") and \
+ ctrl_panel.get("cl-ctrl-framesize-value") and \
+ ctrl_panel.get("cl-ctrl-testtype-value"):
+ disabled = False
+ else:
+ disabled = True
+ ctrl_panel.set({
+ "btn-ctrl-add-disabled": disabled,
+ "cl-normalize-value": norm
+ })
+
+ ret_val = [
+ ctrl_panel.panel, store_sel,
+ row_fig_tput, row_fig_lat, row_btn_dwnld,
+ row_card_sel_tests, row_btns_sel_tests
+ ]
+ ret_val.extend(ctrl_panel.values())
+ return ret_val
+
+ @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)
+ """
+ try:
+ trigger_id = loads(
+ callback_context.triggered[0]["prop_id"].split(".")[0]
+ )["index"]
+ idx = 0 if trigger_id == "tput" else 1
+ graph_data = graph_data[idx]["points"][0]
+ except (JSONDecodeError, IndexError, KeyError, ValueError,
+ TypeError):
+ raise PreventUpdate
+
+ metadata = no_update
+ graph = list()
+
+ children = [
+ dbc.ListGroupItem(
+ [dbc.Badge(x.split(":")[0]), x.split(": ")[1]]
+ ) for x in graph_data.get("text", "").split("<br>")
+ ]
+ if trigger_id == "tput":
+ title = "Throughput"
+ elif trigger_id == "lat":
+ title = "Latency"
+ hdrh_data = graph_data.get("customdata", None)
+ if hdrh_data:
+ graph = [dbc.Card(
+ class_name="gy-2 p-0",
+ children=[
+ dbc.CardHeader(hdrh_data.pop("name")),
+ dbc.CardBody(children=[
+ dcc.Graph(
+ id="hdrh-latency-graph",
+ figure=graph_hdrh_latency(
+ hdrh_data, self.layout
+ )
+ )
+ ])
+ ])
+ ]
+ metadata = [
+ dbc.Card(
+ class_name="gy-2 p-0",
+ children=[
+ dbc.CardHeader(children=[
+ dcc.Clipboard(
+ target_id="tput-lat-metadata",
+ title="Copy",
+ style={"display": "inline-block"}
+ ),
+ title
+ ]),
+ dbc.CardBody(
+ id="tput-lat-metadata",
+ class_name="p-0",
+ children=[dbc.ListGroup(children, flush=True), ]
+ )
+ ]
+ )
+ ]
+
+ return metadata, graph, True
+
+ @app.callback(
+ Output("download-data", "data"),
+ State("selected-tests", "data"),
+ Input("btn-download-data", "n_clicks"),
+ prevent_initial_call=True
+ )
+ def _download_data(store_sel, n_clicks):
+ """Download the data
+
+ :param store_sel: List of tests selected by user stored in the
+ browser.
+ :param n_clicks: Number of clicks on the button "Download".
+ :type store_sel: list
+ :type n_clicks: int
+ :returns: dict of data frame content (base64 encoded) and meta data
+ used by the Download component.
+ :rtype: dict
+ """
+
+ if not n_clicks:
+ raise PreventUpdate
+
+ if not store_sel:
+ raise PreventUpdate
+
+ df = pd.DataFrame()
+ for itm in store_sel:
+ sel_data = select_trending_data(self.data, itm)
+ if sel_data is None:
+ continue
+ df = pd.concat([df, sel_data], ignore_index=True)
+
+ return dcc.send_data_frame(df.to_csv, C.TREND_DOWNLOAD_FILE_NAME)
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..1beb5226b1
--- /dev/null
+++ b/csit.infra.dash/app/cdash/trending/layout.yaml
@@ -0,0 +1,129 @@
+plot-trending-tput:
+ autosize: True
+ showlegend: True
+ yaxis:
+ showticklabels: True
+ tickformat: ".3s"
+ title: "Throughput [pps]"
+ 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
+ legend:
+ orientation: "h"
+ y: -0.18
+ xanchor: "auto"
+ traceorder: "normal"
+ bordercolor: "rgb(238, 238, 238)"
+ paper_bgcolor: "#fff"
+ plot_bgcolor: "#fff"
+ hoverlabel:
+ namelength: -1
+
+plot-trending-lat:
+ autosize: True
+ showlegend: True
+ 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
+ legend:
+ orientation: "h"
+ y: -0.18
+ xanchor: "auto"
+ traceorder: "normal"
+ bordercolor: "rgb(238, 238, 238)"
+ 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: False
+ fixedrange: 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"
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..760eb3906e
--- /dev/null
+++ b/csit.infra.dash/app/cdash/trending/trending.py
@@ -0,0 +1,49 @@
+# Copyright (c) 2022 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
+
+from ..utils.constants import Constants as C
+from .layout import Layout
+
+
+def init_trending(server, time_period=None):
+ """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,
+ html_layout_file=C.HTML_LAYOUT_FILE,
+ graph_layout_file=C.TREND_GRAPH_LAYOUT_FILE,
+ data_spec_file=C.DATA_SPEC_FILE,
+ tooltip_file=C.TOOLTIP_FILE,
+ time_period=time_period
+ )
+ dash_app.index_string = layout.html_layout
+ dash_app.layout = layout.add_content()
+
+ return dash_app.server