UTI: Display Latency graph
[csit.git] / resources / tools / dash / app / pal / trending / graphs.py
index a20ce8e..b71c327 100644 (file)
@@ -20,8 +20,121 @@ import pandas as pd
 import re
 
 from datetime import datetime
+from numpy import isnan
 
-from dash import no_update
+from ..jumpavg import classify
+
+
+_COLORS = (
+    u"#1A1110",
+    u"#DA2647",
+    u"#214FC6",
+    u"#01786F",
+    u"#BD8260",
+    u"#FFD12A",
+    u"#A6E7FF",
+    u"#738276",
+    u"#C95A49",
+    u"#FC5A8D",
+    u"#CEC8EF",
+    u"#391285",
+    u"#6F2DA8",
+    u"#FF878D",
+    u"#45A27D",
+    u"#FFD0B9",
+    u"#FD5240",
+    u"#DB91EF",
+    u"#44D7A8",
+    u"#4F86F7",
+    u"#84DE02",
+    u"#FFCFF1",
+    u"#614051"
+)
+_ANOMALY_COLOR = {
+    u"regression": 0.0,
+    u"normal": 0.5,
+    u"progression": 1.0
+}
+_COLORSCALE_TPUT = [
+    [0.00, u"red"],
+    [0.33, u"red"],
+    [0.33, u"white"],
+    [0.66, u"white"],
+    [0.66, u"green"],
+    [1.00, u"green"]
+]
+_TICK_TEXT_TPUT = [u"Regression", u"Normal", u"Progression"]
+_COLORSCALE_LAT = [
+    [0.00, u"green"],
+    [0.33, u"green"],
+    [0.33, u"white"],
+    [0.66, u"white"],
+    [0.66, u"red"],
+    [1.00, u"red"]
+]
+_TICK_TEXT_LAT = [u"Progression", u"Normal", u"Regression"]
+_VALUE = {
+    "mrr": "result_receive_rate_rate_avg",
+    "ndr": "result_ndr_lower_rate_value",
+    "pdr": "result_pdr_lower_rate_value",
+    "pdr-lat": "result_latency_forward_pdr_50_avg"
+}
+_UNIT = {
+    "mrr": "result_receive_rate_rate_unit",
+    "ndr": "result_ndr_lower_rate_unit",
+    "pdr": "result_pdr_lower_rate_unit",
+    "pdr-lat": "result_latency_forward_pdr_50_unit"
+}
+
+
+def _classify_anomalies(data):
+    """Process the data and return anomalies and trending values.
+
+    Gather data into groups with average as trend value.
+    Decorate values within groups to be normal,
+    the first value of changed average as a regression, or a progression.
+
+    :param data: Full data set with unavailable samples replaced by nan.
+    :type data: OrderedDict
+    :returns: Classification and trend values
+    :rtype: 3-tuple, list of strings, list of floats and list of floats
+    """
+    # NaN means something went wrong.
+    # Use 0.0 to cause that being reported as a severe regression.
+    bare_data = [0.0 if isnan(sample) else sample for sample in data.values()]
+    # TODO: Make BitCountingGroupList a subclass of list again?
+    group_list = classify(bare_data).group_list
+    group_list.reverse()  # Just to use .pop() for FIFO.
+    classification = list()
+    avgs = list()
+    stdevs = list()
+    active_group = None
+    values_left = 0
+    avg = 0.0
+    stdv = 0.0
+    for sample in data.values():
+        if isnan(sample):
+            classification.append(u"outlier")
+            avgs.append(sample)
+            stdevs.append(sample)
+            continue
+        if values_left < 1 or active_group is None:
+            values_left = 0
+            while values_left < 1:  # Ignore empty groups (should not happen).
+                active_group = group_list.pop()
+                values_left = len(active_group.run_list)
+            avg = active_group.stats.avg
+            stdv = active_group.stats.stdev
+            classification.append(active_group.comment)
+            avgs.append(avg)
+            stdevs.append(stdv)
+            values_left -= 1
+            continue
+        classification.append(u"normal")
+        avgs.append(avg)
+        stdevs.append(stdv)
+        values_left -= 1
+    return classification, avgs, stdevs
 
 
 def trending_tput(data: pd.DataFrame, sel:dict, layout: dict, start: datetime,
@@ -30,32 +143,27 @@ def trending_tput(data: pd.DataFrame, sel:dict, layout: dict, start: datetime,
     """
 
     if not sel:
-        return no_update, no_update
-
-    def _generate_trace(ttype: str, name: str, df: pd.DataFrame,
-        start: datetime, end: datetime):
-
-        value = {
-            "mrr": "result_receive_rate_rate_avg",
-            "ndr": "result_ndr_lower_rate_value",
-            "pdr": "result_pdr_lower_rate_value"
-        }
-        unit = {
-            "mrr": "result_receive_rate_rate_unit",
-            "ndr": "result_ndr_lower_rate_unit",
-            "pdr": "result_pdr_lower_rate_unit"
-        }
-
-        x_axis = [
-            d for d in df["start_time"] if d >= start and d <= end
-        ]
-        hover_txt = list()
+        return None, None
+
+    def _generate_traces(ttype: str, name: str, df: pd.DataFrame,
+        start: datetime, end: datetime, color: str):
+
+        df = df.dropna(subset=[_VALUE[ttype], ])
+        if df.empty:
+            return list()
+
+        x_axis = [d for d in df["start_time"] if d >= start and d <= end]
+
+        anomalies, trend_avg, trend_stdev = _classify_anomalies(
+            {k: v for k, v in zip(x_axis, df[_VALUE[ttype]])}
+        )
+
+        hover = list()
         for _, row in df.iterrows():
             hover_itm = (
-                f"date: "
-                f"{row['start_time'].strftime('%d-%m-%Y %H:%M:%S')}<br>"
-                f"average [{row[unit[ttype]]}]: "
-                f"{row[value[ttype]]}<br>"
+                f"date: {row['start_time'].strftime('%d-%m-%Y %H:%M:%S')}<br>"
+                f"<prop> [{row[_UNIT[ttype]]}]: {row[_VALUE[ttype]]}<br>"
+                f"<stdev>"
                 f"{row['dut_type']}-ref: {row['dut_version']}<br>"
                 f"csit-ref: {row['job']}/{row['build']}"
             )
@@ -66,21 +174,113 @@ def trending_tput(data: pd.DataFrame, sel:dict, layout: dict, start: datetime,
                 )
             else:
                 stdev = ""
-            hover_itm = hover_itm.replace("<stdev>", stdev)
-            hover_txt.append(hover_itm)
-
-        return go.Scatter(
-            x=x_axis,
-            y=df[value[ttype]],
-            name=name,
-            mode="markers+lines",
-            text=hover_txt,
-            hoverinfo=u"text+name"
-        )
+            hover_itm = hover_itm.replace(
+                "<prop>", "latency" if ttype == "pdr-lat" else "average"
+            ).replace("<stdev>", stdev)
+            hover.append(hover_itm)
+
+        hover_trend = list()
+        for avg, stdev in zip(trend_avg, trend_stdev):
+            if ttype == "pdr-lat":
+                hover_trend.append(
+                    f"trend [us]: {avg}<br>"
+                    f"stdev [us]: {stdev}"
+                )
+            else:
+                hover_trend.append(
+                    f"trend [pps]: {avg}<br>"
+                    f"stdev [pps]: {stdev}"
+                )
+
+        traces = [
+            go.Scatter(  # Samples
+                x=x_axis,
+                y=df[_VALUE[ttype]],
+                name=name,
+                mode="markers",
+                marker={
+                    u"size": 5,
+                    u"color": color,
+                    u"symbol": u"circle",
+                },
+                text=hover,
+                hoverinfo=u"text+name",
+                showlegend=True,
+                legendgroup=name,
+            ),
+            go.Scatter(  # Trend line
+                x=x_axis,
+                y=trend_avg,
+                name=name,
+                mode="lines",
+                line={
+                    u"shape": u"linear",
+                    u"width": 1,
+                    u"color": color,
+                },
+                text=hover_trend,
+                hoverinfo=u"text+name",
+                showlegend=False,
+                legendgroup=name,
+            )
+        ]
+
+        if anomalies:
+            anomaly_x = list()
+            anomaly_y = list()
+            anomaly_color = list()
+            for idx, anomaly in enumerate(anomalies):
+                if anomaly in (u"regression", u"progression"):
+                    anomaly_x.append(x_axis[idx])
+                    anomaly_y.append(trend_avg[idx])
+                    anomaly_color.append(_ANOMALY_COLOR[anomaly])
+            anomaly_color.extend([0.0, 0.5, 1.0])
+            traces.append(
+                go.Scatter(
+                    x=anomaly_x,
+                    y=anomaly_y,
+                    mode=u"markers",
+                    hoverinfo=u"none",
+                    showlegend=False,
+                    legendgroup=name,
+                    name=f"{name}-anomalies",
+                    marker={
+                        u"size": 15,
+                        u"symbol": u"circle-open",
+                        u"color": anomaly_color,
+                        u"colorscale": _COLORSCALE_LAT \
+                            if ttype == "pdr-lat" else _COLORSCALE_TPUT,
+                        u"showscale": True,
+                        u"line": {
+                            u"width": 2
+                        },
+                        u"colorbar": {
+                            u"y": 0.5,
+                            u"len": 0.8,
+                            u"title": u"Circles Marking Data Classification",
+                            u"titleside": u"right",
+                            u"titlefont": {
+                                u"size": 14
+                            },
+                            u"tickmode": u"array",
+                            u"tickvals": [0.167, 0.500, 0.833],
+                            u"ticktext": _TICK_TEXT_LAT \
+                                if ttype == "pdr-lat" else _TICK_TEXT_TPUT,
+                            u"ticks": u"",
+                            u"ticklen": 0,
+                            u"tickangle": -90,
+                            u"thickness": 10
+                        }
+                    }
+                )
+            )
+
+        return traces
 
     # Generate graph:
-    fig = go.Figure()
-    for itm in sel:
+    fig_tput = None
+    fig_lat = None
+    for idx, itm in enumerate(sel):
         phy = itm["phy"].split("-")
         if len(phy) == 4:
             topo, arch, nic, drv = phy
@@ -88,6 +288,7 @@ def trending_tput(data: pd.DataFrame, sel:dict, layout: dict, start: datetime,
                 drv = ""
             else:
                 drv += "-"
+                drv = drv.replace("_", "-")
         else:
             continue
         cadence = \
@@ -111,16 +312,29 @@ def trending_tput(data: pd.DataFrame, sel:dict, layout: dict, start: datetime,
             f"{itm['phy']}-{itm['framesize']}-{itm['core']}-"
             f"{itm['test']}-{itm['testtype']}"
         )
-        fig.add_trace(_generate_trace(itm['testtype'], name, df, start, end))
 
-    style={
-        "vertical-align": "top",
-        "display": "inline-block",
-        "width": "80%",
-        "padding": "5px"
-    }
+        traces = _generate_traces(
+            itm["testtype"], name, df, start, end, _COLORS[idx % len(_COLORS)]
+        )
+        if traces:
+            if not fig_tput:
+                fig_tput = go.Figure()
+            for trace in traces:
+                fig_tput.add_trace(trace)
+
+        if itm["testtype"] == "pdr":
+            traces = _generate_traces(
+                "pdr-lat", name, df, start, end, _COLORS[idx % len(_COLORS)]
+            )
+            if traces:
+                if not fig_lat:
+                    fig_lat = go.Figure()
+                for trace in traces:
+                    fig_lat.add_trace(trace)
 
-    layout = layout.get("plot-trending", dict())
-    fig.update_layout(layout)
+    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, style
+    return fig_tput, fig_lat