Report: Fix bugs in NAT44 graphs
[csit.git] / resources / tools / presentation / generator_plots.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 """Algorithms to generate plots.
15 """
16
17
18 import re
19 import logging
20
21 from collections import OrderedDict
22 from copy import deepcopy
23
24 import hdrh.histogram
25 import hdrh.codec
26 import pandas as pd
27 import plotly.offline as ploff
28 import plotly.graph_objs as plgo
29
30 from plotly.exceptions import PlotlyError
31
32 from pal_utils import mean, stdev
33
34
35 COLORS = (
36     u"#1A1110",
37     u"#DA2647",
38     u"#214FC6",
39     u"#01786F",
40     u"#BD8260",
41     u"#FFD12A",
42     u"#A6E7FF",
43     u"#738276",
44     u"#C95A49",
45     u"#FC5A8D",
46     u"#CEC8EF",
47     u"#391285",
48     u"#6F2DA8",
49     u"#FF878D",
50     u"#45A27D",
51     u"#FFD0B9",
52     u"#FD5240",
53     u"#DB91EF",
54     u"#44D7A8",
55     u"#4F86F7",
56     u"#84DE02",
57     u"#FFCFF1",
58     u"#614051"
59 )
60
61 REGEX_NIC = re.compile(r'(\d*ge\dp\d\D*\d*[a-z]*)-')
62
63
64 def generate_plots(spec, data):
65     """Generate all plots specified in the specification file.
66
67     :param spec: Specification read from the specification file.
68     :param data: Data to process.
69     :type spec: Specification
70     :type data: InputData
71     """
72
73     generator = {
74         u"plot_nf_reconf_box_name": plot_nf_reconf_box_name,
75         u"plot_perf_box_name": plot_perf_box_name,
76         u"plot_tsa_name": plot_tsa_name,
77         u"plot_http_server_perf_box": plot_http_server_perf_box,
78         u"plot_nf_heatmap": plot_nf_heatmap,
79         u"plot_hdrh_lat_by_percentile": plot_hdrh_lat_by_percentile
80     }
81
82     logging.info(u"Generating the plots ...")
83     for index, plot in enumerate(spec.plots):
84         try:
85             logging.info(f"  Plot nr {index + 1}: {plot.get(u'title', u'')}")
86             plot[u"limits"] = spec.configuration[u"limits"]
87             generator[plot[u"algorithm"]](plot, data)
88             logging.info(u"  Done.")
89         except NameError as err:
90             logging.error(
91                 f"Probably algorithm {plot[u'algorithm']} is not defined: "
92                 f"{repr(err)}"
93             )
94     logging.info(u"Done.")
95
96
97 def plot_hdrh_lat_by_percentile(plot, input_data):
98     """Generate the plot(s) with algorithm: plot_hdrh_lat_by_percentile
99     specified in the specification file.
100
101     :param plot: Plot to generate.
102     :param input_data: Data to process.
103     :type plot: pandas.Series
104     :type input_data: InputData
105     """
106
107     # Transform the data
108     logging.info(
109         f"    Creating the data set for the {plot.get(u'type', u'')} "
110         f"{plot.get(u'title', u'')}."
111     )
112     if plot.get(u"include", None):
113         data = input_data.filter_tests_by_name(
114             plot,
115             params=[u"name", u"latency", u"parent", u"tags", u"type"]
116         )[0][0]
117     elif plot.get(u"filter", None):
118         data = input_data.filter_data(
119             plot,
120             params=[u"name", u"latency", u"parent", u"tags", u"type"],
121             continue_on_error=True
122         )[0][0]
123     else:
124         job = list(plot[u"data"].keys())[0]
125         build = str(plot[u"data"][job][0])
126         data = input_data.tests(job, build)
127
128     if data is None or len(data) == 0:
129         logging.error(u"No data.")
130         return
131
132     desc = {
133         u"LAT0": u"No-load.",
134         u"PDR10": u"Low-load, 10% PDR.",
135         u"PDR50": u"Mid-load, 50% PDR.",
136         u"PDR90": u"High-load, 90% PDR.",
137         u"PDR": u"Full-load, 100% PDR.",
138         u"NDR10": u"Low-load, 10% NDR.",
139         u"NDR50": u"Mid-load, 50% NDR.",
140         u"NDR90": u"High-load, 90% NDR.",
141         u"NDR": u"Full-load, 100% NDR."
142     }
143
144     graphs = [
145         u"LAT0",
146         u"PDR10",
147         u"PDR50",
148         u"PDR90"
149     ]
150
151     file_links = plot.get(u"output-file-links", None)
152     target_links = plot.get(u"target-links", None)
153
154     for test in data:
155         try:
156             if test[u"type"] not in (u"NDRPDR",):
157                 logging.warning(f"Invalid test type: {test[u'type']}")
158                 continue
159             name = re.sub(REGEX_NIC, u"", test[u"parent"].
160                           replace(u'-ndrpdr', u'').replace(u'2n1l-', u''))
161             try:
162                 nic = re.search(REGEX_NIC, test[u"parent"]).group(1)
163             except (IndexError, AttributeError, KeyError, ValueError):
164                 nic = u""
165             name_link = f"{nic}-{test[u'name']}".replace(u'-ndrpdr', u'')
166
167             logging.info(f"    Generating the graph: {name_link}")
168
169             fig = plgo.Figure()
170             layout = deepcopy(plot[u"layout"])
171
172             for color, graph in enumerate(graphs):
173                 for idx, direction in enumerate((u"direction1", u"direction2")):
174                     xaxis = [0.0, ]
175                     yaxis = [0.0, ]
176                     hovertext = [
177                         f"<b>{desc[graph]}</b><br>"
178                         f"Direction: {(u'W-E', u'E-W')[idx % 2]}<br>"
179                         f"Percentile: 0.0%<br>"
180                         f"Latency: 0.0uSec"
181                     ]
182                     try:
183                         decoded = hdrh.histogram.HdrHistogram.decode(
184                             test[u"latency"][graph][direction][u"hdrh"]
185                         )
186                     except hdrh.codec.HdrLengthException:
187                         logging.warning(
188                             f"No data for direction {(u'W-E', u'E-W')[idx % 2]}"
189                         )
190                         continue
191
192                     for item in decoded.get_recorded_iterator():
193                         percentile = item.percentile_level_iterated_to
194                         if percentile > 99.9:
195                             continue
196                         xaxis.append(percentile)
197                         yaxis.append(item.value_iterated_to)
198                         hovertext.append(
199                             f"<b>{desc[graph]}</b><br>"
200                             f"Direction: {(u'W-E', u'E-W')[idx % 2]}<br>"
201                             f"Percentile: {percentile:.5f}%<br>"
202                             f"Latency: {item.value_iterated_to}uSec"
203                         )
204                     fig.add_trace(
205                         plgo.Scatter(
206                             x=xaxis,
207                             y=yaxis,
208                             name=desc[graph],
209                             mode=u"lines",
210                             legendgroup=desc[graph],
211                             showlegend=bool(idx),
212                             line=dict(
213                                 color=COLORS[color],
214                                 dash=u"dash" if idx % 2 else u"solid"
215                             ),
216                             hovertext=hovertext,
217                             hoverinfo=u"text"
218                         )
219                     )
220
221             layout[u"title"][u"text"] = f"<b>Latency:</b> {name}"
222             fig.update_layout(layout)
223
224             # Create plot
225             file_name = f"{plot[u'output-file']}-{name_link}.html"
226             logging.info(f"    Writing file {file_name}")
227
228             try:
229                 # Export Plot
230                 ploff.plot(fig, show_link=False, auto_open=False,
231                            filename=file_name)
232                 # Add link to the file:
233                 if file_links and target_links:
234                     with open(file_links, u"a") as file_handler:
235                         file_handler.write(
236                             f"- `{name_link} "
237                             f"<{target_links}/{file_name.split(u'/')[-1]}>`_\n"
238                         )
239             except FileNotFoundError as err:
240                 logging.error(
241                     f"Not possible to write the link to the file "
242                     f"{file_links}\n{err}"
243                 )
244             except PlotlyError as err:
245                 logging.error(f"   Finished with error: {repr(err)}")
246
247         except hdrh.codec.HdrLengthException as err:
248             logging.warning(repr(err))
249             continue
250
251         except (ValueError, KeyError) as err:
252             logging.warning(repr(err))
253             continue
254
255
256 def plot_nf_reconf_box_name(plot, input_data):
257     """Generate the plot(s) with algorithm: plot_nf_reconf_box_name
258     specified in the specification file.
259
260     :param plot: Plot to generate.
261     :param input_data: Data to process.
262     :type plot: pandas.Series
263     :type input_data: InputData
264     """
265
266     # Transform the data
267     logging.info(
268         f"    Creating the data set for the {plot.get(u'type', u'')} "
269         f"{plot.get(u'title', u'')}."
270     )
271     data = input_data.filter_tests_by_name(
272         plot, params=[u"result", u"parent", u"tags", u"type"]
273     )
274     if data is None:
275         logging.error(u"No data.")
276         return
277
278     # Prepare the data for the plot
279     y_vals = OrderedDict()
280     loss = dict()
281     for job in data:
282         for build in job:
283             for test in build:
284                 if y_vals.get(test[u"parent"], None) is None:
285                     y_vals[test[u"parent"]] = list()
286                     loss[test[u"parent"]] = list()
287                 try:
288                     y_vals[test[u"parent"]].append(test[u"result"][u"time"])
289                     loss[test[u"parent"]].append(test[u"result"][u"loss"])
290                 except (KeyError, TypeError):
291                     y_vals[test[u"parent"]].append(None)
292
293     # Add None to the lists with missing data
294     max_len = 0
295     nr_of_samples = list()
296     for val in y_vals.values():
297         if len(val) > max_len:
298             max_len = len(val)
299         nr_of_samples.append(len(val))
300     for val in y_vals.values():
301         if len(val) < max_len:
302             val.extend([None for _ in range(max_len - len(val))])
303
304     # Add plot traces
305     traces = list()
306     df_y = pd.DataFrame(y_vals)
307     df_y.head()
308     for i, col in enumerate(df_y.columns):
309         tst_name = re.sub(REGEX_NIC, u"",
310                           col.lower().replace(u'-ndrpdr', u'').
311                           replace(u'2n1l-', u''))
312
313         traces.append(plgo.Box(
314             x=[str(i + 1) + u'.'] * len(df_y[col]),
315             y=[y if y else None for y in df_y[col]],
316             name=(
317                 f"{i + 1}. "
318                 f"({nr_of_samples[i]:02d} "
319                 f"run{u's' if nr_of_samples[i] > 1 else u''}, "
320                 f"packets lost average: {mean(loss[col]):.1f}) "
321                 f"{u'-'.join(tst_name.split(u'-')[3:-2])}"
322             ),
323             hoverinfo=u"y+name"
324         ))
325     try:
326         # Create plot
327         layout = deepcopy(plot[u"layout"])
328         layout[u"title"] = f"<b>Time Lost:</b> {layout[u'title']}"
329         layout[u"yaxis"][u"title"] = u"<b>Effective Blocked Time [s]</b>"
330         layout[u"legend"][u"font"][u"size"] = 14
331         layout[u"yaxis"].pop(u"range")
332         plpl = plgo.Figure(data=traces, layout=layout)
333
334         # Export Plot
335         file_type = plot.get(u"output-file-type", u".html")
336         logging.info(f"    Writing file {plot[u'output-file']}{file_type}.")
337         ploff.plot(
338             plpl,
339             show_link=False,
340             auto_open=False,
341             filename=f"{plot[u'output-file']}{file_type}"
342         )
343     except PlotlyError as err:
344         logging.error(
345             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
346         )
347         return
348
349
350 def plot_perf_box_name(plot, input_data):
351     """Generate the plot(s) with algorithm: plot_perf_box_name
352     specified in the specification file.
353
354     :param plot: Plot to generate.
355     :param input_data: Data to process.
356     :type plot: pandas.Series
357     :type input_data: InputData
358     """
359
360     # Transform the data
361     logging.info(
362         f"    Creating data set for the {plot.get(u'type', u'')} "
363         f"{plot.get(u'title', u'')}."
364     )
365     data = input_data.filter_tests_by_name(
366         plot,
367         params=[u"throughput", u"gbps", u"result", u"parent", u"tags", u"type"])
368     if data is None:
369         logging.error(u"No data.")
370         return
371
372     # Prepare the data for the plot
373     plot_title = plot.get(u"title", u"").lower()
374
375     if u"-gbps" in plot_title:
376         value = u"gbps"
377         multiplier = 1e6
378     else:
379         value = u"throughput"
380         multiplier = 1.0
381     y_vals = OrderedDict()
382     test_type = u""
383     for job in data:
384         for build in job:
385             for test in build:
386                 if y_vals.get(test[u"parent"], None) is None:
387                     y_vals[test[u"parent"]] = list()
388                 try:
389                     if test[u"type"] in (u"NDRPDR", u"CPS"):
390                         test_type = test[u"type"]
391
392                         if u"-pdr" in plot_title:
393                             ttype = u"PDR"
394                         elif u"-ndr" in plot_title:
395                             ttype = u"NDR"
396                         else:
397                             raise RuntimeError(
398                                 u"Wrong title. No information about test type. "
399                                 u"Add '-ndr' or '-pdr' to the test title."
400                             )
401
402                         y_vals[test[u"parent"]].append(
403                             test[value][ttype][u"LOWER"] * multiplier
404                         )
405
406                     elif test[u"type"] in (u"SOAK", ):
407                         y_vals[test[u"parent"]].\
408                             append(test[u"throughput"][u"LOWER"])
409                         test_type = u"SOAK"
410
411                     elif test[u"type"] in (u"HOSTSTACK", ):
412                         if u"LDPRELOAD" in test[u"tags"]:
413                             y_vals[test[u"parent"]].append(
414                                 float(test[u"result"][u"bits_per_second"]) / 1e3
415                             )
416                         elif u"VPPECHO" in test[u"tags"]:
417                             y_vals[test[u"parent"]].append(
418                                 (float(test[u"result"][u"client"][u"tx_data"])
419                                  * 8 / 1e3) /
420                                 ((float(test[u"result"][u"client"][u"time"]) +
421                                   float(test[u"result"][u"server"][u"time"])) /
422                                  2)
423                             )
424                         test_type = u"HOSTSTACK"
425
426                     else:
427                         continue
428
429                 except (KeyError, TypeError):
430                     y_vals[test[u"parent"]].append(None)
431
432     # Add None to the lists with missing data
433     max_len = 0
434     nr_of_samples = list()
435     for val in y_vals.values():
436         if len(val) > max_len:
437             max_len = len(val)
438         nr_of_samples.append(len(val))
439     for val in y_vals.values():
440         if len(val) < max_len:
441             val.extend([None for _ in range(max_len - len(val))])
442
443     # Add plot traces
444     traces = list()
445     df_y = pd.DataFrame(y_vals)
446     df_y.head()
447     y_max = list()
448     for i, col in enumerate(df_y.columns):
449         tst_name = re.sub(REGEX_NIC, u"",
450                           col.lower().replace(u'-ndrpdr', u'').
451                           replace(u'2n1l-', u''))
452         kwargs = dict(
453             x=[str(i + 1) + u'.'] * len(df_y[col]),
454             y=[y / 1e6 if y else None for y in df_y[col]],
455             name=(
456                 f"{i + 1}. "
457                 f"({nr_of_samples[i]:02d} "
458                 f"run{u's' if nr_of_samples[i] > 1 else u''}) "
459                 f"{tst_name}"
460             ),
461             hoverinfo=u"y+name"
462         )
463         if test_type in (u"SOAK", ):
464             kwargs[u"boxpoints"] = u"all"
465
466         traces.append(plgo.Box(**kwargs))
467
468         try:
469             val_max = max(df_y[col])
470             if val_max:
471                 y_max.append(int(val_max / 1e6) + 2)
472         except (ValueError, TypeError) as err:
473             logging.error(repr(err))
474             continue
475
476     try:
477         # Create plot
478         layout = deepcopy(plot[u"layout"])
479         if layout.get(u"title", None):
480             if test_type in (u"HOSTSTACK", ):
481                 layout[u"title"] = f"<b>Bandwidth:</b> {layout[u'title']}"
482             elif test_type in (u"CPS", ):
483                 layout[u"title"] = f"<b>CPS:</b> {layout[u'title']}"
484             else:
485                 layout[u"title"] = f"<b>Throughput:</b> {layout[u'title']}"
486         if y_max:
487             layout[u"yaxis"][u"range"] = [0, max(y_max)]
488         plpl = plgo.Figure(data=traces, layout=layout)
489
490         # Export Plot
491         logging.info(f"    Writing file {plot[u'output-file']}.html.")
492         ploff.plot(
493             plpl,
494             show_link=False,
495             auto_open=False,
496             filename=f"{plot[u'output-file']}.html"
497         )
498     except PlotlyError as err:
499         logging.error(
500             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
501         )
502         return
503
504
505 def plot_tsa_name(plot, input_data):
506     """Generate the plot(s) with algorithm:
507     plot_tsa_name
508     specified in the specification file.
509
510     :param plot: Plot to generate.
511     :param input_data: Data to process.
512     :type plot: pandas.Series
513     :type input_data: InputData
514     """
515
516     # Transform the data
517     plot_title = plot.get(u"title", u"")
518     logging.info(
519         f"    Creating data set for the {plot.get(u'type', u'')} {plot_title}."
520     )
521     data = input_data.filter_tests_by_name(
522         plot,
523         params=[u"throughput", u"gbps", u"parent", u"tags", u"type"]
524     )
525     if data is None:
526         logging.error(u"No data.")
527         return
528
529     plot_title = plot_title.lower()
530
531     if u"-gbps" in plot_title:
532         value = u"gbps"
533         h_unit = u"Gbps"
534         multiplier = 1e6
535     else:
536         value = u"throughput"
537         h_unit = u"Mpps"
538         multiplier = 1.0
539
540     y_vals = OrderedDict()
541     for job in data:
542         for build in job:
543             for test in build:
544                 if y_vals.get(test[u"parent"], None) is None:
545                     y_vals[test[u"parent"]] = {
546                         u"1": list(),
547                         u"2": list(),
548                         u"4": list()
549                     }
550                 try:
551                     if test[u"type"] not in (u"NDRPDR", u"CPS"):
552                         continue
553
554                     if u"-pdr" in plot_title:
555                         ttype = u"PDR"
556                     elif u"-ndr" in plot_title:
557                         ttype = u"NDR"
558                     else:
559                         continue
560
561                     if u"1C" in test[u"tags"]:
562                         y_vals[test[u"parent"]][u"1"]. \
563                             append(test[value][ttype][u"LOWER"] * multiplier)
564                     elif u"2C" in test[u"tags"]:
565                         y_vals[test[u"parent"]][u"2"]. \
566                             append(test[value][ttype][u"LOWER"] * multiplier)
567                     elif u"4C" in test[u"tags"]:
568                         y_vals[test[u"parent"]][u"4"]. \
569                             append(test[value][ttype][u"LOWER"] * multiplier)
570                 except (KeyError, TypeError):
571                     pass
572
573     if not y_vals:
574         logging.warning(f"No data for the plot {plot.get(u'title', u'')}")
575         return
576
577     y_1c_max = dict()
578     for test_name, test_vals in y_vals.items():
579         for key, test_val in test_vals.items():
580             if test_val:
581                 avg_val = sum(test_val) / len(test_val)
582                 y_vals[test_name][key] = [avg_val, len(test_val)]
583                 ideal = avg_val / (int(key) * 1e6)
584                 if test_name not in y_1c_max or ideal > y_1c_max[test_name]:
585                     y_1c_max[test_name] = ideal
586
587     vals = OrderedDict()
588     y_max = list()
589     nic_limit = 0
590     lnk_limit = 0
591     pci_limit = 0
592     for test_name, test_vals in y_vals.items():
593         try:
594             if test_vals[u"1"][1]:
595                 name = re.sub(
596                     REGEX_NIC,
597                     u"",
598                     test_name.replace(u'-ndrpdr', u'').replace(u'2n1l-', u'')
599                 )
600                 vals[name] = OrderedDict()
601                 y_val_1 = test_vals[u"1"][0] / 1e6
602                 y_val_2 = test_vals[u"2"][0] / 1e6 if test_vals[u"2"][0] \
603                     else None
604                 y_val_4 = test_vals[u"4"][0] / 1e6 if test_vals[u"4"][0] \
605                     else None
606
607                 vals[name][u"val"] = [y_val_1, y_val_2, y_val_4]
608                 vals[name][u"rel"] = [1.0, None, None]
609                 vals[name][u"ideal"] = [
610                     y_1c_max[test_name],
611                     y_1c_max[test_name] * 2,
612                     y_1c_max[test_name] * 4
613                 ]
614                 vals[name][u"diff"] = [
615                     (y_val_1 - y_1c_max[test_name]) * 100 / y_val_1, None, None
616                 ]
617                 vals[name][u"count"] = [
618                     test_vals[u"1"][1],
619                     test_vals[u"2"][1],
620                     test_vals[u"4"][1]
621                 ]
622
623                 try:
624                     val_max = max(vals[name][u"val"])
625                 except ValueError as err:
626                     logging.error(repr(err))
627                     continue
628                 if val_max:
629                     y_max.append(val_max)
630
631                 if y_val_2:
632                     vals[name][u"rel"][1] = round(y_val_2 / y_val_1, 2)
633                     vals[name][u"diff"][1] = \
634                         (y_val_2 - vals[name][u"ideal"][1]) * 100 / y_val_2
635                 if y_val_4:
636                     vals[name][u"rel"][2] = round(y_val_4 / y_val_1, 2)
637                     vals[name][u"diff"][2] = \
638                         (y_val_4 - vals[name][u"ideal"][2]) * 100 / y_val_4
639         except IndexError as err:
640             logging.warning(f"No data for {test_name}")
641             logging.warning(repr(err))
642
643         # Limits:
644         if u"x520" in test_name:
645             limit = plot[u"limits"][u"nic"][u"x520"]
646         elif u"x710" in test_name:
647             limit = plot[u"limits"][u"nic"][u"x710"]
648         elif u"xxv710" in test_name:
649             limit = plot[u"limits"][u"nic"][u"xxv710"]
650         elif u"xl710" in test_name:
651             limit = plot[u"limits"][u"nic"][u"xl710"]
652         elif u"x553" in test_name:
653             limit = plot[u"limits"][u"nic"][u"x553"]
654         elif u"cx556a" in test_name:
655             limit = plot[u"limits"][u"nic"][u"cx556a"]
656         else:
657             limit = 0
658         if limit > nic_limit:
659             nic_limit = limit
660
661         mul = 2 if u"ge2p" in test_name else 1
662         if u"10ge" in test_name:
663             limit = plot[u"limits"][u"link"][u"10ge"] * mul
664         elif u"25ge" in test_name:
665             limit = plot[u"limits"][u"link"][u"25ge"] * mul
666         elif u"40ge" in test_name:
667             limit = plot[u"limits"][u"link"][u"40ge"] * mul
668         elif u"100ge" in test_name:
669             limit = plot[u"limits"][u"link"][u"100ge"] * mul
670         else:
671             limit = 0
672         if limit > lnk_limit:
673             lnk_limit = limit
674
675         if u"cx556a" in test_name:
676             limit = plot[u"limits"][u"pci"][u"pci-g3-x8"]
677         else:
678             limit = plot[u"limits"][u"pci"][u"pci-g3-x16"]
679         if limit > pci_limit:
680             pci_limit = limit
681
682     traces = list()
683     annotations = list()
684     x_vals = [1, 2, 4]
685
686     # Limits:
687     if u"-gbps" not in plot_title and u"-cps-" not in plot_title:
688         nic_limit /= 1e6
689         lnk_limit /= 1e6
690         pci_limit /= 1e6
691         min_limit = min((nic_limit, lnk_limit, pci_limit))
692         if nic_limit == min_limit:
693             traces.append(plgo.Scatter(
694                 x=x_vals,
695                 y=[nic_limit, ] * len(x_vals),
696                 name=f"NIC: {nic_limit:.2f}Mpps",
697                 showlegend=False,
698                 mode=u"lines",
699                 line=dict(
700                     dash=u"dot",
701                     color=COLORS[-1],
702                     width=1),
703                 hoverinfo=u"none"
704             ))
705             annotations.append(dict(
706                 x=1,
707                 y=nic_limit,
708                 xref=u"x",
709                 yref=u"y",
710                 xanchor=u"left",
711                 yanchor=u"bottom",
712                 text=f"NIC: {nic_limit:.2f}Mpps",
713                 font=dict(
714                     size=14,
715                     color=COLORS[-1],
716                 ),
717                 align=u"left",
718                 showarrow=False
719             ))
720             y_max.append(nic_limit)
721         elif lnk_limit == min_limit:
722             traces.append(plgo.Scatter(
723                 x=x_vals,
724                 y=[lnk_limit, ] * len(x_vals),
725                 name=f"Link: {lnk_limit:.2f}Mpps",
726                 showlegend=False,
727                 mode=u"lines",
728                 line=dict(
729                     dash=u"dot",
730                     color=COLORS[-1],
731                     width=1),
732                 hoverinfo=u"none"
733             ))
734             annotations.append(dict(
735                 x=1,
736                 y=lnk_limit,
737                 xref=u"x",
738                 yref=u"y",
739                 xanchor=u"left",
740                 yanchor=u"bottom",
741                 text=f"Link: {lnk_limit:.2f}Mpps",
742                 font=dict(
743                     size=14,
744                     color=COLORS[-1],
745                 ),
746                 align=u"left",
747                 showarrow=False
748             ))
749             y_max.append(lnk_limit)
750         elif pci_limit == min_limit:
751             traces.append(plgo.Scatter(
752                 x=x_vals,
753                 y=[pci_limit, ] * len(x_vals),
754                 name=f"PCIe: {pci_limit:.2f}Mpps",
755                 showlegend=False,
756                 mode=u"lines",
757                 line=dict(
758                     dash=u"dot",
759                     color=COLORS[-1],
760                     width=1),
761                 hoverinfo=u"none"
762             ))
763             annotations.append(dict(
764                 x=1,
765                 y=pci_limit,
766                 xref=u"x",
767                 yref=u"y",
768                 xanchor=u"left",
769                 yanchor=u"bottom",
770                 text=f"PCIe: {pci_limit:.2f}Mpps",
771                 font=dict(
772                     size=14,
773                     color=COLORS[-1],
774                 ),
775                 align=u"left",
776                 showarrow=False
777             ))
778             y_max.append(pci_limit)
779
780     # Perfect and measured:
781     cidx = 0
782     for name, val in vals.items():
783         hovertext = list()
784         try:
785             for idx in range(len(val[u"val"])):
786                 htext = ""
787                 if isinstance(val[u"val"][idx], float):
788                     htext += (
789                         f"No. of Runs: {val[u'count'][idx]}<br>"
790                         f"Mean: {val[u'val'][idx]:.2f}{h_unit}<br>"
791                     )
792                 if isinstance(val[u"diff"][idx], float):
793                     htext += f"Diff: {round(val[u'diff'][idx]):.0f}%<br>"
794                 if isinstance(val[u"rel"][idx], float):
795                     htext += f"Speedup: {val[u'rel'][idx]:.2f}"
796                 hovertext.append(htext)
797             traces.append(
798                 plgo.Scatter(
799                     x=x_vals,
800                     y=val[u"val"],
801                     name=name,
802                     legendgroup=name,
803                     mode=u"lines+markers",
804                     line=dict(
805                         color=COLORS[cidx],
806                         width=2),
807                     marker=dict(
808                         symbol=u"circle",
809                         size=10
810                     ),
811                     text=hovertext,
812                     hoverinfo=u"text+name"
813                 )
814             )
815             traces.append(
816                 plgo.Scatter(
817                     x=x_vals,
818                     y=val[u"ideal"],
819                     name=f"{name} perfect",
820                     legendgroup=name,
821                     showlegend=False,
822                     mode=u"lines",
823                     line=dict(
824                         color=COLORS[cidx],
825                         width=2,
826                         dash=u"dash"),
827                     text=[f"Perfect: {y:.2f}Mpps" for y in val[u"ideal"]],
828                     hoverinfo=u"text"
829                 )
830             )
831             cidx += 1
832         except (IndexError, ValueError, KeyError) as err:
833             logging.warning(f"No data for {name}\n{repr(err)}")
834
835     try:
836         # Create plot
837         file_type = plot.get(u"output-file-type", u".html")
838         logging.info(f"    Writing file {plot[u'output-file']}{file_type}.")
839         layout = deepcopy(plot[u"layout"])
840         if layout.get(u"title", None):
841             layout[u"title"] = f"<b>Speedup Multi-core:</b> {layout[u'title']}"
842         layout[u"yaxis"][u"range"] = [0, int(max(y_max) * 1.1)]
843         layout[u"annotations"].extend(annotations)
844         plpl = plgo.Figure(data=traces, layout=layout)
845
846         # Export Plot
847         ploff.plot(
848             plpl,
849             show_link=False,
850             auto_open=False,
851             filename=f"{plot[u'output-file']}{file_type}"
852         )
853     except PlotlyError as err:
854         logging.error(
855             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
856         )
857         return
858
859
860 def plot_http_server_perf_box(plot, input_data):
861     """Generate the plot(s) with algorithm: plot_http_server_perf_box
862     specified in the specification file.
863
864     :param plot: Plot to generate.
865     :param input_data: Data to process.
866     :type plot: pandas.Series
867     :type input_data: InputData
868     """
869
870     # Transform the data
871     logging.info(
872         f"    Creating the data set for the {plot.get(u'type', u'')} "
873         f"{plot.get(u'title', u'')}."
874     )
875     data = input_data.filter_data(plot)
876     if data is None:
877         logging.error(u"No data.")
878         return
879
880     # Prepare the data for the plot
881     y_vals = dict()
882     for job in data:
883         for build in job:
884             for test in build:
885                 if y_vals.get(test[u"name"], None) is None:
886                     y_vals[test[u"name"]] = list()
887                 try:
888                     y_vals[test[u"name"]].append(test[u"result"])
889                 except (KeyError, TypeError):
890                     y_vals[test[u"name"]].append(None)
891
892     # Add None to the lists with missing data
893     max_len = 0
894     nr_of_samples = list()
895     for val in y_vals.values():
896         if len(val) > max_len:
897             max_len = len(val)
898         nr_of_samples.append(len(val))
899     for val in y_vals.values():
900         if len(val) < max_len:
901             val.extend([None for _ in range(max_len - len(val))])
902
903     # Add plot traces
904     traces = list()
905     df_y = pd.DataFrame(y_vals)
906     df_y.head()
907     for i, col in enumerate(df_y.columns):
908         name = \
909             f"{i + 1}. " \
910             f"({nr_of_samples[i]:02d} " \
911             f"run{u's' if nr_of_samples[i] > 1 else u''}) " \
912             f"{col.lower().replace(u'-ndrpdr', u'')}"
913         if len(name) > 50:
914             name_lst = name.split(u'-')
915             name = u""
916             split_name = True
917             for segment in name_lst:
918                 if (len(name) + len(segment) + 1) > 50 and split_name:
919                     name += u"<br>    "
920                     split_name = False
921                 name += segment + u'-'
922             name = name[:-1]
923
924         traces.append(plgo.Box(x=[str(i + 1) + u'.'] * len(df_y[col]),
925                                y=df_y[col],
926                                name=name,
927                                **plot[u"traces"]))
928     try:
929         # Create plot
930         plpl = plgo.Figure(data=traces, layout=plot[u"layout"])
931
932         # Export Plot
933         logging.info(
934             f"    Writing file {plot[u'output-file']}"
935             f"{plot[u'output-file-type']}."
936         )
937         ploff.plot(
938             plpl,
939             show_link=False,
940             auto_open=False,
941             filename=f"{plot[u'output-file']}{plot[u'output-file-type']}"
942         )
943     except PlotlyError as err:
944         logging.error(
945             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
946         )
947         return
948
949
950 def plot_nf_heatmap(plot, input_data):
951     """Generate the plot(s) with algorithm: plot_nf_heatmap
952     specified in the specification file.
953
954     :param plot: Plot to generate.
955     :param input_data: Data to process.
956     :type plot: pandas.Series
957     :type input_data: InputData
958     """
959
960     regex_cn = re.compile(r'^(\d*)R(\d*)C$')
961     regex_test_name = re.compile(r'^.*-(\d+ch|\d+pl)-'
962                                  r'(\d+mif|\d+vh)-'
963                                  r'(\d+vm\d+t|\d+dcr\d+t|\d+dcr\d+c).*$')
964     vals = dict()
965
966     # Transform the data
967     logging.info(
968         f"    Creating the data set for the {plot.get(u'type', u'')} "
969         f"{plot.get(u'title', u'')}."
970     )
971     data = input_data.filter_data(plot, continue_on_error=True)
972     if data is None or data.empty:
973         logging.error(u"No data.")
974         return
975
976     for job in data:
977         for build in job:
978             for test in build:
979                 for tag in test[u"tags"]:
980                     groups = re.search(regex_cn, tag)
981                     if groups:
982                         chain = str(groups.group(1))
983                         node = str(groups.group(2))
984                         break
985                 else:
986                     continue
987                 groups = re.search(regex_test_name, test[u"name"])
988                 if groups and len(groups.groups()) == 3:
989                     hover_name = (
990                         f"{str(groups.group(1))}-"
991                         f"{str(groups.group(2))}-"
992                         f"{str(groups.group(3))}"
993                     )
994                 else:
995                     hover_name = u""
996                 if vals.get(chain, None) is None:
997                     vals[chain] = dict()
998                 if vals[chain].get(node, None) is None:
999                     vals[chain][node] = dict(
1000                         name=hover_name,
1001                         vals=list(),
1002                         nr=None,
1003                         mean=None,
1004                         stdev=None
1005                     )
1006                 try:
1007                     if plot[u"include-tests"] == u"MRR":
1008                         result = test[u"result"][u"receive-rate"]
1009                     elif plot[u"include-tests"] == u"PDR":
1010                         result = test[u"throughput"][u"PDR"][u"LOWER"]
1011                     elif plot[u"include-tests"] == u"NDR":
1012                         result = test[u"throughput"][u"NDR"][u"LOWER"]
1013                     else:
1014                         result = None
1015                 except TypeError:
1016                     result = None
1017
1018                 if result:
1019                     vals[chain][node][u"vals"].append(result)
1020
1021     if not vals:
1022         logging.error(u"No data.")
1023         return
1024
1025     txt_chains = list()
1026     txt_nodes = list()
1027     for key_c in vals:
1028         txt_chains.append(key_c)
1029         for key_n in vals[key_c].keys():
1030             txt_nodes.append(key_n)
1031             if vals[key_c][key_n][u"vals"]:
1032                 vals[key_c][key_n][u"nr"] = len(vals[key_c][key_n][u"vals"])
1033                 vals[key_c][key_n][u"mean"] = \
1034                     round(mean(vals[key_c][key_n][u"vals"]) / 1000000, 1)
1035                 vals[key_c][key_n][u"stdev"] = \
1036                     round(stdev(vals[key_c][key_n][u"vals"]) / 1000000, 1)
1037     txt_nodes = list(set(txt_nodes))
1038
1039     def sort_by_int(value):
1040         """Makes possible to sort a list of strings which represent integers.
1041
1042         :param value: Integer as a string.
1043         :type value: str
1044         :returns: Integer representation of input parameter 'value'.
1045         :rtype: int
1046         """
1047         return int(value)
1048
1049     txt_chains = sorted(txt_chains, key=sort_by_int)
1050     txt_nodes = sorted(txt_nodes, key=sort_by_int)
1051
1052     chains = [i + 1 for i in range(len(txt_chains))]
1053     nodes = [i + 1 for i in range(len(txt_nodes))]
1054
1055     data = [list() for _ in range(len(chains))]
1056     for chain in chains:
1057         for node in nodes:
1058             try:
1059                 val = vals[txt_chains[chain - 1]][txt_nodes[node - 1]][u"mean"]
1060             except (KeyError, IndexError):
1061                 val = None
1062             data[chain - 1].append(val)
1063
1064     # Color scales:
1065     my_green = [[0.0, u"rgb(235, 249, 242)"],
1066                 [1.0, u"rgb(45, 134, 89)"]]
1067
1068     my_blue = [[0.0, u"rgb(236, 242, 248)"],
1069                [1.0, u"rgb(57, 115, 172)"]]
1070
1071     my_grey = [[0.0, u"rgb(230, 230, 230)"],
1072                [1.0, u"rgb(102, 102, 102)"]]
1073
1074     hovertext = list()
1075     annotations = list()
1076
1077     text = (u"Test: {name}<br>"
1078             u"Runs: {nr}<br>"
1079             u"Thput: {val}<br>"
1080             u"StDev: {stdev}")
1081
1082     for chain, _ in enumerate(txt_chains):
1083         hover_line = list()
1084         for node, _ in enumerate(txt_nodes):
1085             if data[chain][node] is not None:
1086                 annotations.append(
1087                     dict(
1088                         x=node+1,
1089                         y=chain+1,
1090                         xref=u"x",
1091                         yref=u"y",
1092                         xanchor=u"center",
1093                         yanchor=u"middle",
1094                         text=str(data[chain][node]),
1095                         font=dict(
1096                             size=14,
1097                         ),
1098                         align=u"center",
1099                         showarrow=False
1100                     )
1101                 )
1102                 hover_line.append(text.format(
1103                     name=vals[txt_chains[chain]][txt_nodes[node]][u"name"],
1104                     nr=vals[txt_chains[chain]][txt_nodes[node]][u"nr"],
1105                     val=data[chain][node],
1106                     stdev=vals[txt_chains[chain]][txt_nodes[node]][u"stdev"]))
1107         hovertext.append(hover_line)
1108
1109     traces = [
1110         plgo.Heatmap(
1111             x=nodes,
1112             y=chains,
1113             z=data,
1114             colorbar=dict(
1115                 title=plot.get(u"z-axis", u""),
1116                 titleside=u"right",
1117                 titlefont=dict(
1118                     size=16
1119                 ),
1120                 tickfont=dict(
1121                     size=16,
1122                 ),
1123                 tickformat=u".1f",
1124                 yanchor=u"bottom",
1125                 y=-0.02,
1126                 len=0.925,
1127             ),
1128             showscale=True,
1129             colorscale=my_green,
1130             text=hovertext,
1131             hoverinfo=u"text"
1132         )
1133     ]
1134
1135     for idx, item in enumerate(txt_nodes):
1136         # X-axis, numbers:
1137         annotations.append(
1138             dict(
1139                 x=idx+1,
1140                 y=0.05,
1141                 xref=u"x",
1142                 yref=u"y",
1143                 xanchor=u"center",
1144                 yanchor=u"top",
1145                 text=item,
1146                 font=dict(
1147                     size=16,
1148                 ),
1149                 align=u"center",
1150                 showarrow=False
1151             )
1152         )
1153     for idx, item in enumerate(txt_chains):
1154         # Y-axis, numbers:
1155         annotations.append(
1156             dict(
1157                 x=0.35,
1158                 y=idx+1,
1159                 xref=u"x",
1160                 yref=u"y",
1161                 xanchor=u"right",
1162                 yanchor=u"middle",
1163                 text=item,
1164                 font=dict(
1165                     size=16,
1166                 ),
1167                 align=u"center",
1168                 showarrow=False
1169             )
1170         )
1171     # X-axis, title:
1172     annotations.append(
1173         dict(
1174             x=0.55,
1175             y=-0.15,
1176             xref=u"paper",
1177             yref=u"y",
1178             xanchor=u"center",
1179             yanchor=u"bottom",
1180             text=plot.get(u"x-axis", u""),
1181             font=dict(
1182                 size=16,
1183             ),
1184             align=u"center",
1185             showarrow=False
1186         )
1187     )
1188     # Y-axis, title:
1189     annotations.append(
1190         dict(
1191             x=-0.1,
1192             y=0.5,
1193             xref=u"x",
1194             yref=u"paper",
1195             xanchor=u"center",
1196             yanchor=u"middle",
1197             text=plot.get(u"y-axis", u""),
1198             font=dict(
1199                 size=16,
1200             ),
1201             align=u"center",
1202             textangle=270,
1203             showarrow=False
1204         )
1205     )
1206     updatemenus = list([
1207         dict(
1208             x=1.0,
1209             y=0.0,
1210             xanchor=u"right",
1211             yanchor=u"bottom",
1212             direction=u"up",
1213             buttons=list([
1214                 dict(
1215                     args=[
1216                         {
1217                             u"colorscale": [my_green, ],
1218                             u"reversescale": False
1219                         }
1220                     ],
1221                     label=u"Green",
1222                     method=u"update"
1223                 ),
1224                 dict(
1225                     args=[
1226                         {
1227                             u"colorscale": [my_blue, ],
1228                             u"reversescale": False
1229                         }
1230                     ],
1231                     label=u"Blue",
1232                     method=u"update"
1233                 ),
1234                 dict(
1235                     args=[
1236                         {
1237                             u"colorscale": [my_grey, ],
1238                             u"reversescale": False
1239                         }
1240                     ],
1241                     label=u"Grey",
1242                     method=u"update"
1243                 )
1244             ])
1245         )
1246     ])
1247
1248     try:
1249         layout = deepcopy(plot[u"layout"])
1250     except KeyError as err:
1251         logging.error(f"Finished with error: No layout defined\n{repr(err)}")
1252         return
1253
1254     layout[u"annotations"] = annotations
1255     layout[u'updatemenus'] = updatemenus
1256
1257     try:
1258         # Create plot
1259         plpl = plgo.Figure(data=traces, layout=layout)
1260
1261         # Export Plot
1262         logging.info(f"    Writing file {plot[u'output-file']}.html")
1263         ploff.plot(
1264             plpl,
1265             show_link=False,
1266             auto_open=False,
1267             filename=f"{plot[u'output-file']}.html"
1268         )
1269     except PlotlyError as err:
1270         logging.error(
1271             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
1272         )
1273         return