d9347b1c131e7e24f85f3070c7831404e87bc346
[csit.git] / csit.infra.dash / app / cdash / utils / utils.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 """Function used by Dash applications.
15 """
16
17 import pandas as pd
18 import plotly.graph_objects as go
19 import dash_bootstrap_components as dbc
20
21 import hdrh.histogram
22 import hdrh.codec
23
24 from math import sqrt
25 from numpy import isnan
26 from dash import dcc
27 from datetime import datetime
28
29 from ..jumpavg import classify
30 from ..utils.constants import Constants as C
31 from ..utils.url_processing import url_encode
32
33
34 def classify_anomalies(data):
35     """Process the data and return anomalies and trending values.
36
37     Gather data into groups with average as trend value.
38     Decorate values within groups to be normal,
39     the first value of changed average as a regression, or a progression.
40
41     :param data: Full data set with unavailable samples replaced by nan.
42     :type data: OrderedDict
43     :returns: Classification and trend values
44     :rtype: 3-tuple, list of strings, list of floats and list of floats
45     """
46     # NaN means something went wrong.
47     # Use 0.0 to cause that being reported as a severe regression.
48     bare_data = [0.0 if isnan(sample) else sample for sample in data.values()]
49     # TODO: Make BitCountingGroupList a subclass of list again?
50     group_list = classify(bare_data).group_list
51     group_list.reverse()  # Just to use .pop() for FIFO.
52     classification = list()
53     avgs = list()
54     stdevs = list()
55     active_group = None
56     values_left = 0
57     avg = 0.0
58     stdv = 0.0
59     for sample in data.values():
60         if isnan(sample):
61             classification.append("outlier")
62             avgs.append(sample)
63             stdevs.append(sample)
64             continue
65         if values_left < 1 or active_group is None:
66             values_left = 0
67             while values_left < 1:  # Ignore empty groups (should not happen).
68                 active_group = group_list.pop()
69                 values_left = len(active_group.run_list)
70             avg = active_group.stats.avg
71             stdv = active_group.stats.stdev
72             classification.append(active_group.comment)
73             avgs.append(avg)
74             stdevs.append(stdv)
75             values_left -= 1
76             continue
77         classification.append("normal")
78         avgs.append(avg)
79         stdevs.append(stdv)
80         values_left -= 1
81     return classification, avgs, stdevs
82
83
84 def get_color(idx: int) -> str:
85     """Returns a color from the list defined in Constants.PLOT_COLORS defined by
86     its index.
87
88     :param idx: Index of the color.
89     :type idx: int
90     :returns: Color defined by hex code.
91     :trype: str
92     """
93     return C.PLOT_COLORS[idx % len(C.PLOT_COLORS)]
94
95
96 def show_tooltip(tooltips:dict, id: str, title: str,
97         clipboard_id: str=None) -> list:
98     """Generate list of elements to display a text (e.g. a title) with a
99     tooltip and optionaly with Copy&Paste icon and the clipboard
100     functionality enabled.
101
102     :param tooltips: Dictionary with tooltips.
103     :param id: Tooltip ID.
104     :param title: A text for which the tooltip will be displayed.
105     :param clipboard_id: If defined, a Copy&Paste icon is displayed and the
106         clipboard functionality is enabled.
107     :type tooltips: dict
108     :type id: str
109     :type title: str
110     :type clipboard_id: str
111     :returns: List of elements to display a text with a tooltip and
112         optionaly with Copy&Paste icon.
113     :rtype: list
114     """
115
116     return [
117         dcc.Clipboard(target_id=clipboard_id, title="Copy URL") \
118             if clipboard_id else str(),
119         f"{title} ",
120         dbc.Badge(
121             id=id,
122             children="?",
123             pill=True,
124             color="white",
125             text_color="info",
126             class_name="border ms-1",
127         ),
128         dbc.Tooltip(
129             children=tooltips.get(id, str()),
130             target=id,
131             placement="auto"
132         )
133     ]
134
135
136 def label(key: str) -> str:
137     """Returns a label for input elements (dropdowns, ...).
138
139     If the label is not defined, the function returns the provided key.
140
141     :param key: The key to the label defined in Constants.LABELS.
142     :type key: str
143     :returns: Label.
144     :rtype: str
145     """
146     return C.LABELS.get(key, key)
147
148
149 def sync_checklists(options: list, sel: list, all: list, id: str) -> tuple:
150     """Synchronize a checklist with defined "options" with its "All" checklist.
151
152     :param options: List of options for the cheklist.
153     :param sel: List of selected options.
154     :param all: List of selected option from "All" checklist.
155     :param id: ID of a checklist to be used for synchronization.
156     :returns: Tuple of lists with otions for both checklists.
157     :rtype: tuple of lists
158     """
159     opts = {v["value"] for v in options}
160     if id =="all":
161         sel = list(opts) if all else list()
162     else:
163         all = ["all", ] if set(sel) == opts else list()
164     return sel, all
165
166
167 def list_tests(selection: dict) -> list:
168     """Transform list of tests to a list of dictionaries usable by checkboxes.
169
170     :param selection: List of tests to be displayed in "Selected tests" window.
171     :type selection: list
172     :returns: List of dictionaries with "label", "value" pairs for a checkbox.
173     :rtype: list
174     """
175     if selection:
176         return [{"label": v["id"], "value": v["id"]} for v in selection]
177     else:
178         return list()
179
180
181 def get_date(s_date: str) -> datetime:
182     """Transform string reprezentation of date to datetime.datetime data type.
183
184     :param s_date: String reprezentation of date.
185     :type s_date: str
186     :returns: Date as datetime.datetime.
187     :rtype: datetime.datetime
188     """
189     return datetime(int(s_date[0:4]), int(s_date[5:7]), int(s_date[8:10]))
190
191
192 def gen_new_url(url_components: dict, params: dict) -> str:
193     """Generate a new URL with encoded parameters.
194
195     :param url_components: Dictionary with URL elements. It should contain
196         "scheme", "netloc" and "path".
197     :param url_components: URL parameters to be encoded to the URL.
198     :type parsed_url: dict
199     :type params: dict
200     :returns Encoded URL with parameters.
201     :rtype: str
202     """
203
204     if url_components:
205         return url_encode(
206             {
207                 "scheme": url_components.get("scheme", ""),
208                 "netloc": url_components.get("netloc", ""),
209                 "path": url_components.get("path", ""),
210                 "params": params
211             }
212         )
213     else:
214         return str()
215
216
217 def get_duts(df: pd.DataFrame) -> list:
218     """Get the list of DUTs from the pre-processed information about jobs.
219
220     :param df: DataFrame with information about jobs.
221     :type df: pandas.DataFrame
222     :returns: Alphabeticaly sorted list of DUTs.
223     :rtype: list
224     """
225     return sorted(list(df["dut"].unique()))
226
227
228 def get_ttypes(df: pd.DataFrame, dut: str) -> list:
229     """Get the list of test types from the pre-processed information about
230     jobs.
231
232     :param df: DataFrame with information about jobs.
233     :param dut: The DUT for which the list of test types will be populated.
234     :type df: pandas.DataFrame
235     :type dut: str
236     :returns: Alphabeticaly sorted list of test types.
237     :rtype: list
238     """
239     return sorted(list(df.loc[(df["dut"] == dut)]["ttype"].unique()))
240
241
242 def get_cadences(df: pd.DataFrame, dut: str, ttype: str) -> list:
243     """Get the list of cadences from the pre-processed information about
244     jobs.
245
246     :param df: DataFrame with information about jobs.
247     :param dut: The DUT for which the list of cadences will be populated.
248     :param ttype: The test type for which the list of cadences will be
249         populated.
250     :type df: pandas.DataFrame
251     :type dut: str
252     :type ttype: str
253     :returns: Alphabeticaly sorted list of cadences.
254     :rtype: list
255     """
256     return sorted(list(df.loc[(
257         (df["dut"] == dut) &
258         (df["ttype"] == ttype)
259     )]["cadence"].unique()))
260
261
262 def get_test_beds(df: pd.DataFrame, dut: str, ttype: str, cadence: str) -> list:
263     """Get the list of test beds from the pre-processed information about
264     jobs.
265
266     :param df: DataFrame with information about jobs.
267     :param dut: The DUT for which the list of test beds will be populated.
268     :param ttype: The test type for which the list of test beds will be
269         populated.
270     :param cadence: The cadence for which the list of test beds will be
271         populated.
272     :type df: pandas.DataFrame
273     :type dut: str
274     :type ttype: str
275     :type cadence: str
276     :returns: Alphabeticaly sorted list of test beds.
277     :rtype: list
278     """
279     return sorted(list(df.loc[(
280         (df["dut"] == dut) &
281         (df["ttype"] == ttype) &
282         (df["cadence"] == cadence)
283     )]["tbed"].unique()))
284
285
286 def get_job(df: pd.DataFrame, dut, ttype, cadence, testbed):
287     """Get the name of a job defined by dut, ttype, cadence, test bed.
288     Input information comes from the control panel.
289
290     :param df: DataFrame with information about jobs.
291     :param dut: The DUT for which the job name will be created.
292     :param ttype: The test type for which the job name will be created.
293     :param cadence: The cadence for which the job name will be created.
294     :param testbed: The test bed for which the job name will be created.
295     :type df: pandas.DataFrame
296     :type dut: str
297     :type ttype: str
298     :type cadence: str
299     :type testbed: str
300     :returns: Job name.
301     :rtype: str
302     """
303     return df.loc[(
304         (df["dut"] == dut) &
305         (df["ttype"] == ttype) &
306         (df["cadence"] == cadence) &
307         (df["tbed"] == testbed)
308     )]["job"].item()
309
310
311 def generate_options(opts: list, sort: bool=True) -> list:
312     """Return list of options for radio items in control panel. The items in
313     the list are dictionaries with keys "label" and "value".
314
315     :params opts: List of options (str) to be used for the generated list.
316     :type opts: list
317     :returns: List of options (dict).
318     :rtype: list
319     """
320     if sort:
321         opts = sorted(opts)
322     return [{"label": i, "value": i} for i in opts]
323
324
325 def set_job_params(df: pd.DataFrame, job: str) -> dict:
326     """Create a dictionary with all options and values for (and from) the
327     given job.
328
329     :param df: DataFrame with information about jobs.
330     :params job: The name of job for and from which the dictionary will be
331         created.
332     :type df: pandas.DataFrame
333     :type job: str
334     :returns: Dictionary with all options and values for (and from) the
335         given job.
336     :rtype: dict
337     """
338
339     l_job = job.split("-")
340     return {
341         "job": job,
342         "dut": l_job[1],
343         "ttype": l_job[3],
344         "cadence": l_job[4],
345         "tbed": "-".join(l_job[-2:]),
346         "duts": generate_options(get_duts(df)),
347         "ttypes": generate_options(get_ttypes(df, l_job[1])),
348         "cadences": generate_options(get_cadences(df, l_job[1], l_job[3])),
349         "tbeds": generate_options(
350             get_test_beds(df, l_job[1], l_job[3], l_job[4]))
351     }
352
353
354 def get_list_group_items(
355         items: list,
356         type: str,
357         colorize: bool=True,
358         add_index: bool=False
359     ) -> list:
360     """Generate list of ListGroupItems with checkboxes with selected items.
361
362     :param items: List of items to be displayed in the ListGroup.
363     :param type: The type part of an element ID.
364     :param colorize: If True, the color of labels is set, otherwise the default
365         color is used.
366     :param add_index: Add index to the list items.
367     :type items: list
368     :type type: str
369     :type colorize: bool
370     :type add_index: bool
371     :returns: List of ListGroupItems with checkboxes with selected items.
372     :rtype: list
373     """
374
375     children = list()
376     for i, l in enumerate(items):
377         idx = f"{i + 1}. " if add_index else str()
378         label = f"{idx}{l['id']}" if isinstance(l, dict) else f"{idx}{l}"
379         children.append(
380             dbc.ListGroupItem(
381                 children=[
382                     dbc.Checkbox(
383                         id={"type": type, "index": i},
384                         label=label,
385                         value=False,
386                         label_class_name="m-0 p-0",
387                         label_style={
388                             "font-size": ".875em",
389                             "color": get_color(i) if colorize else "#55595c"
390                         },
391                         class_name="info"
392                     )
393                 ],
394                 class_name="p-0"
395             )
396         )
397
398     return children
399
400
401 def relative_change_stdev(mean1, mean2, std1, std2):
402     """Compute relative standard deviation of change of two values.
403
404     The "1" values are the base for comparison.
405     Results are returned as percentage (and percentual points for stdev).
406     Linearized theory is used, so results are wrong for relatively large stdev.
407
408     :param mean1: Mean of the first number.
409     :param mean2: Mean of the second number.
410     :param std1: Standard deviation estimate of the first number.
411     :param std2: Standard deviation estimate of the second number.
412     :type mean1: float
413     :type mean2: float
414     :type std1: float
415     :type std2: float
416     :returns: Relative change and its stdev.
417     :rtype: float
418     """
419     mean1, mean2 = float(mean1), float(mean2)
420     quotient = mean2 / mean1
421     first = std1 / mean1
422     second = std2 / mean2
423     std = quotient * sqrt(first * first + second * second)
424     return (quotient - 1) * 100, std * 100
425
426
427 def get_hdrh_latencies(row: pd.Series, name: str) -> dict:
428     """Get the HDRH latencies from the test data.
429
430     :param row: A row fron the data frame with test data.
431     :param name: The test name to be displayed as the graph title.
432     :type row: pandas.Series
433     :type name: str
434     :returns: Dictionary with HDRH latencies.
435     :rtype: dict
436     """
437
438     latencies = {"name": name}
439     for key in C.LAT_HDRH:
440         try:
441             latencies[key] = row[key]
442         except KeyError:
443             return None
444
445     return latencies
446
447
448 def graph_hdrh_latency(data: dict, layout: dict) -> go.Figure:
449     """Generate HDR Latency histogram graphs.
450
451     :param data: HDRH data.
452     :param layout: Layout of plot.ly graph.
453     :type data: dict
454     :type layout: dict
455     :returns: HDR latency Histogram.
456     :rtype: plotly.graph_objects.Figure
457     """
458
459     fig = None
460
461     traces = list()
462     for idx, (lat_name, lat_hdrh) in enumerate(data.items()):
463         try:
464             decoded = hdrh.histogram.HdrHistogram.decode(lat_hdrh)
465         except (hdrh.codec.HdrLengthException, TypeError):
466             continue
467         previous_x = 0.0
468         prev_perc = 0.0
469         xaxis = list()
470         yaxis = list()
471         hovertext = list()
472         for item in decoded.get_recorded_iterator():
473             # The real value is "percentile".
474             # For 100%, we cut that down to "x_perc" to avoid
475             # infinity.
476             percentile = item.percentile_level_iterated_to
477             x_perc = min(percentile, C.PERCENTILE_MAX)
478             xaxis.append(previous_x)
479             yaxis.append(item.value_iterated_to)
480             hovertext.append(
481                 f"<b>{C.GRAPH_LAT_HDRH_DESC[lat_name]}</b><br>"
482                 f"Direction: {('W-E', 'E-W')[idx % 2]}<br>"
483                 f"Percentile: {prev_perc:.5f}-{percentile:.5f}%<br>"
484                 f"Latency: {item.value_iterated_to}uSec"
485             )
486             next_x = 100.0 / (100.0 - x_perc)
487             xaxis.append(next_x)
488             yaxis.append(item.value_iterated_to)
489             hovertext.append(
490                 f"<b>{C.GRAPH_LAT_HDRH_DESC[lat_name]}</b><br>"
491                 f"Direction: {('W-E', 'E-W')[idx % 2]}<br>"
492                 f"Percentile: {prev_perc:.5f}-{percentile:.5f}%<br>"
493                 f"Latency: {item.value_iterated_to}uSec"
494             )
495             previous_x = next_x
496             prev_perc = percentile
497
498         traces.append(
499             go.Scatter(
500                 x=xaxis,
501                 y=yaxis,
502                 name=C.GRAPH_LAT_HDRH_DESC[lat_name],
503                 mode="lines",
504                 legendgroup=C.GRAPH_LAT_HDRH_DESC[lat_name],
505                 showlegend=bool(idx % 2),
506                 line=dict(
507                     color=get_color(int(idx/2)),
508                     dash="solid",
509                     width=1 if idx % 2 else 2
510                 ),
511                 hovertext=hovertext,
512                 hoverinfo="text"
513             )
514         )
515     if traces:
516         fig = go.Figure()
517         fig.add_traces(traces)
518         layout_hdrh = layout.get("plot-hdrh-latency", None)
519         if lat_hdrh:
520             fig.update_layout(layout_hdrh)
521
522     return fig