diff options
Diffstat (limited to 'csit.infra.dash/app/cdash/report')
-rw-r--r-- | csit.infra.dash/app/cdash/report/__init__.py | 12 | ||||
-rw-r--r-- | csit.infra.dash/app/cdash/report/graphs.py | 311 | ||||
-rw-r--r-- | csit.infra.dash/app/cdash/report/layout.py | 1332 | ||||
-rw-r--r-- | csit.infra.dash/app/cdash/report/layout.yaml | 156 | ||||
-rw-r--r-- | csit.infra.dash/app/cdash/report/report.py | 52 |
5 files changed, 1863 insertions, 0 deletions
diff --git a/csit.infra.dash/app/cdash/report/__init__.py b/csit.infra.dash/app/cdash/report/__init__.py new file mode 100644 index 0000000000..c6a5f639fe --- /dev/null +++ b/csit.infra.dash/app/cdash/report/__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/report/graphs.py b/csit.infra.dash/app/cdash/report/graphs.py new file mode 100644 index 0000000000..02202ea155 --- /dev/null +++ b/csit.infra.dash/app/cdash/report/graphs.py @@ -0,0 +1,311 @@ +# 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 iterative data. +""" + +import plotly.graph_objects as go +import pandas as pd + +from copy import deepcopy +from numpy import percentile + +from ..utils.constants import Constants as C +from ..utils.utils import get_color, get_hdrh_latencies + + +def select_iterative_data(data: pd.DataFrame, itm:dict) -> pd.DataFrame: + """Select the data for graphs and tables from the provided data frame. + + :param data: Data frame with data for graphs and tables. + :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 + + 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["release"] == itm["rls"]) & + (data["test_type"] == test_type) & + (data["passed"] == True) + )] + + core = str() if itm["dut"] == "trex" else f"{itm['core']}" + ttype = "ndrpdr" if itm["testtype"] in ("ndr", "pdr") else itm["testtype"] + regex_test = \ + f"^.*[.|-]{nic}.*{itm['framesize']}-{core}-{drv}{itm['test']}-{ttype}$" + df = df[ + (df.job.str.endswith(f"{topo}-{arch}")) & + (df.dut_version.str.contains(itm["dutver"].replace(".r", "-r").\ + replace("rls", "release"))) & + (df.test_id.str.contains(regex_test, regex=True)) + ] + + return df + + +def graph_iterative(data: pd.DataFrame, sel: list, layout: dict, + normalize: bool=False, remove_outliers: bool=False) -> tuple: + """Generate the statistical box graph with iterative data (MRR, NDR and PDR, + for PDR also Latencies). + + :param data: Data frame with iterative data. + :param sel: Selected tests. + :param layout: Layout of plot.ly graph. + :param normalize: If True, the data is normalized to CPU frequency + Constants.NORM_FREQUENCY. + :param remove_outliers: If True the outliers are removed before + generating the table. + :type data: pandas.DataFrame + :type sel: list + :type layout: dict + :type normalize: bool + :type remove_outliers: bool + :returns: Tuple of graphs - throughput and latency. + :rtype: tuple(plotly.graph_objects.Figure, plotly.graph_objects.Figure) + """ + + def get_y_values(data, y_data_max, param, norm_factor, release=str(), + remove_outliers=False): + if param == "result_receive_rate_rate_values": + if release == "rls2402": + y_vals_raw = data["result_receive_rate_rate_avg"].to_list() + else: + y_vals_raw = data[param].to_list()[0] + else: + y_vals_raw = data[param].to_list() + y_data = [(y * norm_factor) for y in y_vals_raw] + + if remove_outliers: + try: + q1 = percentile(y_data, 25, method=C.COMP_PERCENTILE_METHOD) + q3 = percentile(y_data, 75, method=C.COMP_PERCENTILE_METHOD) + irq = q3 - q1 + lif = q1 - C.COMP_OUTLIER_TYPE * irq + uif = q3 + C.COMP_OUTLIER_TYPE * irq + y_data = [i for i in y_data if i >= lif and i <= uif] + except TypeError: + pass + try: + y_data_max = max(max(y_data), y_data_max) + except TypeError: + y_data_max = 0 + return y_data, y_data_max + + fig_tput = None + fig_band = None + fig_lat = None + + tput_traces = list() + y_tput_max = 0 + y_units = set() + + lat_traces = list() + y_lat_max = 0 + x_lat = list() + + band_traces = list() + y_band_max = 0 + y_band_units = set() + x_band = list() + + for idx, itm in enumerate(sel): + + itm_data = select_iterative_data(data, itm) + if itm_data.empty: + continue + + 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 normalize else 1.0 + + if itm["area"] == "hoststack": + ttype = f"hoststack-{itm['testtype']}" + else: + ttype = itm["testtype"] + + y_units.update(itm_data[C.UNIT[ttype]].unique().tolist()) + + y_data, y_tput_max = get_y_values( + itm_data, + y_tput_max, + C.VALUE_ITER[ttype], + norm_factor, + itm["rls"], + remove_outliers + ) + + nr_of_samples = len(y_data) + + customdata = list() + metadata = { + "csit release": itm["rls"], + "dut": itm["dut"], + "dut version": itm["dutver"], + "infra": itm["phy"], + "test": ( + f"{itm['area']}-{itm['framesize']}-{itm['core']}-" + f"{itm['test']}-{itm['testtype']}" + ) + } + + if itm["testtype"] == "mrr" and itm["rls"] in ("rls2306", "rls2310"): + trial_run = "trial" + metadata["csit-ref"] = ( + f"{itm_data['job'].to_list()[0]}/", + f"{itm_data['build'].to_list()[0]}" + ) + customdata = [{"metadata": metadata}, ] * nr_of_samples + else: + trial_run = "run" + for _, row in itm_data.iterrows(): + metadata["csit-ref"] = f"{row['job']}/{row['build']}" + try: + metadata["hosts"] = ", ".join(row["hosts"]) + except (KeyError, TypeError): + pass + customdata.append({"metadata": deepcopy(metadata)}) + tput_kwargs = dict( + y=y_data, + name=( + f"{idx + 1}. " + f"({nr_of_samples:02d} " + f"{trial_run}{'s' if nr_of_samples > 1 else ''}) " + f"{itm['id']}" + ), + hoverinfo=u"y+name", + boxpoints="all", + jitter=0.3, + marker=dict(color=get_color(idx)), + customdata=customdata + ) + tput_traces.append(go.Box(**tput_kwargs)) + + if ttype in C.TESTS_WITH_BANDWIDTH: + y_band, y_band_max = get_y_values( + itm_data, + y_band_max, + C.VALUE_ITER[f"{ttype}-bandwidth"], + norm_factor, + remove_outliers=remove_outliers + ) + if not all(pd.isna(y_band)): + y_band_units.update( + itm_data[C.UNIT[f"{ttype}-bandwidth"]].unique().\ + dropna().tolist() + ) + band_kwargs = dict( + y=y_band, + name=( + f"{idx + 1}. " + f"({nr_of_samples:02d} " + f"run{'s' if nr_of_samples > 1 else ''}) " + f"{itm['id']}" + ), + hoverinfo=u"y+name", + boxpoints="all", + jitter=0.3, + marker=dict(color=get_color(idx)), + customdata=customdata + ) + x_band.append(idx + 1) + band_traces.append(go.Box(**band_kwargs)) + + if ttype in C.TESTS_WITH_LATENCY: + y_lat, y_lat_max = get_y_values( + itm_data, + y_lat_max, + C.VALUE_ITER["latency"], + 1 / norm_factor, + remove_outliers=remove_outliers + ) + if not all(pd.isna(y_lat)): + customdata = list() + for _, row in itm_data.iterrows(): + hdrh = get_hdrh_latencies( + row, + f"{metadata['infra']}-{metadata['test']}" + ) + metadata["csit-ref"] = f"{row['job']}/{row['build']}" + customdata.append({ + "metadata": deepcopy(metadata), + "hdrh": hdrh + }) + nr_of_samples = len(y_lat) + lat_kwargs = dict( + y=y_lat, + name=( + f"{idx + 1}. " + f"({nr_of_samples:02d} " + f"run{u's' if nr_of_samples > 1 else u''}) " + f"{itm['id']}" + ), + hoverinfo="all", + boxpoints="all", + jitter=0.3, + marker=dict(color=get_color(idx)), + customdata=customdata + ) + x_lat.append(idx + 1) + lat_traces.append(go.Box(**lat_kwargs)) + + if tput_traces: + pl_tput = deepcopy(layout["plot-throughput"]) + pl_tput["xaxis"]["tickvals"] = [i for i in range(len(sel))] + pl_tput["xaxis"]["ticktext"] = [str(i + 1) for i in range(len(sel))] + pl_tput["yaxis"]["title"] = f"Throughput [{'|'.join(sorted(y_units))}]" + if y_tput_max: + pl_tput["yaxis"]["range"] = [0, int(y_tput_max) * 1.1] + fig_tput = go.Figure(data=tput_traces, layout=pl_tput) + + if band_traces: + pl_band = deepcopy(layout["plot-bandwidth"]) + pl_band["xaxis"]["tickvals"] = [i for i in range(len(x_band))] + pl_band["xaxis"]["ticktext"] = x_band + pl_band["yaxis"]["title"] = \ + f"Bandwidth [{'|'.join(sorted(y_band_units))}]" + if y_band_max: + pl_band["yaxis"]["range"] = [0, int(y_band_max) * 1.1] + fig_band = go.Figure(data=band_traces, layout=pl_band) + + if lat_traces: + pl_lat = deepcopy(layout["plot-latency"]) + pl_lat["xaxis"]["tickvals"] = [i for i in range(len(x_lat))] + pl_lat["xaxis"]["ticktext"] = x_lat + if y_lat_max: + pl_lat["yaxis"]["range"] = [0, int(y_lat_max) + 5] + fig_lat = go.Figure(data=lat_traces, layout=pl_lat) + + return fig_tput, fig_band, fig_lat diff --git a/csit.infra.dash/app/cdash/report/layout.py b/csit.infra.dash/app/cdash/report/layout.py new file mode 100644 index 0000000000..f485894d78 --- /dev/null +++ b/csit.infra.dash/app/cdash/report/layout.py @@ -0,0 +1,1332 @@ +# 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 ..utils.constants import Constants as C +from ..utils.control_panel import ControlPanel +from ..utils.trigger import Trigger +from ..utils.utils import show_tooltip, label, sync_checklists, gen_new_url, \ + generate_options, get_list_group_items, navbar_report, \ + show_iterative_graph_data +from ..utils.url_processing import url_decode +from .graphs import graph_iterative, select_iterative_data + + +# Control panel partameters and their default values. +CP_PARAMS = { + "dd-rls-val": str(), + "dd-dut-opt": list(), + "dd-dut-dis": True, + "dd-dut-val": str(), + "dd-dutver-opt": list(), + "dd-dutver-dis": True, + "dd-dutver-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_iterative: 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 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 html_layout_file: str + :type graph_layout_file: str + :type tooltip_file: str + """ + + # Inputs + self._app = app + self._html_layout_file = html_layout_file + self._graph_layout_file = graph_layout_file + self._tooltip_file = tooltip_file + self._data = data_iterative + + # Get structure of tests: + tbs = dict() + cols = [ + "job", "test_id", "test_type", "dut_version", "tg_type", "release" + ] + for _, row in self._data[cols].drop_duplicates().iterrows(): + rls = row["release"] + lst_job = row["job"].split("-") + dut = lst_job[1] + d_ver = row["dut_version"] + tbed = "-".join(lst_job[-2:]) + lst_test_id = row["test_id"].split(".") + if dut == "dpdk": + area = "dpdk" + else: + area = ".".join(lst_test_id[3:-2]) + suite = lst_test_id[-2].replace("2n1l-", "").replace("1n1l-", "").\ + replace("2n-", "") + test = lst_test_id[-1] + nic = suite.split("-")[0] + for drv in C.DRIVERS: + if drv in test: + driver = drv.replace("-", "_") + 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(rls, None) is None: + tbs[rls] = dict() + if tbs[rls].get(dut, None) is None: + tbs[rls][dut] = dict() + if tbs[rls][dut].get(d_ver, None) is None: + tbs[rls][dut][d_ver] = dict() + if tbs[rls][dut][d_ver].get(area, None) is None: + tbs[rls][dut][d_ver][area] = dict() + if tbs[rls][dut][d_ver][area].get(test, None) is None: + tbs[rls][dut][d_ver][area][test] = dict() + if tbs[rls][dut][d_ver][area][test].get(infra, None) is None: + tbs[rls][dut][d_ver][area][test][infra] = { + "core": list(), + "frame-size": list(), + "test-type": list() + } + tst_params = tbs[rls][dut][d_ver][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" and \ + row["tg_type"] in ("iperf", "vpp"): + if "BPS" not in tst_params["test-type"]: + tst_params["test-type"].append("BPS") + elif row["test_type"] == "hoststack" and 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=[ + dbc.Row( + id="row-navbar", + class_name="g-0", + children=[navbar_report((True, False, False, False)), ] + ), + dbc.Row( + id="row-main", + class_name="g-0", + children=[ + dcc.Store(id="store-selected-tests"), + dcc.Store(id="store-control-panel"), + dcc.Location(id="url", refresh=False), + self._add_ctrl_col(), + self._add_plotting_col() + ] + ), + dbc.Spinner( + 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") + ] + ), + 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_REL_NOTES, + width="100%", + height="100%" + ) + ) + ] + ) + else: + return html.Div( + id="div-main-error", + children=[ + dbc.Alert( + [ + "An Error Occured" + ], + color="danger" + ) + ] + ) + + 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. It is placed on the right side. + + :returns: Column with plots. + :rtype: dbc.Col + """ + return dbc.Col( + id="col-plotting-area", + children=[ + dbc.Spinner( + children=[ + dbc.Row( + id="plotting-area", + class_name="g-0 p-0", + children=[ + C.PLACEHOLDER + ] + ) + ] + ) + ], + width=9 + ) + + def _add_ctrl_panel(self) -> list: + """Add control panel. + + :returns: Control panel. + :rtype: list + """ + return [ + dbc.Row( + class_name="g-0 p-1", + children=[ + dbc.InputGroup( + [ + dbc.InputGroupText(show_tooltip( + self._tooltips, + "help-release", + "CSIT Release" + )), + dbc.Select( + id={"type": "ctrl-dd", "index": "rls"}, + placeholder="Select a Release...", + 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(show_tooltip( + self._tooltips, + "help-dut", + "DUT" + )), + dbc.Select( + id={"type": "ctrl-dd", "index": "dut"}, + placeholder="Select a Device under Test..." + ) + ], + size="sm" + ) + ] + ), + dbc.Row( + class_name="g-0 p-1", + children=[ + dbc.InputGroup( + [ + dbc.InputGroupText(show_tooltip( + self._tooltips, + "help-dut-ver", + "DUT Version" + )), + dbc.Select( + id={"type": "ctrl-dd", "index": "dutver"}, + placeholder=\ + "Select a Version of Device under Test..." + ) + ], + size="sm" + ) + ] + ), + dbc.Row( + class_name="g-0 p-1", + children=[ + 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" + ) + ] + ), + dbc.Row( + class_name="g-0 p-1", + children=[ + 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" + ) + ] + ), + dbc.Row( + class_name="g-0 p-1", + children=[ + 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" + ) + ] + ), + dbc.Row( + class_name="g-0 p-1", + children=[ + dbc.InputGroup( + [ + dbc.InputGroupText( + children=show_tooltip( + self._tooltips, + "help-framesize", + "Frame Size" + ) + ), + dbc.Col( + children=[ + dbc.Checklist( + id={ + "type": "ctrl-cl", + "index": "frmsize-all" + }, + options=C.CL_ALL_DISABLED, + inline=True, + class_name="ms-2" + ) + ], + width=2 + ), + dbc.Col( + children=[ + dbc.Checklist( + id={ + "type": "ctrl-cl", + "index": "frmsize" + }, + inline=True + ) + ] + ) + ], + style={"align-items": "center"}, + size="sm" + ) + ] + ), + dbc.Row( + class_name="g-0 p-1", + children=[ + dbc.InputGroup( + [ + dbc.InputGroupText( + children=show_tooltip( + self._tooltips, + "help-cores", + "Number of Cores" + ) + ), + dbc.Col( + children=[ + dbc.Checklist( + id={ + "type": "ctrl-cl", + "index": "core-all" + }, + options=C.CL_ALL_DISABLED, + inline=True, + class_name="ms-2" + ) + ], + width=2 + ), + dbc.Col( + children=[ + dbc.Checklist( + id={ + "type": "ctrl-cl", + "index": "core" + }, + inline=True + ) + ] + ) + ], + style={"align-items": "center"}, + size="sm" + ) + ] + ), + dbc.Row( + class_name="g-0 p-1", + children=[ + dbc.InputGroup( + [ + dbc.InputGroupText( + children=show_tooltip( + self._tooltips, + "help-ttype", + "Test Type" + ) + ), + dbc.Col( + children=[ + dbc.Checklist( + id={ + "type": "ctrl-cl", + "index": "tsttype-all" + }, + options=C.CL_ALL_DISABLED, + inline=True, + class_name="ms-2" + ) + ], + width=2 + ), + dbc.Col( + children=[ + dbc.Checklist( + id={ + "type": "ctrl-cl", + "index": "tsttype" + }, + inline=True + ) + ] + ) + ], + style={"align-items": "center"}, + size="sm" + ) + ] + ), + dbc.Row( + class_name="g-0 p-1", + children=[ + dbc.InputGroup( + [ + dbc.InputGroupText( + children=show_tooltip( + self._tooltips, + "help-normalize", + "Normalization" + ) + ), + dbc.Col( + children=[ + 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" + ) + ] + ), + dbc.Row( + class_name="g-0 p-1", + children=[ + dbc.Button( + id={"type": "ctrl-btn", "index": "add-test"}, + children="Add Selected", + color="info" + ) + ] + ), + dbc.Row( + id="row-card-sel-tests", + class_name="g-0 p-1", + style=C.STYLE_DISABLED, + children=[ + dbc.ListGroup( + class_name="overflow-auto p-0", + id="lg-selected", + children=[], + style={"max-height": "20em"}, + flush=True + ) + ] + ), + dbc.Stack( + id="row-btns-sel-tests", + class_name="g-0 p-1", + style=C.STYLE_DISABLED, + gap=2, + children=[ + dbc.ButtonGroup(children=[ + dbc.Button( + id={"type": "ctrl-btn", "index": "rm-test"}, + children="Remove Selected", + class_name="w-100", + color="info", + disabled=False + ), + dbc.Button( + id={"type": "ctrl-btn", "index": "rm-test-all"}, + children="Remove All", + class_name="w-100", + color="info", + disabled=False + ) + ]), + dbc.ButtonGroup(children=[ + dbc.Button( + id="plot-btn-url", + children="Show URL", + class_name="w-100", + color="info", + disabled=False + ), + dbc.Button( + id="plot-btn-download", + children="Download Data", + class_name="w-100", + color="info", + disabled=False + ) + ]) + ] + ) + ] + + def _get_plotting_area( + self, + tests: list, + normalize: bool, + url: str + ) -> list: + """Generate the plotting area with all its content. + + :param tests: List of tests to be displayed in the graphs. + :param normalize: If true, the values in graphs are normalized. + :param url: URL to be displayed in the modal window. + :type tests: list + :type normalize: bool + :type url: str + :returns: List of rows with elements to be displayed in the plotting + area. + :rtype: list + """ + if not tests: + return C.PLACEHOLDER + + graphs = \ + graph_iterative(self._data, tests, self._graph_layout, normalize) + + 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" + ) + ) + + return [ + dbc.Row( + dbc.Tabs( + children=tab_items, + id="tabs", + active_tab="tab-tput", + ), + class_name="g-0 p-0" + ), + dbc.Modal( + [ + dbc.ModalHeader(dbc.ModalTitle("URL")), + dbc.ModalBody(url) + ], + id="plot-mod-url", + size="xl", + is_open=False, + scrollable=True + ), + dcc.Download(id="download-iterative-data") + ] + + def callbacks(self, app): + """Callbacks for the whole application. + + :param app: The application. + :type app: Flask + """ + + @app.callback( + [ + Output("store-control-panel", "data"), + Output("store-selected-tests", "data"), + Output("plotting-area", "children"), + Output("row-card-sel-tests", "style"), + Output("row-btns-sel-tests", "style"), + Output("lg-selected", "children"), + + Output({"type": "ctrl-dd", "index": "rls"}, "value"), + Output({"type": "ctrl-dd", "index": "dut"}, "options"), + Output({"type": "ctrl-dd", "index": "dut"}, "disabled"), + Output({"type": "ctrl-dd", "index": "dut"}, "value"), + Output({"type": "ctrl-dd", "index": "dutver"}, "options"), + Output({"type": "ctrl-dd", "index": "dutver"}, "disabled"), + Output({"type": "ctrl-dd", "index": "dutver"}, "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-control-panel", "data"), + State("store-selected-tests", "data"), + State({"type": "sel-cl", "index": ALL}, "value") + ], + [ + Input("url", "href"), + Input("normalize", "value"), + + Input({"type": "ctrl-dd", "index": ALL}, "value"), + Input({"type": "ctrl-cl", "index": ALL}, "value"), + Input({"type": "ctrl-btn", "index": ALL}, "n_clicks") + ] + ) + def _update_application( + control_panel: dict, + store_sel: list, + lst_sel: list, + href: str, + normalize: list, + *_ + ) -> tuple: + """Update the application when the event is detected. + """ + + ctrl_panel = ControlPanel(CP_PARAMS, control_panel) + on_draw = False + + # Parse the url: + parsed_url = url_decode(href) + if parsed_url: + url_params = parsed_url["params"] + else: + url_params = None + + plotting_area = no_update + row_card_sel_tests = no_update + row_btns_sel_tests = no_update + lg_selected = no_update + + trigger = Trigger(callback_context.triggered) + + if trigger.type == "url" and url_params: + try: + store_sel = literal_eval(url_params["store_sel"][0]) + normalize = literal_eval(url_params["norm"][0]) + except (KeyError, IndexError, AttributeError): + 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["rls"]][last_test["dut"]]\ + [last_test["dutver"]][last_test["area"]]\ + [last_test["test"]][last_test["phy"]] + ctrl_panel.set({ + "dd-rls-val": last_test["rls"], + "dd-dut-val": last_test["dut"], + "dd-dut-opt": generate_options( + self._spec_tbs[last_test["rls"]].keys() + ), + "dd-dut-dis": False, + "dd-dutver-val": last_test["dutver"], + "dd-dutver-opt": generate_options( + self._spec_tbs[last_test["rls"]]\ + [last_test["dut"]].keys() + ), + "dd-dutver-dis": False, + "dd-area-val": last_test["area"], + "dd-area-opt": [ + {"label": label(v), "value": v} for v in \ + sorted(self._spec_tbs[last_test["rls"]]\ + [last_test["dut"]]\ + [last_test["dutver"]].keys()) + ], + "dd-area-dis": False, + "dd-test-val": last_test["test"], + "dd-test-opt": generate_options( + self._spec_tbs[last_test["rls"]][last_test["dut"]]\ + [last_test["dutver"]][last_test["area"]].keys() + ), + "dd-test-dis": False, + "dd-phy-val": last_test["phy"], + "dd-phy-opt": generate_options( + self._spec_tbs[last_test["rls"]][last_test["dut"]]\ + [last_test["dutver"]][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 + }) + on_draw = True + elif trigger.type == "normalize": + ctrl_panel.set({"cl-normalize-val": normalize}) + on_draw = True + elif trigger.type == "ctrl-dd": + if trigger.idx == "rls": + try: + options = generate_options( + self._spec_tbs[trigger.value].keys() + ) + disabled = False + except KeyError: + options = list() + disabled = True + ctrl_panel.set({ + "dd-rls-val": trigger.value, + "dd-dut-val": str(), + "dd-dut-opt": options, + "dd-dut-dis": disabled, + "dd-dutver-val": str(), + "dd-dutver-opt": list(), + "dd-dutver-dis": True, + "dd-phy-val": str(), + "dd-phy-opt": list(), + "dd-phy-dis": True, + "dd-area-val": str(), + "dd-area-opt": list(), + "dd-area-dis": True, + "dd-test-val": str(), + "dd-test-opt": list(), + "dd-test-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 + }) + elif trigger.idx == "dut": + try: + rls = ctrl_panel.get("dd-rls-val") + dut = self._spec_tbs[rls][trigger.value] + options = generate_options(dut.keys()) + disabled = False + except KeyError: + options = list() + disabled = True + ctrl_panel.set({ + "dd-dut-val": trigger.value, + "dd-dutver-val": str(), + "dd-dutver-opt": options, + "dd-dutver-dis": disabled, + "dd-phy-val": str(), + "dd-phy-opt": list(), + "dd-phy-dis": True, + "dd-area-val": str(), + "dd-area-opt": list(), + "dd-area-dis": True, + "dd-test-val": str(), + "dd-test-opt": list(), + "dd-test-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 + }) + elif trigger.idx == "dutver": + try: + rls = ctrl_panel.get("dd-rls-val") + dut = ctrl_panel.get("dd-dut-val") + dutver = self._spec_tbs[rls][dut][trigger.value] + options = [{"label": label(v), "value": v} \ + for v in sorted(dutver.keys())] + disabled = False + except KeyError: + options = list() + disabled = True + ctrl_panel.set({ + "dd-dutver-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 + }) + elif trigger.idx == "area": + try: + rls = ctrl_panel.get("dd-rls-val") + dut = ctrl_panel.get("dd-dut-val") + dutver = ctrl_panel.get("dd-dutver-val") + area = self._spec_tbs[rls][dut][dutver][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 + }) + elif trigger.idx == "test": + try: + rls = ctrl_panel.get("dd-rls-val") + dut = ctrl_panel.get("dd-dut-val") + dutver = ctrl_panel.get("dd-dutver-val") + area = ctrl_panel.get("dd-area-val") + test = self._spec_tbs[rls][dut][dutver][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 + }) + elif trigger.idx == "phy": + rls = ctrl_panel.get("dd-rls-val") + dut = ctrl_panel.get("dd-dut-val") + dutver = ctrl_panel.get("dd-dutver-val") + area = ctrl_panel.get("dd-area-val") + test = ctrl_panel.get("dd-test-val") + if all((rls, dut, dutver, area, test, trigger.value, )): + phy = self._spec_tbs[rls][dut][dutver][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": + on_draw = True + if trigger.idx == "add-test": + rls = ctrl_panel.get("dd-rls-val") + dut = ctrl_panel.get("dd-dut-val") + dutver = ctrl_panel.get("dd-dutver-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 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(( + rls, + dut, + dutver, + 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, + "rls": rls, + "dut": dut, + "dutver": dutver, + "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() + + if on_draw: + if store_sel: + lg_selected = get_list_group_items( + store_sel, "sel-cl", add_index=True + ) + plotting_area = self._get_plotting_area( + store_sel, + bool(normalize), + gen_new_url( + parsed_url, + {"store_sel": store_sel, "norm": normalize} + ) + ) + row_card_sel_tests = C.STYLE_ENABLED + row_btns_sel_tests = C.STYLE_ENABLED + else: + plotting_area = C.PLACEHOLDER + row_card_sel_tests = C.STYLE_DISABLED + row_btns_sel_tests = C.STYLE_DISABLED + store_sel = list() + + ret_val = [ + ctrl_panel.panel, + store_sel, + plotting_area, + row_card_sel_tests, + row_btns_sel_tests, + lg_selected + ] + ret_val.extend(ctrl_panel.values) + return ret_val + + @app.callback( + Output("plot-mod-url", "is_open"), + Output("plot-btn-url", "n_clicks"), + Input("plot-btn-url", "n_clicks"), + State("plot-mod-url", "is_open") + ) + def toggle_plot_mod_url(n, is_open): + """Toggle the modal window with url. + """ + if n: + return not is_open, 0 + return is_open, 0 + + @app.callback( + Output("download-iterative-data", "data"), + State("store-selected-tests", "data"), + Input("plot-btn-download", "n_clicks"), + prevent_initial_call=True + ) + def _download_iterative_data(store_sel, _): + """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_sel: + raise PreventUpdate + + df = pd.DataFrame() + for itm in store_sel: + sel_data = select_iterative_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.REPORT_DOWNLOAD_FILE_NAME) + + @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_iterative_graph_data( + trigger, graph_data, self._graph_layout) + + @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/report/layout.yaml b/csit.infra.dash/app/cdash/report/layout.yaml new file mode 100644 index 0000000000..dea50144c2 --- /dev/null +++ b/csit.infra.dash/app/cdash/report/layout.yaml @@ -0,0 +1,156 @@ +plot-throughput: + xaxis: + title: "Test Cases [Index]" + autorange: True + fixedrange: False + gridcolor: "rgb(230, 230, 230)" + linecolor: "rgb(220, 220, 220)" + linewidth: 1 + showgrid: True + showline: True + showticklabels: True + tickcolor: "rgb(220, 220, 220)" + tickmode: "array" + zeroline: True + yaxis: + title: "Throughput [pps|cps|rps|bps]" + gridcolor: "rgb(230, 230, 230)" + hoverformat: ".3s" + tickformat: ".3s" + linecolor: "rgb(220, 220, 220)" + linewidth: 1 + showgrid: True + showline: True + showticklabels: True + tickcolor: "rgb(220, 220, 220)" + zeroline: True + range: [0, 100] + autosize: True + margin: + t: 50 + b: 0 + l: 80 + r: 20 + showlegend: False + height: 850 + paper_bgcolor: "#fff" + plot_bgcolor: "#fff" + hoverlabel: + namelength: -1 + +plot-bandwidth: + xaxis: + title: "Test Cases [Index]" + autorange: True + fixedrange: False + gridcolor: "rgb(230, 230, 230)" + linecolor: "rgb(220, 220, 220)" + linewidth: 1 + showgrid: True + showline: True + showticklabels: True + tickcolor: "rgb(220, 220, 220)" + tickmode: "array" + zeroline: True + yaxis: + title: "Bandwidth [bps]" + gridcolor: "rgb(230, 230, 230)" + hoverformat: ".3s" + tickformat: ".3s" + linecolor: "rgb(220, 220, 220)" + linewidth: 1 + showgrid: True + showline: True + showticklabels: True + tickcolor: "rgb(220, 220, 220)" + zeroline: True + range: [0, 200] + autosize: True + margin: + t: 50 + b: 0 + l: 80 + r: 20 + showlegend: False + height: 850 + paper_bgcolor: "#fff" + plot_bgcolor: "#fff" + hoverlabel: + namelength: -1 + +plot-latency: + xaxis: + title: "Test Cases [Index]" + autorange: True + fixedrange: False + gridcolor: "rgb(230, 230, 230)" + linecolor: "rgb(220, 220, 220)" + linewidth: 1 + showgrid: True + showline: True + showticklabels: True + tickcolor: "rgb(220, 220, 220)" + tickmode: "array" + zeroline: True + yaxis: + title: "Average Latency at 50% PDR [us]" + gridcolor: "rgb(230, 230, 230)" + hoverformat: ".3s" + tickformat: ".3s" + linecolor: "rgb(220, 220, 220)" + linewidth: 1 + showgrid: True + showline: True + showticklabels: True + tickcolor: "rgb(220, 220, 220)" + zeroline: True + range: [0, 200] + autosize: True + margin: + t: 50 + b: 0 + l: 80 + r: 20 + showlegend: False + height: 850 + 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" diff --git a/csit.infra.dash/app/cdash/report/report.py b/csit.infra.dash/app/cdash/report/report.py new file mode 100644 index 0000000000..ce5e977f4c --- /dev/null +++ b/csit.infra.dash/app/cdash/report/report.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 Report Dash application. +""" +import dash +import pandas as pd + +from ..utils.constants import Constants as C +from .layout import Layout + + +def init_report( + server, + data_iterative: 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.REPORT_ROUTES_PATHNAME_PREFIX, + external_stylesheets=C.EXTERNAL_STYLESHEETS, + title=C.REPORT_TITLE + ) + + layout = Layout( + app=dash_app, + data_iterative=data_iterative, + html_layout_file=C.HTML_LAYOUT_FILE, + graph_layout_file=C.REPORT_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 |