UTI: PoC - Make detailed hover information copyable
[csit.git] / resources / tools / dash / app / pal / trending / graphs.py
1 # Copyright (c) 2022 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """
15 """
16
17
18 import logging
19 import plotly.graph_objects as go
20 import pandas as pd
21 import re
22
23 from datetime import datetime
24 from numpy import isnan
25 from dash import no_update
26
27 from ..jumpavg import classify
28
29
30 _COLORS = (
31     u"#1A1110",
32     u"#DA2647",
33     u"#214FC6",
34     u"#01786F",
35     u"#BD8260",
36     u"#FFD12A",
37     u"#A6E7FF",
38     u"#738276",
39     u"#C95A49",
40     u"#FC5A8D",
41     u"#CEC8EF",
42     u"#391285",
43     u"#6F2DA8",
44     u"#FF878D",
45     u"#45A27D",
46     u"#FFD0B9",
47     u"#FD5240",
48     u"#DB91EF",
49     u"#44D7A8",
50     u"#4F86F7",
51     u"#84DE02",
52     u"#FFCFF1",
53     u"#614051"
54 )
55 _ANOMALY_COLOR = {
56     u"regression": 0.0,
57     u"normal": 0.5,
58     u"progression": 1.0
59 }
60 _COLORSCALE = [
61     [0.00, u"red"],
62     [0.33, u"red"],
63     [0.33, u"white"],
64     [0.66, u"white"],
65     [0.66, u"green"],
66     [1.00, u"green"]
67 ]
68 _VALUE = {
69     "mrr": "result_receive_rate_rate_avg",
70     "ndr": "result_ndr_lower_rate_value",
71     "pdr": "result_pdr_lower_rate_value"
72 }
73 _UNIT = {
74     "mrr": "result_receive_rate_rate_unit",
75     "ndr": "result_ndr_lower_rate_unit",
76     "pdr": "result_pdr_lower_rate_unit"
77 }
78
79
80 def _classify_anomalies(data):
81     """Process the data and return anomalies and trending values.
82
83     Gather data into groups with average as trend value.
84     Decorate values within groups to be normal,
85     the first value of changed average as a regression, or a progression.
86
87     :param data: Full data set with unavailable samples replaced by nan.
88     :type data: OrderedDict
89     :returns: Classification and trend values
90     :rtype: 3-tuple, list of strings, list of floats and list of floats
91     """
92     # NaN means something went wrong.
93     # Use 0.0 to cause that being reported as a severe regression.
94     bare_data = [0.0 if isnan(sample) else sample for sample in data.values()]
95     # TODO: Make BitCountingGroupList a subclass of list again?
96     group_list = classify(bare_data).group_list
97     group_list.reverse()  # Just to use .pop() for FIFO.
98     classification = list()
99     avgs = list()
100     stdevs = list()
101     active_group = None
102     values_left = 0
103     avg = 0.0
104     stdv = 0.0
105     for sample in data.values():
106         if isnan(sample):
107             classification.append(u"outlier")
108             avgs.append(sample)
109             stdevs.append(sample)
110             continue
111         if values_left < 1 or active_group is None:
112             values_left = 0
113             while values_left < 1:  # Ignore empty groups (should not happen).
114                 active_group = group_list.pop()
115                 values_left = len(active_group.run_list)
116             avg = active_group.stats.avg
117             stdv = active_group.stats.stdev
118             classification.append(active_group.comment)
119             avgs.append(avg)
120             stdevs.append(stdv)
121             values_left -= 1
122             continue
123         classification.append(u"normal")
124         avgs.append(avg)
125         stdevs.append(stdv)
126         values_left -= 1
127     return classification, avgs, stdevs
128
129
130 def trending_tput(data: pd.DataFrame, sel:dict, layout: dict, start: datetime,
131     end: datetime):
132     """
133     """
134
135     if not sel:
136         return no_update, no_update
137
138     def _generate_traces(ttype: str, name: str, df: pd.DataFrame,
139         start: datetime, end: datetime, color: str):
140
141         df = df.dropna(subset=[_VALUE[ttype], ])
142         if df.empty:
143             return list()
144
145         x_axis = [d for d in df["start_time"] if d >= start and d <= end]
146
147         anomalies, trend_avg, trend_stdev = _classify_anomalies(
148             {k: v for k, v in zip(x_axis, df[_VALUE[ttype]])}
149         )
150
151         hover = list()
152         for _, row in df.iterrows():
153             hover_itm = (
154                 f"date: "
155                 f"{row['start_time'].strftime('%d-%m-%Y %H:%M:%S')}<br>"
156                 f"average [{row[_UNIT[ttype]]}]: "
157                 f"{row[_VALUE[ttype]]}<br>"
158                 f"{row['dut_type']}-ref: {row['dut_version']}<br>"
159                 f"csit-ref: {row['job']}/{row['build']}"
160             )
161             if ttype == "mrr":
162                 stdev = (
163                     f"stdev [{row['result_receive_rate_rate_unit']}]: "
164                     f"{row['result_receive_rate_rate_stdev']}<br>"
165                 )
166             else:
167                 stdev = ""
168             hover_itm = hover_itm.replace("<stdev>", stdev)
169             hover.append(hover_itm)
170
171         hover_trend = list()
172         for avg, stdev in zip(trend_avg, trend_stdev):
173             hover_trend.append(
174                 f"trend [pps]: {avg}<br>"
175                 f"stdev [pps]: {stdev}"
176             )
177
178         traces = [
179             go.Scatter(  # Samples
180                 x=x_axis,
181                 y=df[_VALUE[ttype]],
182                 name=name,
183                 mode="markers",
184                 marker={
185                     u"size": 5,
186                     u"color": color,
187                     u"symbol": u"circle",
188                 },
189                 text=hover,
190                 hoverinfo=u"text+name",
191                 showlegend=True,
192                 legendgroup=name,
193             ),
194             go.Scatter(  # Trend line
195                 x=x_axis,
196                 y=trend_avg,
197                 name=name,
198                 mode="lines",
199                 line={
200                     u"shape": u"linear",
201                     u"width": 1,
202                     u"color": color,
203                 },
204                 text=hover_trend,
205                 hoverinfo=u"text+name",
206                 showlegend=False,
207                 legendgroup=name,
208             )
209         ]
210
211         if anomalies:
212             anomaly_x = list()
213             anomaly_y = list()
214             anomaly_color = list()
215             ticktext = [u"Regression", u"Normal", u"Progression"]
216             for idx, anomaly in enumerate(anomalies):
217                 if anomaly in (u"regression", u"progression"):
218                     anomaly_x.append(x_axis[idx])
219                     anomaly_y.append(trend_avg[idx])
220                     anomaly_color.append(_ANOMALY_COLOR[anomaly])
221             anomaly_color.append([0.0, 0.5, 1.0])
222             traces.append(
223                 go.Scatter(
224                     x=anomaly_x,
225                     y=anomaly_y,
226                     mode=u"markers",
227                     hoverinfo=u"none",
228                     showlegend=False,
229                     legendgroup=name,
230                     name=f"{name}-anomalies",
231                     marker={
232                         u"size": 15,
233                         u"symbol": u"circle-open",
234                         u"color": anomaly_color,
235                         u"colorscale": _COLORSCALE,
236                         u"showscale": True,
237                         u"line": {
238                             u"width": 2
239                         },
240                         u"colorbar": {
241                             u"y": 0.5,
242                             u"len": 0.8,
243                             u"title": u"Circles Marking Data Classification",
244                             u"titleside": u"right",
245                             u"titlefont": {
246                                 u"size": 14
247                             },
248                             u"tickmode": u"array",
249                             u"tickvals": [0.167, 0.500, 0.833],
250                             u"ticktext": ticktext,
251                             u"ticks": u"",
252                             u"ticklen": 0,
253                             u"tickangle": -90,
254                             u"thickness": 10
255                         }
256                     }
257                 )
258             )
259
260         return traces
261
262     # Generate graph:
263     fig = go.Figure()
264     for idx, itm in enumerate(sel):
265         phy = itm["phy"].split("-")
266         if len(phy) == 4:
267             topo, arch, nic, drv = phy
268             if drv in ("dpdk", "ixgbe"):
269                 drv = ""
270             else:
271                 drv += "-"
272                 drv = drv.replace("_", "-")
273         else:
274             continue
275         cadence = \
276             "weekly" if (arch == "aws" or itm["testtype"] != "mrr") else "daily"
277         sel_topo_arch = (
278             f"csit-vpp-perf-"
279             f"{itm['testtype'] if itm['testtype'] == 'mrr' else 'ndrpdr'}-"
280             f"{cadence}-master-{topo}-{arch}"
281         )
282         df_sel = data.loc[(data["job"] == sel_topo_arch)]
283         regex = (
284             f"^.*{nic}.*\.{itm['framesize']}-{itm['core']}-{drv}{itm['test']}-"
285             f"{'mrr' if itm['testtype'] == 'mrr' else 'ndrpdr'}$"
286         )
287         df = df_sel.loc[
288             df_sel["test_id"].apply(
289                 lambda x: True if re.search(regex, x) else False
290             )
291         ].sort_values(by="start_time", ignore_index=True)
292         name = (
293             f"{itm['phy']}-{itm['framesize']}-{itm['core']}-"
294             f"{itm['test']}-{itm['testtype']}"
295         )
296         for trace in _generate_traces(itm['testtype'], name, df, start, end,
297                 _COLORS[idx % len(_COLORS)]):
298             fig.add_trace(trace)
299
300     style={
301         "vertical-align": "top",
302         "display": "inline-block",
303         "width": "80%",
304         "padding": "5px"
305     }
306
307     layout = layout.get("plot-trending", dict())
308     fig.update_layout(layout)
309
310     return fig, style