Trending: Add NAT44 tests
[csit.git] / resources / tools / presentation / generator_cpta.py
1 # Copyright (c) 2020 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 """Generation of Continuous Performance Trending and Analysis.
15 """
16
17 import logging
18 import csv
19
20 from collections import OrderedDict
21 from datetime import datetime
22 from copy import deepcopy
23
24 import prettytable
25 import plotly.offline as ploff
26 import plotly.graph_objs as plgo
27 import plotly.exceptions as plerr
28
29 from pal_utils import archive_input_data, execute_command, classify_anomalies
30
31
32 # Command to build the html format of the report
33 HTML_BUILDER = u'sphinx-build -v -c conf_cpta -a ' \
34                u'-b html -E ' \
35                u'-t html ' \
36                u'-D version="{date}" ' \
37                u'{working_dir} ' \
38                u'{build_dir}/'
39
40 # .css file for the html format of the report
41 THEME_OVERRIDES = u"""/* override table width restrictions */
42 .wy-nav-content {
43     max-width: 1200px !important;
44 }
45 .rst-content blockquote {
46     margin-left: 0px;
47     line-height: 18px;
48     margin-bottom: 0px;
49 }
50 .wy-menu-vertical a {
51     display: inline-block;
52     line-height: 18px;
53     padding: 0 2em;
54     display: block;
55     position: relative;
56     font-size: 90%;
57     color: #d9d9d9
58 }
59 .wy-menu-vertical li.current a {
60     color: gray;
61     border-right: solid 1px #c9c9c9;
62     padding: 0 3em;
63 }
64 .wy-menu-vertical li.toctree-l2.current > a {
65     background: #c9c9c9;
66     padding: 0 3em;
67 }
68 .wy-menu-vertical li.toctree-l2.current li.toctree-l3 > a {
69     display: block;
70     background: #c9c9c9;
71     padding: 0 4em;
72 }
73 .wy-menu-vertical li.toctree-l3.current li.toctree-l4 > a {
74     display: block;
75     background: #bdbdbd;
76     padding: 0 5em;
77 }
78 .wy-menu-vertical li.on a, .wy-menu-vertical li.current > a {
79     color: #404040;
80     padding: 0 2em;
81     font-weight: bold;
82     position: relative;
83     background: #fcfcfc;
84     border: none;
85         border-top-width: medium;
86         border-bottom-width: medium;
87         border-top-style: none;
88         border-bottom-style: none;
89         border-top-color: currentcolor;
90         border-bottom-color: currentcolor;
91     padding-left: 2em -4px;
92 }
93 """
94
95 COLORS = (
96     u"#1A1110",
97     u"#DA2647",
98     u"#214FC6",
99     u"#01786F",
100     u"#BD8260",
101     u"#FFD12A",
102     u"#A6E7FF",
103     u"#738276",
104     u"#C95A49",
105     u"#FC5A8D",
106     u"#CEC8EF",
107     u"#391285",
108     u"#6F2DA8",
109     u"#FF878D",
110     u"#45A27D",
111     u"#FFD0B9",
112     u"#FD5240",
113     u"#DB91EF",
114     u"#44D7A8",
115     u"#4F86F7",
116     u"#84DE02",
117     u"#FFCFF1",
118     u"#614051"
119 )
120
121
122 def generate_cpta(spec, data):
123     """Generate all formats and versions of the Continuous Performance Trending
124     and Analysis.
125
126     :param spec: Specification read from the specification file.
127     :param data: Full data set.
128     :type spec: Specification
129     :type data: InputData
130     """
131
132     logging.info(u"Generating the Continuous Performance Trending and Analysis "
133                  u"...")
134
135     ret_code = _generate_all_charts(spec, data)
136
137     cmd = HTML_BUILDER.format(
138         date=datetime.utcnow().strftime(u'%Y-%m-%d %H:%M UTC'),
139         working_dir=spec.environment[u'paths'][u'DIR[WORKING,SRC]'],
140         build_dir=spec.environment[u'paths'][u'DIR[BUILD,HTML]'])
141     execute_command(cmd)
142
143     with open(spec.environment[u'paths'][u'DIR[CSS_PATCH_FILE]'], u'w') as \
144             css_file:
145         css_file.write(THEME_OVERRIDES)
146
147     with open(spec.environment[u'paths'][u'DIR[CSS_PATCH_FILE2]'], u'w') as \
148             css_file:
149         css_file.write(THEME_OVERRIDES)
150
151     if spec.configuration.get(u"archive-inputs", True):
152         archive_input_data(spec)
153
154     logging.info(u"Done.")
155
156     return ret_code
157
158
159 def _generate_trending_traces(in_data, job_name, build_info,
160                               name=u"", color=u"", incl_tests=u"MRR"):
161     """Generate the trending traces:
162      - samples,
163      - outliers, regress, progress
164      - average of normal samples (trending line)
165
166     :param in_data: Full data set.
167     :param job_name: The name of job which generated the data.
168     :param build_info: Information about the builds.
169     :param name: Name of the plot
170     :param color: Name of the color for the plot.
171     :param incl_tests: Included tests, accepted values: MRR, NDR, PDR
172     :type in_data: OrderedDict
173     :type job_name: str
174     :type build_info: dict
175     :type name: str
176     :type color: str
177     :type incl_tests: str
178     :returns: Generated traces (list) and the evaluated result.
179     :rtype: tuple(traces, result)
180     """
181
182     if incl_tests not in (u"MRR", u"NDR", u"PDR"):
183         return list(), None
184
185     data_x = list(in_data.keys())
186     data_y_pps = list()
187     data_y_mpps = list()
188     data_y_stdev = list()
189     for item in in_data.values():
190         data_y_pps.append(float(item[u"receive-rate"]))
191         data_y_stdev.append(float(item[u"receive-stdev"]) / 1e6)
192         data_y_mpps.append(float(item[u"receive-rate"]) / 1e6)
193
194     hover_text = list()
195     xaxis = list()
196     for index, key in enumerate(data_x):
197         str_key = str(key)
198         date = build_info[job_name][str_key][0]
199         hover_str = (u"date: {date}<br>"
200                      u"{property} [Mpps]: {value:.3f}<br>"
201                      u"<stdev>"
202                      u"{sut}-ref: {build}<br>"
203                      u"csit-ref: {test}-{period}-build-{build_nr}<br>"
204                      u"testbed: {testbed}")
205         if incl_tests == u"MRR":
206             hover_str = hover_str.replace(
207                 u"<stdev>", f"stdev [Mpps]: {data_y_stdev[index]:.3f}<br>"
208             )
209         else:
210             hover_str = hover_str.replace(u"<stdev>", u"")
211         if u"-cps" in name:
212             hover_str = hover_str.replace(u"[Mpps]", u"[Mcps]")
213         if u"dpdk" in job_name:
214             hover_text.append(hover_str.format(
215                 date=date,
216                 property=u"average" if incl_tests == u"MRR" else u"throughput",
217                 value=data_y_mpps[index],
218                 sut=u"dpdk",
219                 build=build_info[job_name][str_key][1].rsplit(u'~', 1)[0],
220                 test=incl_tests.lower(),
221                 period=u"weekly",
222                 build_nr=str_key,
223                 testbed=build_info[job_name][str_key][2]))
224         elif u"vpp" in job_name:
225             hover_str = hover_str.format(
226                 date=date,
227                 property=u"average" if incl_tests == u"MRR" else u"throughput",
228                 value=data_y_mpps[index],
229                 sut=u"vpp",
230                 build=build_info[job_name][str_key][1].rsplit(u'~', 1)[0],
231                 test=incl_tests.lower(),
232                 period=u"daily" if incl_tests == u"MRR" else u"weekly",
233                 build_nr=str_key,
234                 testbed=build_info[job_name][str_key][2])
235             if u"-cps" in name:
236                 hover_str = hover_str.replace(u"throughput", u"connection rate")
237             hover_text.append(hover_str)
238
239         xaxis.append(datetime(int(date[0:4]), int(date[4:6]), int(date[6:8]),
240                               int(date[9:11]), int(date[12:])))
241
242     data_pd = OrderedDict()
243     for key, value in zip(xaxis, data_y_pps):
244         data_pd[key] = value
245
246     anomaly_classification, avgs_pps, stdevs_pps = classify_anomalies(data_pd)
247     avgs_mpps = [avg_pps / 1e6 for avg_pps in avgs_pps]
248     stdevs_mpps = [stdev_pps / 1e6 for stdev_pps in stdevs_pps]
249
250     anomalies = OrderedDict()
251     anomalies_colors = list()
252     anomalies_avgs = list()
253     anomaly_color = {
254         u"regression": 0.0,
255         u"normal": 0.5,
256         u"progression": 1.0
257     }
258     if anomaly_classification:
259         for index, (key, value) in enumerate(data_pd.items()):
260             if anomaly_classification[index] in (u"regression", u"progression"):
261                 anomalies[key] = value / 1e6
262                 anomalies_colors.append(
263                     anomaly_color[anomaly_classification[index]])
264                 anomalies_avgs.append(avgs_mpps[index])
265         anomalies_colors.extend([0.0, 0.5, 1.0])
266
267     # Create traces
268
269     trace_samples = plgo.Scatter(
270         x=xaxis,
271         y=data_y_mpps,
272         mode=u"markers",
273         line={
274             u"width": 1
275         },
276         showlegend=True,
277         legendgroup=name,
278         name=f"{name}",
279         marker={
280             u"size": 5,
281             u"color": color,
282             u"symbol": u"circle",
283         },
284         text=hover_text,
285         hoverinfo=u"text+name"
286     )
287     traces = [trace_samples, ]
288
289     trend_hover_text = list()
290     for idx in range(len(data_x)):
291         trend_hover_str = (
292             f"trend [Mpps]: {avgs_mpps[idx]:.3f}<br>"
293             f"stdev [Mpps]: {stdevs_mpps[idx]:.3f}"
294         )
295         trend_hover_text.append(trend_hover_str)
296
297     trace_trend = plgo.Scatter(
298         x=xaxis,
299         y=avgs_mpps,
300         mode=u"lines",
301         line={
302             u"shape": u"linear",
303             u"width": 1,
304             u"color": color,
305         },
306         showlegend=False,
307         legendgroup=name,
308         name=f"{name}",
309         text=trend_hover_text,
310         hoverinfo=u"text+name"
311     )
312     traces.append(trace_trend)
313
314     trace_anomalies = plgo.Scatter(
315         x=list(anomalies.keys()),
316         y=anomalies_avgs,
317         mode=u"markers",
318         hoverinfo=u"none",
319         showlegend=False,
320         legendgroup=name,
321         name=f"{name}-anomalies",
322         marker={
323             u"size": 15,
324             u"symbol": u"circle-open",
325             u"color": anomalies_colors,
326             u"colorscale": [
327                 [0.00, u"red"],
328                 [0.33, u"red"],
329                 [0.33, u"white"],
330                 [0.66, u"white"],
331                 [0.66, u"green"],
332                 [1.00, u"green"]
333             ],
334             u"showscale": True,
335             u"line": {
336                 u"width": 2
337             },
338             u"colorbar": {
339                 u"y": 0.5,
340                 u"len": 0.8,
341                 u"title": u"Circles Marking Data Classification",
342                 u"titleside": u"right",
343                 u"titlefont": {
344                     u"size": 14
345                 },
346                 u"tickmode": u"array",
347                 u"tickvals": [0.167, 0.500, 0.833],
348                 u"ticktext": [u"Regression", u"Normal", u"Progression"],
349                 u"ticks": u"",
350                 u"ticklen": 0,
351                 u"tickangle": -90,
352                 u"thickness": 10
353             }
354         }
355     )
356     traces.append(trace_anomalies)
357
358     if anomaly_classification:
359         return traces, anomaly_classification[-1]
360
361     return traces, None
362
363
364 def _generate_all_charts(spec, input_data):
365     """Generate all charts specified in the specification file.
366
367     :param spec: Specification.
368     :param input_data: Full data set.
369     :type spec: Specification
370     :type input_data: InputData
371     """
372
373     def _generate_chart(graph):
374         """Generates the chart.
375
376         :param graph: The graph to be generated
377         :type graph: dict
378         :returns: Dictionary with the job name, csv table with results and
379             list of tests classification results.
380         :rtype: dict
381         """
382
383         logging.info(f"  Generating the chart {graph.get(u'title', u'')} ...")
384
385         incl_tests = graph.get(u"include-tests", u"MRR")
386
387         job_name = list(graph[u"data"].keys())[0]
388
389         csv_tbl = list()
390         res = dict()
391
392         # Transform the data
393         logging.info(
394             f"    Creating the data set for the {graph.get(u'type', u'')} "
395             f"{graph.get(u'title', u'')}."
396         )
397
398         if graph.get(u"include", None):
399             data = input_data.filter_tests_by_name(
400                 graph,
401                 params=[u"type", u"result", u"throughput", u"tags"],
402                 continue_on_error=True
403             )
404         else:
405             data = input_data.filter_data(
406                 graph,
407                 params=[u"type", u"result", u"throughput", u"tags"],
408                 continue_on_error=True)
409
410         if data is None or data.empty:
411             logging.error(u"No data.")
412             return dict()
413
414         chart_data = dict()
415         chart_tags = dict()
416         for job, job_data in data.items():
417             if job != job_name:
418                 continue
419             for index, bld in job_data.items():
420                 for test_name, test in bld.items():
421                     if chart_data.get(test_name, None) is None:
422                         chart_data[test_name] = OrderedDict()
423                     try:
424                         if incl_tests == u"MRR":
425                             rate = test[u"result"][u"receive-rate"]
426                             stdev = test[u"result"][u"receive-stdev"]
427                         elif incl_tests == u"NDR":
428                             rate = test[u"throughput"][u"NDR"][u"LOWER"]
429                             stdev = float(u"nan")
430                         elif incl_tests == u"PDR":
431                             rate = test[u"throughput"][u"PDR"][u"LOWER"]
432                             stdev = float(u"nan")
433                         else:
434                             continue
435                         chart_data[test_name][int(index)] = {
436                             u"receive-rate": rate,
437                             u"receive-stdev": stdev
438                         }
439                         chart_tags[test_name] = test.get(u"tags", None)
440                     except (KeyError, TypeError):
441                         pass
442
443         # Add items to the csv table:
444         for tst_name, tst_data in chart_data.items():
445             tst_lst = list()
446             for bld in builds_dict[job_name]:
447                 itm = tst_data.get(int(bld), dict())
448                 # CSIT-1180: Itm will be list, compute stats.
449                 try:
450                     tst_lst.append(str(itm.get(u"receive-rate", u"")))
451                 except AttributeError:
452                     tst_lst.append(u"")
453             csv_tbl.append(f"{tst_name}," + u",".join(tst_lst) + u'\n')
454
455         # Generate traces:
456         traces = list()
457         index = 0
458         groups = graph.get(u"groups", None)
459         visibility = list()
460
461         if groups:
462             for group in groups:
463                 visible = list()
464                 for tag in group:
465                     for tst_name, test_data in chart_data.items():
466                         if not test_data:
467                             logging.warning(f"No data for the test {tst_name}")
468                             continue
469                         if tag not in chart_tags[tst_name]:
470                             continue
471                         try:
472                             trace, rslt = _generate_trending_traces(
473                                 test_data,
474                                 job_name=job_name,
475                                 build_info=build_info,
476                                 name=u'-'.join(tst_name.split(u'.')[-1].
477                                                split(u'-')[2:-1]),
478                                 color=COLORS[index],
479                                 incl_tests=incl_tests
480                             )
481                         except IndexError:
482                             logging.error(f"Out of colors: index: "
483                                           f"{index}, test: {tst_name}")
484                             index += 1
485                             continue
486                         traces.extend(trace)
487                         visible.extend([True for _ in range(len(trace))])
488                         res[tst_name] = rslt
489                         index += 1
490                         break
491                 visibility.append(visible)
492         else:
493             for tst_name, test_data in chart_data.items():
494                 if not test_data:
495                     logging.warning(f"No data for the test {tst_name}")
496                     continue
497                 try:
498                     trace, rslt = _generate_trending_traces(
499                         test_data,
500                         job_name=job_name,
501                         build_info=build_info,
502                         name=u'-'.join(
503                             tst_name.split(u'.')[-1].split(u'-')[2:-1]),
504                         color=COLORS[index],
505                         incl_tests=incl_tests
506                     )
507                 except IndexError:
508                     logging.error(
509                         f"Out of colors: index: {index}, test: {tst_name}"
510                     )
511                     index += 1
512                     continue
513                 traces.extend(trace)
514                 res[tst_name] = rslt
515                 index += 1
516
517         if traces:
518             # Generate the chart:
519             try:
520                 layout = deepcopy(graph[u"layout"])
521             except KeyError as err:
522                 logging.error(u"Finished with error: No layout defined")
523                 logging.error(repr(err))
524                 return dict()
525             if groups:
526                 show = list()
527                 for i in range(len(visibility)):
528                     visible = list()
529                     for vis_idx, _ in enumerate(visibility):
530                         for _ in range(len(visibility[vis_idx])):
531                             visible.append(i == vis_idx)
532                     show.append(visible)
533
534                 buttons = list()
535                 buttons.append(dict(
536                     label=u"All",
537                     method=u"update",
538                     args=[{u"visible": [True for _ in range(len(show[0]))]}, ]
539                 ))
540                 for i in range(len(groups)):
541                     try:
542                         label = graph[u"group-names"][i]
543                     except (IndexError, KeyError):
544                         label = f"Group {i + 1}"
545                     buttons.append(dict(
546                         label=label,
547                         method=u"update",
548                         args=[{u"visible": show[i]}, ]
549                     ))
550
551                 layout[u"updatemenus"] = list([
552                     dict(
553                         active=0,
554                         type=u"dropdown",
555                         direction=u"down",
556                         xanchor=u"left",
557                         yanchor=u"bottom",
558                         x=-0.12,
559                         y=1.0,
560                         buttons=buttons
561                     )
562                 ])
563
564             name_file = (
565                 f"{spec.cpta[u'output-file']}/{graph[u'output-file-name']}"
566                 f"{spec.cpta[u'output-file-type']}")
567
568             logging.info(f"    Writing the file {name_file} ...")
569             plpl = plgo.Figure(data=traces, layout=layout)
570             try:
571                 ploff.plot(plpl, show_link=False, auto_open=False,
572                            filename=name_file)
573             except plerr.PlotlyEmptyDataError:
574                 logging.warning(u"No data for the plot. Skipped.")
575
576         return {u"job_name": job_name, u"csv_table": csv_tbl, u"results": res}
577
578     builds_dict = dict()
579     for job in spec.input[u"builds"].keys():
580         if builds_dict.get(job, None) is None:
581             builds_dict[job] = list()
582         for build in spec.input[u"builds"][job]:
583             status = build[u"status"]
584             if status not in (u"failed", u"not found", u"removed", None):
585                 builds_dict[job].append(str(build[u"build"]))
586
587     # Create "build ID": "date" dict:
588     build_info = dict()
589     tb_tbl = spec.environment.get(u"testbeds", None)
590     for job_name, job_data in builds_dict.items():
591         if build_info.get(job_name, None) is None:
592             build_info[job_name] = OrderedDict()
593         for build in job_data:
594             testbed = u""
595             tb_ip = input_data.metadata(job_name, build).get(u"testbed", u"")
596             if tb_ip and tb_tbl:
597                 testbed = tb_tbl.get(tb_ip, u"")
598             build_info[job_name][build] = (
599                 input_data.metadata(job_name, build).get(u"generated", u""),
600                 input_data.metadata(job_name, build).get(u"version", u""),
601                 testbed
602             )
603
604     anomaly_classifications = dict()
605
606     # Create the table header:
607     csv_tables = dict()
608     for job_name in builds_dict:
609         if csv_tables.get(job_name, None) is None:
610             csv_tables[job_name] = list()
611         header = f"Build Number:,{u','.join(builds_dict[job_name])}\n"
612         csv_tables[job_name].append(header)
613         build_dates = [x[0] for x in build_info[job_name].values()]
614         header = f"Build Date:,{u','.join(build_dates)}\n"
615         csv_tables[job_name].append(header)
616         versions = [x[1] for x in build_info[job_name].values()]
617         header = f"Version:,{u','.join(versions)}\n"
618         csv_tables[job_name].append(header)
619
620     for chart in spec.cpta[u"plots"]:
621         result = _generate_chart(chart)
622         if not result:
623             continue
624
625         csv_tables[result[u"job_name"]].extend(result[u"csv_table"])
626
627         if anomaly_classifications.get(result[u"job_name"], None) is None:
628             anomaly_classifications[result[u"job_name"]] = dict()
629         anomaly_classifications[result[u"job_name"]].update(result[u"results"])
630
631     # Write the tables:
632     for job_name, csv_table in csv_tables.items():
633         file_name = f"{spec.cpta[u'output-file']}/{job_name}-trending"
634         with open(f"{file_name}.csv", u"wt") as file_handler:
635             file_handler.writelines(csv_table)
636
637         txt_table = None
638         with open(f"{file_name}.csv", u"rt") as csv_file:
639             csv_content = csv.reader(csv_file, delimiter=u',', quotechar=u'"')
640             line_nr = 0
641             for row in csv_content:
642                 if txt_table is None:
643                     txt_table = prettytable.PrettyTable(row)
644                 else:
645                     if line_nr > 1:
646                         for idx, item in enumerate(row):
647                             try:
648                                 row[idx] = str(round(float(item) / 1000000, 2))
649                             except ValueError:
650                                 pass
651                     try:
652                         txt_table.add_row(row)
653                     # PrettyTable raises Exception
654                     except Exception as err:
655                         logging.warning(
656                             f"Error occurred while generating TXT table:\n{err}"
657                         )
658                 line_nr += 1
659             txt_table.align[u"Build Number:"] = u"l"
660         with open(f"{file_name}.txt", u"wt") as txt_file:
661             txt_file.write(str(txt_table))
662
663     # Evaluate result:
664     if anomaly_classifications:
665         result = u"PASS"
666         for job_name, job_data in anomaly_classifications.items():
667             file_name = \
668                 f"{spec.cpta[u'output-file']}/regressions-{job_name}.txt"
669             with open(file_name, u'w') as txt_file:
670                 for test_name, classification in job_data.items():
671                     if classification == u"regression":
672                         txt_file.write(test_name + u'\n')
673                     if classification in (u"regression", u"outlier"):
674                         result = u"FAIL"
675             file_name = \
676                 f"{spec.cpta[u'output-file']}/progressions-{job_name}.txt"
677             with open(file_name, u'w') as txt_file:
678                 for test_name, classification in job_data.items():
679                     if classification == u"progression":
680                         txt_file.write(test_name + u'\n')
681     else:
682         result = u"FAIL"
683
684     logging.info(f"Partial results: {anomaly_classifications}")
685     logging.info(f"Result: {result}")
686
687     return result