aboutsummaryrefslogtreecommitdiffstats
path: root/csit.infra.dash/app/cdash
diff options
context:
space:
mode:
authorTibor Frank <tifrank@cisco.com>2024-03-20 05:43:03 +0000
committerTibor Frank <tifrank@cisco.com>2024-03-22 08:38:36 +0000
commit230044632667a3eb7794218a6ba3e2fa2c9b71b4 (patch)
tree7e132acf27b291c4898b38c4e6490e1cc682c973 /csit.infra.dash/app/cdash
parent18cf48d954d1e814430211e69b04718ae9c7d03c (diff)
C-Dash: Add detailed views to comparison tables
Change-Id: I0936f736497299f8b9fc1254012b2a0b20c41bfb Signed-off-by: Tibor Frank <tifrank@cisco.com>
Diffstat (limited to 'csit.infra.dash/app/cdash')
-rw-r--r--csit.infra.dash/app/cdash/comparisons/comparisons.py1
-rw-r--r--csit.infra.dash/app/cdash/comparisons/layout.py202
-rw-r--r--csit.infra.dash/app/cdash/comparisons/tables.py15
-rw-r--r--csit.infra.dash/app/cdash/report/graphs.py42
-rw-r--r--csit.infra.dash/app/cdash/search/layout.py29
-rw-r--r--csit.infra.dash/app/cdash/utils/utils.py33
6 files changed, 286 insertions, 36 deletions
diff --git a/csit.infra.dash/app/cdash/comparisons/comparisons.py b/csit.infra.dash/app/cdash/comparisons/comparisons.py
index 5700552af3..f2cda8138b 100644
--- a/csit.infra.dash/app/cdash/comparisons/comparisons.py
+++ b/csit.infra.dash/app/cdash/comparisons/comparisons.py
@@ -44,6 +44,7 @@ def init_comparisons(
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
diff --git a/csit.infra.dash/app/cdash/comparisons/layout.py b/csit.infra.dash/app/cdash/comparisons/layout.py
index d32542617c..3aa32399cf 100644
--- a/csit.infra.dash/app/cdash/comparisons/layout.py
+++ b/csit.infra.dash/app/cdash/comparisons/layout.py
@@ -26,14 +26,16 @@ from dash.exceptions import PreventUpdate
from dash.dash_table.Format import Format, Scheme
from ast import literal_eval
from yaml import load, FullLoader, YAMLError
+from copy import deepcopy
from ..utils.constants import Constants as C
from ..utils.control_panel import ControlPanel
from ..utils.trigger import Trigger
from ..utils.url_processing import url_decode
from ..utils.utils import generate_options, gen_new_url, navbar_report, \
- filter_table_data, show_tooltip
+ filter_table_data, sort_table_data, show_iterative_graph_data, show_tooltip
from .tables import comparison_table
+from ..report.graphs import graph_iterative
# Control panel partameters and their default values.
@@ -80,12 +82,15 @@ class Layout:
app: Flask,
data_iterative: pd.DataFrame,
html_layout_file: str,
+ graph_layout_file: str,
tooltip_file: str
) -> None:
"""Initialization:
- save the input parameters,
- prepare data for the control panel,
- read HTML layout file,
+ - read graph layout file,
+ - read tooltips from the tooltip file.
:param app: Flask application running the dash application.
:param data_iterative: Iterative data to be used in comparison tables.
@@ -93,9 +98,12 @@ class Layout:
layout of the dash application.
:param tooltip_file: Path and name of the yaml file specifying the
tooltips.
+ :param graph_layout_file: Path and name of the file with layout of
+ plot.ly graphs.
:type app: Flask
:type data_iterative: pandas.DataFrame
:type html_layout_file: str
+ :type graph_layout_file: str
:type tooltip_file: str
"""
@@ -103,6 +111,7 @@ class Layout:
self._app = app
self._data = data_iterative
self._html_layout_file = html_layout_file
+ self._graph_layout_file = graph_layout_file
self._tooltip_file = tooltip_file
# Get structure of tests:
@@ -175,6 +184,20 @@ class Layout:
)
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:
@@ -232,6 +255,31 @@ class Layout:
self._add_plotting_col()
]
),
+ dbc.Spinner(
+ dbc.Offcanvas(
+ class_name="w-75",
+ id="offcanvas-details",
+ title="Test Details",
+ placement="end",
+ is_open=False,
+ children=[]
+ ),
+ delay_show=C.SPINNER_DELAY
+ ),
+ dbc.Spinner(
+ dbc.Offcanvas(
+ class_name="w-50",
+ id="offcanvas-metadata",
+ title="Detailed Information",
+ placement="end",
+ is_open=False,
+ children=[
+ dbc.Row(id="metadata-tput-lat"),
+ dbc.Row(id="metadata-hdrh-graph")
+ ]
+ ),
+ delay_show=C.SPINNER_DELAY
+ ),
dbc.Offcanvas(
class_name="w-75",
id="offcanvas-documentation",
@@ -625,7 +673,7 @@ class Layout:
editable=False,
filter_action="custom",
filter_query="",
- sort_action="native",
+ sort_action="custom",
sort_mode="multi",
selected_columns=[],
selected_rows=[],
@@ -749,6 +797,7 @@ class Layout:
Input("normalize", "value"),
Input("outliers", "value"),
Input({"type": "table", "index": ALL}, "filter_query"),
+ Input({"type": "table", "index": ALL}, "sort_by"),
Input({"type": "ctrl-dd", "index": ALL}, "value"),
Input({"type": "ctrl-cl", "index": ALL}, "value"),
Input({"type": "ctrl-btn", "index": ALL}, "n_clicks")
@@ -763,7 +812,6 @@ class Layout:
href: str,
normalize: list,
outliers: bool,
- table_filter: str,
*_
) -> tuple:
"""Update the application when the event is detected.
@@ -1020,10 +1068,16 @@ class Layout:
"cmp-val-val": str()
})
elif trigger.type == "table" and trigger.idx == "comparison":
- filtered_data = filter_table_data(
- store_table_data,
- table_filter[0]
- )
+ if trigger.parameter == "filter_query":
+ filtered_data = filter_table_data(
+ store_table_data,
+ trigger.value
+ )
+ elif trigger.parameter == "sort_by":
+ filtered_data = sort_table_data(
+ store_table_data,
+ trigger.value
+ )
table_data = [filtered_data, ]
if all((on_draw, selected["reference"]["set"],
@@ -1149,3 +1203,137 @@ class Layout:
if n_clicks:
return not is_open
return is_open
+
+ @app.callback(
+ Output("offcanvas-details", "is_open"),
+ Output("offcanvas-details", "children"),
+ State("store-selected", "data"),
+ State("store-filtered-table-data", "data"),
+ State("normalize", "value"),
+ State("outliers", "value"),
+ Input({"type": "table", "index": ALL}, "active_cell"),
+ prevent_initial_call=True
+ )
+ def show_test_data(cp_sel, table, normalize, outliers, *_):
+ """Show offcanvas with graphs and tables based on selected test(s).
+ """
+
+ trigger = Trigger(callback_context.triggered)
+ if not all((trigger.value, cp_sel["reference"]["set"], \
+ cp_sel["compare"]["set"])):
+ raise PreventUpdate
+
+ try:
+ test_name = pd.DataFrame.from_records(table).\
+ iloc[[trigger.value["row"]]]["Test Name"].iloc[0]
+ dut = cp_sel["reference"]["selection"]["dut"]
+ rls, dutver = cp_sel["reference"]["selection"]["dutver"].\
+ split("-", 1)
+ phy = cp_sel["reference"]["selection"]["infra"]
+ framesize, core, test_id = test_name.split("-", 2)
+ test, ttype = test_id.rsplit("-", 1)
+ ttype = "pdr" if ttype == "latency" else ttype
+ l_phy = phy.split("-")
+ tb = "-".join(l_phy[:2])
+ nic = l_phy[2]
+ stype = "ndrpdr" if ttype in ("ndr", "pdr") else ttype
+ except(KeyError, IndexError, AttributeError, ValueError):
+ raise PreventUpdate
+
+ df = pd.DataFrame(self._data.loc[(
+ (self._data["dut_type"] == dut) &
+ (self._data["dut_version"] == dutver) &
+ (self._data["release"] == rls)
+ )])
+ df = df[df.job.str.endswith(tb)]
+ df = df[df.test_id.str.contains(
+ f"{nic}.*{test}-{stype}", regex=True
+ )]
+ if df.empty:
+ raise PreventUpdate
+
+ l_test_id = df["test_id"].iloc[0].split(".")
+ area = ".".join(l_test_id[3:-2])
+
+ r_sel = {
+ "id": f"{test}-{ttype}",
+ "rls": rls,
+ "dut": dut,
+ "dutver": dutver,
+ "phy": phy,
+ "area": area,
+ "test": test,
+ "framesize": framesize,
+ "core": core,
+ "testtype": ttype
+ }
+
+ c_sel = deepcopy(r_sel)
+ param = cp_sel["compare"]["parameter"]
+ val = cp_sel["compare"]["value"].lower()
+ if param == "dutver":
+ c_sel["rls"], c_sel["dutver"] = val.split("-", 1)
+ elif param == "ttype":
+ c_sel["id"] = f"{test}-{val}"
+ c_sel["testtype"] = val
+ elif param == "infra":
+ c_sel["phy"] = val
+ else:
+ c_sel[param] = val
+
+ r_sel["id"] = "-".join(
+ (r_sel["phy"], r_sel["framesize"], r_sel["core"], r_sel["id"])
+ )
+ c_sel["id"] = "-".join(
+ (c_sel["phy"], c_sel["framesize"], c_sel["core"], c_sel["id"])
+ )
+ selected = [r_sel, c_sel]
+
+ indexes = ("tput", "bandwidth", "lat")
+ graphs = graph_iterative(
+ self._data,
+ selected,
+ self._graph_layout,
+ bool(normalize),
+ bool(outliers)
+ )
+ cols = list()
+ for graph, idx in zip(graphs, indexes):
+ if graph:
+ cols.append(dbc.Col(dcc.Graph(
+ figure=graph,
+ id={"type": "graph-iter", "index": idx},
+ )))
+ if not cols:
+ cols="No data."
+ ret_val = [
+ dbc.Row(
+ class_name="g-0 p-0",
+ children=dbc.Alert(test, color="info"),
+ ),
+ dbc.Row(class_name="g-0 p-0", children=cols)
+ ]
+
+ return True, ret_val
+
+ @app.callback(
+ Output("metadata-tput-lat", "children"),
+ Output("metadata-hdrh-graph", "children"),
+ Output("offcanvas-metadata", "is_open"),
+ Input({"type": "graph-iter", "index": ALL}, "clickData"),
+ prevent_initial_call=True
+ )
+ def _show_metadata_from_graph(iter_data: dict) -> tuple:
+ """Generates the data for the offcanvas displayed when a particular
+ point in a graph is clicked on.
+ """
+
+ trigger = Trigger(callback_context.triggered)
+ if not trigger.value:
+ raise PreventUpdate
+
+ if trigger.type == "graph-iter":
+ return show_iterative_graph_data(
+ trigger, iter_data, self._graph_layout)
+ else:
+ raise PreventUpdate
diff --git a/csit.infra.dash/app/cdash/comparisons/tables.py b/csit.infra.dash/app/cdash/comparisons/tables.py
index 18f9404f0a..0e32f38b6c 100644
--- a/csit.infra.dash/app/cdash/comparisons/tables.py
+++ b/csit.infra.dash/app/cdash/comparisons/tables.py
@@ -95,15 +95,14 @@ def select_comp_data(
tmp_df.extend(l_itm)
l_df = tmp_df
- if remove_outliers:
- q1 = percentile(l_df, 25, method=C.COMP_PERCENTILE_METHOD)
- q3 = percentile(l_df, 75, method=C.COMP_PERCENTILE_METHOD)
- irq = q3 - q1
- lif = q1 - C.COMP_OUTLIER_TYPE * irq
- uif = q3 + C.COMP_OUTLIER_TYPE * irq
- l_df = [i for i in l_df if i >= lif and i <= uif]
-
try:
+ if remove_outliers:
+ q1 = percentile(l_df, 25, method=C.COMP_PERCENTILE_METHOD)
+ q3 = percentile(l_df, 75, method=C.COMP_PERCENTILE_METHOD)
+ irq = q3 - q1
+ lif = q1 - C.COMP_OUTLIER_TYPE * irq
+ uif = q3 + C.COMP_OUTLIER_TYPE * irq
+ l_df = [i for i in l_df if i >= lif and i <= uif]
mean_val = mean(l_df)
std_val = std(l_df)
except (TypeError, ValueError):
diff --git a/csit.infra.dash/app/cdash/report/graphs.py b/csit.infra.dash/app/cdash/report/graphs.py
index 44c57d4183..0627411d0f 100644
--- a/csit.infra.dash/app/cdash/report/graphs.py
+++ b/csit.infra.dash/app/cdash/report/graphs.py
@@ -14,11 +14,11 @@
"""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
@@ -74,7 +74,7 @@ def select_iterative_data(data: pd.DataFrame, itm:dict) -> pd.DataFrame:
def graph_iterative(data: pd.DataFrame, sel: list, layout: dict,
- normalize: bool=False) -> tuple:
+ normalize: bool=False, remove_outliers: bool=False) -> tuple:
"""Generate the statistical box graph with iterative data (MRR, NDR and PDR,
for PDR also Latencies).
@@ -83,15 +83,19 @@ def graph_iterative(data: pd.DataFrame, sel: list, layout: dict,
:param layout: Layout of plot.ly graph.
:param normalize: If True, the data is normalized to CPU frequency
Constants.NORM_FREQUENCY.
- :param data: pandas.DataFrame
- :param sel: list
- :param layout: dict
- :param normalize: bool
+ :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()):
+ 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()
@@ -100,6 +104,17 @@ def graph_iterative(data: pd.DataFrame, sel: list, layout: dict,
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:
@@ -142,7 +157,12 @@ def graph_iterative(data: pd.DataFrame, sel: list, layout: dict,
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"]
+ itm_data,
+ y_tput_max,
+ C.VALUE_ITER[ttype],
+ norm_factor,
+ itm["rls"],
+ remove_outliers
)
nr_of_samples = len(y_data)
@@ -192,7 +212,8 @@ def graph_iterative(data: pd.DataFrame, sel: list, layout: dict,
itm_data,
y_band_max,
C.VALUE_ITER[f"{ttype}-bandwidth"],
- norm_factor
+ norm_factor,
+ remove_outliers=remove_outliers
)
if not all(pd.isna(y_band)):
y_band_units.update(
@@ -221,7 +242,8 @@ def graph_iterative(data: pd.DataFrame, sel: list, layout: dict,
itm_data,
y_lat_max,
C.VALUE_ITER["latency"],
- 1 / norm_factor
+ 1 / norm_factor,
+ remove_outliers=remove_outliers
)
if not all(pd.isna(y_lat)):
customdata = list()
diff --git a/csit.infra.dash/app/cdash/search/layout.py b/csit.infra.dash/app/cdash/search/layout.py
index c8035055e4..aa4dd53d5b 100644
--- a/csit.infra.dash/app/cdash/search/layout.py
+++ b/csit.infra.dash/app/cdash/search/layout.py
@@ -32,8 +32,8 @@ from ..utils.constants import Constants as C
from ..utils.control_panel import ControlPanel
from ..utils.trigger import Trigger
from ..utils.utils import gen_new_url, generate_options, navbar_trending, \
- filter_table_data, show_trending_graph_data, show_iterative_graph_data, \
- show_tooltip
+ filter_table_data, sort_table_data, show_trending_graph_data, \
+ show_iterative_graph_data, show_tooltip
from ..utils.url_processing import url_decode
from .tables import search_table
from ..coverage.tables import coverage_tables
@@ -448,7 +448,7 @@ class Layout:
columns=columns,
data=table.to_dict("records"),
filter_action="custom",
- sort_action="native",
+ sort_action="custom",
sort_mode="multi",
selected_columns=[],
selected_rows=[],
@@ -538,6 +538,7 @@ class Layout:
State({"type": "table", "index": ALL}, "data"),
Input("url", "href"),
Input({"type": "table", "index": ALL}, "filter_query"),
+ Input({"type": "table", "index": ALL}, "sort_by"),
Input({"type": "ctrl-dd", "index": ALL}, "value"),
prevent_initial_call=True
)
@@ -679,10 +680,16 @@ class Layout:
}
on_draw = True
elif trigger.type == "table" and trigger.idx == "search":
- filtered_data = filter_table_data(
- store_table_data,
- trigger.value
- )
+ if trigger.parameter == "filter_query":
+ filtered_data = filter_table_data(
+ store_table_data,
+ trigger.value
+ )
+ elif trigger.parameter == "sort_by":
+ filtered_data = sort_table_data(
+ store_table_data,
+ trigger.value
+ )
table_data = [filtered_data, ]
if on_draw:
@@ -735,8 +742,8 @@ class Layout:
rls = store["selection"]["release"]
tb = row["Test Bed"].iloc[0]
nic = row["NIC"].iloc[0]
- driver = row['Driver'].iloc[0]
- test_name = row['Test'].iloc[0]
+ driver = row["Driver"].iloc[0]
+ test_name = row["Test"].iloc[0]
dutver = str()
except(KeyError, IndexError, AttributeError, ValueError):
raise PreventUpdate
@@ -777,7 +784,7 @@ class Layout:
testtype = [testtype, ]
core = l_test[1] if l_test[1] else "8c"
test = "-".join(l_test[2: -1])
- test_id = f"{tb}-{nic}-{driver}-{core}-{l_test[0]}-{test}"
+ test_id = f"{tb}-{nic}-{driver}-{l_test[0]}-{core}-{test}"
title = dbc.Row(
class_name="g-0 p-0",
children=dbc.Alert(test_id, color="info"),
@@ -873,7 +880,7 @@ class Layout:
Input({"type": "graph-iter", "index": ALL}, "clickData"),
prevent_initial_call=True
)
- def _show_metadata_from_trend_graph(
+ def _show_metadata_from_graph(
trend_data: dict,
iter_data: dict
) -> tuple:
diff --git a/csit.infra.dash/app/cdash/utils/utils.py b/csit.infra.dash/app/cdash/utils/utils.py
index 3d2866fbe0..692e45efb2 100644
--- a/csit.infra.dash/app/cdash/utils/utils.py
+++ b/csit.infra.dash/app/cdash/utils/utils.py
@@ -631,6 +631,39 @@ def filter_table_data(
return df.to_dict("records")
+def sort_table_data(
+ store_table_data: list,
+ sort_by: list
+ ) -> list:
+ """Sort table data using user specified order.
+
+ :param store_table_data: Table data represented as a list of records.
+ :param sort_by: User specified sorting order (multicolumn).
+ :type store_table_data: list
+ :type sort_by: list
+ :returns: A new table created by sorting the table data represented as
+ a list of records.
+ :rtype: list
+ """
+
+ # Checks:
+ if not any((sort_by, store_table_data, )):
+ return store_table_data
+
+ df = pd.DataFrame.from_records(store_table_data)
+ if len(sort_by):
+ dff = df.sort_values(
+ [col["column_id"] for col in sort_by],
+ ascending=[col["direction"] == "asc" for col in sort_by],
+ inplace=False
+ )
+ else:
+ # No sort is applied
+ dff = df
+
+ return dff.to_dict("records")
+
+
def show_trending_graph_data(
trigger: Trigger,
data: dict,