C-Dash: Fix anomaly detection for the news
[csit.git] / csit.infra.dash / app / cdash / trending / graphs.py
1 # Copyright (c) 2023 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 """Implementation of graphs for trending data.
15 """
16
17 import plotly.graph_objects as go
18 import pandas as pd
19
20 from ..utils.constants import Constants as C
21 from ..utils.utils import get_color, get_hdrh_latencies
22 from ..utils.anomalies import classify_anomalies
23
24
25 def select_trending_data(data: pd.DataFrame, itm: dict) -> pd.DataFrame:
26     """Select the data for graphs from the provided data frame.
27
28     :param data: Data frame with data for graphs.
29     :param itm: Item (in this case job name) which data will be selected from
30         the input data frame.
31     :type data: pandas.DataFrame
32     :type itm: dict
33     :returns: A data frame with selected data.
34     :rtype: pandas.DataFrame
35     """
36
37     phy = itm["phy"].split("-")
38     if len(phy) == 4:
39         topo, arch, nic, drv = phy
40         if drv == "dpdk":
41             drv = ""
42         else:
43             drv += "-"
44             drv = drv.replace("_", "-")
45     else:
46         return None
47
48     if itm["testtype"] in ("ndr", "pdr"):
49         test_type = "ndrpdr"
50     elif itm["testtype"] == "mrr":
51         test_type = "mrr"
52     elif itm["area"] == "hoststack":
53         test_type = "hoststack"
54     df = data.loc[(
55         (data["test_type"] == test_type) &
56         (data["passed"] == True)
57     )]
58     df = df[df.job.str.endswith(f"{topo}-{arch}")]
59     core = str() if itm["dut"] == "trex" else f"{itm['core']}"
60     ttype = "ndrpdr" if itm["testtype"] in ("ndr", "pdr") else itm["testtype"]
61     df = df[df.test_id.str.contains(
62         f"^.*[.|-]{nic}.*{itm['framesize']}-{core}-{drv}{itm['test']}-{ttype}$",
63         regex=True
64     )].sort_values(by="start_time", ignore_index=True)
65
66     return df
67
68
69 def graph_trending(
70         data: pd.DataFrame,
71         sel: dict,
72         layout: dict,
73         normalize: bool
74     ) -> tuple:
75     """Generate the trending graph(s) - MRR, NDR, PDR and for PDR also Latences
76     (result_latency_forward_pdr_50_avg).
77
78     :param data: Data frame with test results.
79     :param sel: Selected tests.
80     :param layout: Layout of plot.ly graph.
81     :param normalize: If True, the data is normalized to CPU frquency
82         Constants.NORM_FREQUENCY.
83     :type data: pandas.DataFrame
84     :type sel: dict
85     :type layout: dict
86     :type normalize: bool
87     :returns: Trending graph(s)
88     :rtype: tuple(plotly.graph_objects.Figure, plotly.graph_objects.Figure)
89     """
90
91     if not sel:
92         return None, None
93
94
95     def _generate_trending_traces(
96             ttype: str,
97             name: str,
98             df: pd.DataFrame,
99             color: str,
100             norm_factor: float
101         ) -> list:
102         """Generate the trending traces for the trending graph.
103
104         :param ttype: Test type (MRR, NDR, PDR).
105         :param name: The test name to be displayed as the graph title.
106         :param df: Data frame with test data.
107         :param color: The color of the trace (samples and trend line).
108         :param norm_factor: The factor used for normalization of the results to
109             CPU frequency set to Constants.NORM_FREQUENCY.
110         :type ttype: str
111         :type name: str
112         :type df: pandas.DataFrame
113         :type color: str
114         :type norm_factor: float
115         :returns: Traces (samples, trending line, anomalies)
116         :rtype: list
117         """
118
119         df = df.dropna(subset=[C.VALUE[ttype], ])
120         if df.empty:
121             return list(), list()
122
123         x_axis = df["start_time"].tolist()
124         if ttype == "latency":
125             y_data = [(v / norm_factor) for v in df[C.VALUE[ttype]].tolist()]
126         else:
127             y_data = [(v * norm_factor) for v in df[C.VALUE[ttype]].tolist()]
128         units = df[C.UNIT[ttype]].unique().tolist()
129
130         anomalies, trend_avg, trend_stdev = classify_anomalies(
131             {k: v for k, v in zip(x_axis, y_data)}
132         )
133
134         hover = list()
135         customdata = list()
136         customdata_samples = list()
137         for idx, (_, row) in enumerate(df.iterrows()):
138             d_type = "trex" if row["dut_type"] == "none" else row["dut_type"]
139             hover_itm = (
140                 f"date: {row['start_time'].strftime('%Y-%m-%d %H:%M:%S')}<br>"
141                 f"<prop> [{row[C.UNIT[ttype]]}]: {y_data[idx]:,.0f}<br>"
142                 f"<stdev>"
143                 f"<additional-info>"
144                 f"{d_type}-ref: {row['dut_version']}<br>"
145                 f"csit-ref: {row['job']}/{row['build']}<br>"
146                 f"hosts: {', '.join(row['hosts'])}"
147             )
148             if ttype == "mrr":
149                 stdev = (
150                     f"stdev [{row['result_receive_rate_rate_unit']}]: "
151                     f"{row['result_receive_rate_rate_stdev']:,.0f}<br>"
152                 )
153             else:
154                 stdev = str()
155             if ttype in ("hoststack-cps", "hoststack-rps"):
156                 add_info = (
157                     f"bandwidth [{row[C.UNIT['hoststack-bps']]}]: "
158                     f"{row[C.VALUE['hoststack-bps']]:,.0f}<br>"
159                     f"latency [{row[C.UNIT['hoststack-lat']]}]: "
160                     f"{row[C.VALUE['hoststack-lat']]:,.0f}<br>"
161                 )
162             else:
163                 add_info = str()
164             hover_itm = hover_itm.replace(
165                 "<prop>", "latency" if ttype == "latency" else "average"
166             ).replace("<stdev>", stdev).replace("<additional-info>", add_info)
167             hover.append(hover_itm)
168             if ttype == "latency":
169                 customdata_samples.append(get_hdrh_latencies(row, name))
170                 customdata.append({"name": name})
171             else:
172                 customdata_samples.append(
173                     {"name": name, "show_telemetry": True}
174                 )
175                 customdata.append({"name": name})
176
177         hover_trend = list()
178         for avg, stdev, (_, row) in zip(trend_avg, trend_stdev, df.iterrows()):
179             d_type = "trex" if row["dut_type"] == "none" else row["dut_type"]
180             hover_itm = (
181                 f"date: {row['start_time'].strftime('%Y-%m-%d %H:%M:%S')}<br>"
182                 f"trend [{row[C.UNIT[ttype]]}]: {avg:,.0f}<br>"
183                 f"stdev [{row[C.UNIT[ttype]]}]: {stdev:,.0f}<br>"
184                 f"{d_type}-ref: {row['dut_version']}<br>"
185                 f"csit-ref: {row['job']}/{row['build']}<br>"
186                 f"hosts: {', '.join(row['hosts'])}"
187             )
188             if ttype == "latency":
189                 hover_itm = hover_itm.replace("[pps]", "[us]")
190             hover_trend.append(hover_itm)
191
192         traces = [
193             go.Scatter(  # Samples
194                 x=x_axis,
195                 y=y_data,
196                 name=name,
197                 mode="markers",
198                 marker={
199                     "size": 5,
200                     "color": color,
201                     "symbol": "circle",
202                 },
203                 text=hover,
204                 hoverinfo="text+name",
205                 showlegend=True,
206                 legendgroup=name,
207                 customdata=customdata_samples
208             ),
209             go.Scatter(  # Trend line
210                 x=x_axis,
211                 y=trend_avg,
212                 name=name,
213                 mode="lines",
214                 line={
215                     "shape": "linear",
216                     "width": 1,
217                     "color": color,
218                 },
219                 text=hover_trend,
220                 hoverinfo="text+name",
221                 showlegend=False,
222                 legendgroup=name,
223                 customdata=customdata
224             )
225         ]
226
227         if anomalies:
228             anomaly_x = list()
229             anomaly_y = list()
230             anomaly_color = list()
231             hover = list()
232             for idx, anomaly in enumerate(anomalies):
233                 if anomaly in ("regression", "progression"):
234                     anomaly_x.append(x_axis[idx])
235                     anomaly_y.append(trend_avg[idx])
236                     anomaly_color.append(C.ANOMALY_COLOR[anomaly])
237                     hover_itm = (
238                         f"date: {x_axis[idx].strftime('%Y-%m-%d %H:%M:%S')}<br>"
239                         f"trend [pps]: {trend_avg[idx]:,.0f}<br>"
240                         f"classification: {anomaly}"
241                     )
242                     if ttype == "latency":
243                         hover_itm = hover_itm.replace("[pps]", "[us]")
244                     hover.append(hover_itm)
245             anomaly_color.extend([0.0, 0.5, 1.0])
246             traces.append(
247                 go.Scatter(
248                     x=anomaly_x,
249                     y=anomaly_y,
250                     mode="markers",
251                     text=hover,
252                     hoverinfo="text+name",
253                     showlegend=False,
254                     legendgroup=name,
255                     name=name,
256                     customdata=customdata,
257                     marker={
258                         "size": 15,
259                         "symbol": "circle-open",
260                         "color": anomaly_color,
261                         "colorscale": C.COLORSCALE_LAT \
262                             if ttype == "latency" else C.COLORSCALE_TPUT,
263                         "showscale": True,
264                         "line": {
265                             "width": 2
266                         },
267                         "colorbar": {
268                             "y": 0.5,
269                             "len": 0.8,
270                             "title": "Circles Marking Data Classification",
271                             "titleside": "right",
272                             "tickmode": "array",
273                             "tickvals": [0.167, 0.500, 0.833],
274                             "ticktext": C.TICK_TEXT_LAT \
275                                 if ttype == "latency" else C.TICK_TEXT_TPUT,
276                             "ticks": "",
277                             "ticklen": 0,
278                             "tickangle": -90,
279                             "thickness": 10
280                         }
281                     }
282                 )
283             )
284
285         return traces, units
286
287
288     fig_tput = None
289     fig_lat = None
290     y_units = set()
291     for idx, itm in enumerate(sel):
292         df = select_trending_data(data, itm)
293         if df is None or df.empty:
294             continue
295
296         if normalize:
297             phy = itm["phy"].split("-")
298             topo_arch = f"{phy[0]}-{phy[1]}" if len(phy) == 4 else str()
299             norm_factor = (C.NORM_FREQUENCY / C.FREQUENCY[topo_arch]) \
300                 if topo_arch else 1.0
301         else:
302             norm_factor = 1.0
303
304         if itm["area"] == "hoststack":
305             ttype = f"hoststack-{itm['testtype']}"
306         else:
307             ttype = itm["testtype"]
308
309         traces, units = _generate_trending_traces(
310             ttype,
311             itm["id"],
312             df,
313             get_color(idx),
314             norm_factor
315         )
316         if traces:
317             if not fig_tput:
318                 fig_tput = go.Figure()
319             fig_tput.add_traces(traces)
320
321         if itm["testtype"] == "pdr":
322             traces, _ = _generate_trending_traces(
323                 "latency",
324                 itm["id"],
325                 df,
326                 get_color(idx),
327                 norm_factor
328             )
329             if traces:
330                 if not fig_lat:
331                     fig_lat = go.Figure()
332                 fig_lat.add_traces(traces)
333
334         y_units.update(units)
335
336     if fig_tput:
337         fig_layout = layout.get("plot-trending-tput", dict())
338         fig_layout["yaxis"]["title"] = \
339             f"Throughput [{'|'.join(sorted(y_units))}]"
340         fig_tput.update_layout(fig_layout)
341     if fig_lat:
342         fig_lat.update_layout(layout.get("plot-trending-lat", dict()))
343
344     return fig_tput, fig_lat
345
346
347 def graph_tm_trending(data: pd.DataFrame, layout: dict) -> list:
348     """Generates one trending graph per test, each graph includes all selected
349     metrics.
350
351     :param data: Data frame with telemetry data.
352     :param layout: Layout of plot.ly graph.
353     :type data: pandas.DataFrame
354     :type layout: dict
355     :returns: List of generated graphs together with test names.
356         list(tuple(plotly.graph_objects.Figure(), str()), tuple(...), ...)
357     :rtype: list
358     """
359
360
361     def _generate_graph(
362             data: pd.DataFrame,
363             test: str,
364             layout: dict
365         ) -> go.Figure:
366         """Generates a trending graph for given test with all metrics.
367
368         :param data: Data frame with telemetry data for the given test.
369         :param test: The name of the test.
370         :param layout: Layout of plot.ly graph.
371         :type data: pandas.DataFrame
372         :type test: str
373         :type layout: dict
374         :returns: A trending graph.
375         :rtype: plotly.graph_objects.Figure
376         """
377         graph = None
378         traces = list()
379         for idx, metric in enumerate(data.tm_metric.unique()):
380             if "-pdr" in test and "='pdr'" not in metric:
381                 continue
382             if "-ndr" in test and "='ndr'" not in metric:
383                 continue
384
385             df = data.loc[(data["tm_metric"] == metric)]
386             x_axis = df["start_time"].tolist()
387             y_data = [float(itm) for itm in df["tm_value"].tolist()]
388             hover = list()
389             for i, (_, row) in enumerate(df.iterrows()):
390                 if row["test_type"] == "mrr":
391                     rate = (
392                         f"mrr avg [{row[C.UNIT['mrr']]}]: "
393                         f"{row[C.VALUE['mrr']]:,.0f}<br>"
394                         f"mrr stdev [{row[C.UNIT['mrr']]}]: "
395                         f"{row['result_receive_rate_rate_stdev']:,.0f}<br>"
396                     )
397                 elif row["test_type"] == "ndrpdr":
398                     if "-pdr" in test:
399                         rate = (
400                             f"pdr [{row[C.UNIT['pdr']]}]: "
401                             f"{row[C.VALUE['pdr']]:,.0f}<br>"
402                         )
403                     elif "-ndr" in test:
404                         rate = (
405                             f"ndr [{row[C.UNIT['ndr']]}]: "
406                             f"{row[C.VALUE['ndr']]:,.0f}<br>"
407                         )
408                     else:
409                         rate = str()
410                 else:
411                     rate = str()
412                 hover.append(
413                     f"date: "
414                     f"{row['start_time'].strftime('%Y-%m-%d %H:%M:%S')}<br>"
415                     f"value: {y_data[i]:,.0f}<br>"
416                     f"{rate}"
417                     f"{row['dut_type']}-ref: {row['dut_version']}<br>"
418                     f"csit-ref: {row['job']}/{row['build']}<br>"
419                 )
420             if any(y_data):
421                 anomalies, trend_avg, trend_stdev = classify_anomalies(
422                     {k: v for k, v in zip(x_axis, y_data)}
423                 )
424                 hover_trend = list()
425                 for avg, stdev, (_, row) in \
426                         zip(trend_avg, trend_stdev, df.iterrows()):
427                     hover_trend.append(
428                         f"date: "
429                         f"{row['start_time'].strftime('%Y-%m-%d %H:%M:%S')}<br>"
430                         f"trend: {avg:,.0f}<br>"
431                         f"stdev: {stdev:,.0f}<br>"
432                         f"{row['dut_type']}-ref: {row['dut_version']}<br>"
433                         f"csit-ref: {row['job']}/{row['build']}"
434                     )
435             else:
436                 anomalies = None
437             color = get_color(idx)
438             traces.append(
439                 go.Scatter(  # Samples
440                     x=x_axis,
441                     y=y_data,
442                     name=metric,
443                     mode="markers",
444                     marker={
445                         "size": 5,
446                         "color": color,
447                         "symbol": "circle",
448                     },
449                     text=hover,
450                     hoverinfo="text+name",
451                     showlegend=True,
452                     legendgroup=metric
453                 )
454             )
455             if anomalies:
456                 traces.append(
457                     go.Scatter(  # Trend line
458                         x=x_axis,
459                         y=trend_avg,
460                         name=metric,
461                         mode="lines",
462                         line={
463                             "shape": "linear",
464                             "width": 1,
465                             "color": color,
466                         },
467                         text=hover_trend,
468                         hoverinfo="text+name",
469                         showlegend=False,
470                         legendgroup=metric
471                     )
472                 )
473
474                 anomaly_x = list()
475                 anomaly_y = list()
476                 anomaly_color = list()
477                 hover = list()
478                 for idx, anomaly in enumerate(anomalies):
479                     if anomaly in ("regression", "progression"):
480                         anomaly_x.append(x_axis[idx])
481                         anomaly_y.append(trend_avg[idx])
482                         anomaly_color.append(C.ANOMALY_COLOR[anomaly])
483                         hover_itm = (
484                             f"date: {x_axis[idx].strftime('%Y-%m-%d %H:%M:%S')}"
485                             f"<br>trend: {trend_avg[idx]:,.0f}"
486                             f"<br>classification: {anomaly}"
487                         )
488                         hover.append(hover_itm)
489                 anomaly_color.extend([0.0, 0.5, 1.0])
490                 traces.append(
491                     go.Scatter(
492                         x=anomaly_x,
493                         y=anomaly_y,
494                         mode="markers",
495                         text=hover,
496                         hoverinfo="text+name",
497                         showlegend=False,
498                         legendgroup=metric,
499                         name=metric,
500                         marker={
501                             "size": 15,
502                             "symbol": "circle-open",
503                             "color": anomaly_color,
504                             "colorscale": C.COLORSCALE_TPUT,
505                             "showscale": True,
506                             "line": {
507                                 "width": 2
508                             },
509                             "colorbar": {
510                                 "y": 0.5,
511                                 "len": 0.8,
512                                 "title": "Circles Marking Data Classification",
513                                 "titleside": "right",
514                                 "tickmode": "array",
515                                 "tickvals": [0.167, 0.500, 0.833],
516                                 "ticktext": C.TICK_TEXT_TPUT,
517                                 "ticks": "",
518                                 "ticklen": 0,
519                                 "tickangle": -90,
520                                 "thickness": 10
521                             }
522                         }
523                     )
524                 )
525
526         if traces:
527             graph = go.Figure()
528             graph.add_traces(traces)
529             graph.update_layout(layout.get("plot-trending-telemetry", dict()))
530
531         return graph
532
533
534     tm_trending_graphs = list()
535
536     if data.empty:
537         return tm_trending_graphs
538
539     for test in data.test_name.unique():
540         df = data.loc[(data["test_name"] == test)]
541         graph = _generate_graph(df, test, layout)
542         if graph:
543             tm_trending_graphs.append((graph, test, ))
544
545     return tm_trending_graphs