Report: Fix plot_perf_box_name
[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", ):
390                         test_type = u"NDRPDR"
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             else:
483                 layout[u"title"] = f"<b>Throughput:</b> {layout[u'title']}"
484         if y_max:
485             layout[u"yaxis"][u"range"] = [0, max(y_max)]
486         plpl = plgo.Figure(data=traces, layout=layout)
487
488         # Export Plot
489         logging.info(f"    Writing file {plot[u'output-file']}.html.")
490         ploff.plot(
491             plpl,
492             show_link=False,
493             auto_open=False,
494             filename=f"{plot[u'output-file']}.html"
495         )
496     except PlotlyError as err:
497         logging.error(
498             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
499         )
500         return
501
502
503 def plot_tsa_name(plot, input_data):
504     """Generate the plot(s) with algorithm:
505     plot_tsa_name
506     specified in the specification file.
507
508     :param plot: Plot to generate.
509     :param input_data: Data to process.
510     :type plot: pandas.Series
511     :type input_data: InputData
512     """
513
514     # Transform the data
515     plot_title = plot.get(u"title", u"")
516     logging.info(
517         f"    Creating data set for the {plot.get(u'type', u'')} {plot_title}."
518     )
519     data = input_data.filter_tests_by_name(
520         plot,
521         params=[u"throughput", u"gbps", u"parent", u"tags", u"type"]
522     )
523     if data is None:
524         logging.error(u"No data.")
525         return
526
527     plot_title = plot_title.lower()
528
529     if u"-gbps" in plot_title:
530         value = u"gbps"
531         h_unit = u"Gbps"
532         multiplier = 1e6
533     else:
534         value = u"throughput"
535         h_unit = u"Mpps"
536         multiplier = 1.0
537
538     y_vals = OrderedDict()
539     for job in data:
540         for build in job:
541             for test in build:
542                 if y_vals.get(test[u"parent"], None) is None:
543                     y_vals[test[u"parent"]] = {
544                         u"1": list(),
545                         u"2": list(),
546                         u"4": list()
547                     }
548                 try:
549                     if test[u"type"] not in (u"NDRPDR",):
550                         continue
551
552                     if u"-pdr" in plot_title:
553                         ttype = u"PDR"
554                     elif u"-ndr" in plot_title:
555                         ttype = u"NDR"
556                     else:
557                         continue
558
559                     if u"1C" in test[u"tags"]:
560                         y_vals[test[u"parent"]][u"1"]. \
561                             append(test[value][ttype][u"LOWER"] * multiplier)
562                     elif u"2C" in test[u"tags"]:
563                         y_vals[test[u"parent"]][u"2"]. \
564                             append(test[value][ttype][u"LOWER"] * multiplier)
565                     elif u"4C" in test[u"tags"]:
566                         y_vals[test[u"parent"]][u"4"]. \
567                             append(test[value][ttype][u"LOWER"] * multiplier)
568                 except (KeyError, TypeError):
569                     pass
570
571     if not y_vals:
572         logging.warning(f"No data for the plot {plot.get(u'title', u'')}")
573         return
574
575     y_1c_max = dict()
576     for test_name, test_vals in y_vals.items():
577         for key, test_val in test_vals.items():
578             if test_val:
579                 avg_val = sum(test_val) / len(test_val)
580                 y_vals[test_name][key] = [avg_val, len(test_val)]
581                 ideal = avg_val / (int(key) * 1e6)
582                 if test_name not in y_1c_max or ideal > y_1c_max[test_name]:
583                     y_1c_max[test_name] = ideal
584
585     vals = OrderedDict()
586     y_max = list()
587     nic_limit = 0
588     lnk_limit = 0
589     pci_limit = plot[u"limits"][u"pci"][u"pci-g3-x8"]
590     for test_name, test_vals in y_vals.items():
591         try:
592             if test_vals[u"1"][1]:
593                 name = re.sub(
594                     REGEX_NIC,
595                     u"",
596                     test_name.replace(u'-ndrpdr', u'').replace(u'2n1l-', u'')
597                 )
598                 vals[name] = OrderedDict()
599                 y_val_1 = test_vals[u"1"][0] / 1e6
600                 y_val_2 = test_vals[u"2"][0] / 1e6 if test_vals[u"2"][0] \
601                     else None
602                 y_val_4 = test_vals[u"4"][0] / 1e6 if test_vals[u"4"][0] \
603                     else None
604
605                 vals[name][u"val"] = [y_val_1, y_val_2, y_val_4]
606                 vals[name][u"rel"] = [1.0, None, None]
607                 vals[name][u"ideal"] = [
608                     y_1c_max[test_name],
609                     y_1c_max[test_name] * 2,
610                     y_1c_max[test_name] * 4
611                 ]
612                 vals[name][u"diff"] = [
613                     (y_val_1 - y_1c_max[test_name]) * 100 / y_val_1, None, None
614                 ]
615                 vals[name][u"count"] = [
616                     test_vals[u"1"][1],
617                     test_vals[u"2"][1],
618                     test_vals[u"4"][1]
619                 ]
620
621                 try:
622                     val_max = max(vals[name][u"val"])
623                 except ValueError as err:
624                     logging.error(repr(err))
625                     continue
626                 if val_max:
627                     y_max.append(val_max)
628
629                 if y_val_2:
630                     vals[name][u"rel"][1] = round(y_val_2 / y_val_1, 2)
631                     vals[name][u"diff"][1] = \
632                         (y_val_2 - vals[name][u"ideal"][1]) * 100 / y_val_2
633                 if y_val_4:
634                     vals[name][u"rel"][2] = round(y_val_4 / y_val_1, 2)
635                     vals[name][u"diff"][2] = \
636                         (y_val_4 - vals[name][u"ideal"][2]) * 100 / y_val_4
637         except IndexError as err:
638             logging.warning(f"No data for {test_name}")
639             logging.warning(repr(err))
640
641         # Limits:
642         if u"x520" in test_name:
643             limit = plot[u"limits"][u"nic"][u"x520"]
644         elif u"x710" in test_name:
645             limit = plot[u"limits"][u"nic"][u"x710"]
646         elif u"xxv710" in test_name:
647             limit = plot[u"limits"][u"nic"][u"xxv710"]
648         elif u"xl710" in test_name:
649             limit = plot[u"limits"][u"nic"][u"xl710"]
650         elif u"x553" in test_name:
651             limit = plot[u"limits"][u"nic"][u"x553"]
652         elif u"cx556a" in test_name:
653             limit = plot[u"limits"][u"nic"][u"cx556a"]
654         else:
655             limit = 0
656         if limit > nic_limit:
657             nic_limit = limit
658
659         mul = 2 if u"ge2p" in test_name else 1
660         if u"10ge" in test_name:
661             limit = plot[u"limits"][u"link"][u"10ge"] * mul
662         elif u"25ge" in test_name:
663             limit = plot[u"limits"][u"link"][u"25ge"] * mul
664         elif u"40ge" in test_name:
665             limit = plot[u"limits"][u"link"][u"40ge"] * mul
666         elif u"100ge" in test_name:
667             limit = plot[u"limits"][u"link"][u"100ge"] * mul
668         else:
669             limit = 0
670         if limit > lnk_limit:
671             lnk_limit = limit
672
673     traces = list()
674     annotations = list()
675     x_vals = [1, 2, 4]
676
677     # Limits:
678     if u"-gbps" not in plot_title:
679         try:
680             threshold = 1.1 * max(y_max)  # 10%
681         except ValueError as err:
682             logging.error(err)
683             return
684         nic_limit /= 1e6
685         traces.append(plgo.Scatter(
686             x=x_vals,
687             y=[nic_limit, ] * len(x_vals),
688             name=f"NIC: {nic_limit:.2f}Mpps",
689             showlegend=False,
690             mode=u"lines",
691             line=dict(
692                 dash=u"dot",
693                 color=COLORS[-1],
694                 width=1),
695             hoverinfo=u"none"
696         ))
697         annotations.append(dict(
698             x=1,
699             y=nic_limit,
700             xref=u"x",
701             yref=u"y",
702             xanchor=u"left",
703             yanchor=u"bottom",
704             text=f"NIC: {nic_limit:.2f}Mpps",
705             font=dict(
706                 size=14,
707                 color=COLORS[-1],
708             ),
709             align=u"left",
710             showarrow=False
711         ))
712         y_max.append(nic_limit)
713
714         lnk_limit /= 1e6
715         if lnk_limit < threshold:
716             traces.append(plgo.Scatter(
717                 x=x_vals,
718                 y=[lnk_limit, ] * len(x_vals),
719                 name=f"Link: {lnk_limit:.2f}Mpps",
720                 showlegend=False,
721                 mode=u"lines",
722                 line=dict(
723                     dash=u"dot",
724                     color=COLORS[-2],
725                     width=1),
726                 hoverinfo=u"none"
727             ))
728             annotations.append(dict(
729                 x=1,
730                 y=lnk_limit,
731                 xref=u"x",
732                 yref=u"y",
733                 xanchor=u"left",
734                 yanchor=u"bottom",
735                 text=f"Link: {lnk_limit:.2f}Mpps",
736                 font=dict(
737                     size=14,
738                     color=COLORS[-2],
739                 ),
740                 align=u"left",
741                 showarrow=False
742             ))
743             y_max.append(lnk_limit)
744
745         pci_limit /= 1e6
746         if (pci_limit < threshold and
747                 (pci_limit < lnk_limit * 0.95 or lnk_limit > lnk_limit * 1.05)):
748             traces.append(plgo.Scatter(
749                 x=x_vals,
750                 y=[pci_limit, ] * len(x_vals),
751                 name=f"PCIe: {pci_limit:.2f}Mpps",
752                 showlegend=False,
753                 mode=u"lines",
754                 line=dict(
755                     dash=u"dot",
756                     color=COLORS[-3],
757                     width=1),
758                 hoverinfo=u"none"
759             ))
760             annotations.append(dict(
761                 x=1,
762                 y=pci_limit,
763                 xref=u"x",
764                 yref=u"y",
765                 xanchor=u"left",
766                 yanchor=u"bottom",
767                 text=f"PCIe: {pci_limit:.2f}Mpps",
768                 font=dict(
769                     size=14,
770                     color=COLORS[-3],
771                 ),
772                 align=u"left",
773                 showarrow=False
774             ))
775             y_max.append(pci_limit)
776
777     # Perfect and measured:
778     cidx = 0
779     for name, val in vals.items():
780         hovertext = list()
781         try:
782             for idx in range(len(val[u"val"])):
783                 htext = ""
784                 if isinstance(val[u"val"][idx], float):
785                     htext += (
786                         f"No. of Runs: {val[u'count'][idx]}<br>"
787                         f"Mean: {val[u'val'][idx]:.2f}{h_unit}<br>"
788                     )
789                 if isinstance(val[u"diff"][idx], float):
790                     htext += f"Diff: {round(val[u'diff'][idx]):.0f}%<br>"
791                 if isinstance(val[u"rel"][idx], float):
792                     htext += f"Speedup: {val[u'rel'][idx]:.2f}"
793                 hovertext.append(htext)
794             traces.append(
795                 plgo.Scatter(
796                     x=x_vals,
797                     y=val[u"val"],
798                     name=name,
799                     legendgroup=name,
800                     mode=u"lines+markers",
801                     line=dict(
802                         color=COLORS[cidx],
803                         width=2),
804                     marker=dict(
805                         symbol=u"circle",
806                         size=10
807                     ),
808                     text=hovertext,
809                     hoverinfo=u"text+name"
810                 )
811             )
812             traces.append(
813                 plgo.Scatter(
814                     x=x_vals,
815                     y=val[u"ideal"],
816                     name=f"{name} perfect",
817                     legendgroup=name,
818                     showlegend=False,
819                     mode=u"lines",
820                     line=dict(
821                         color=COLORS[cidx],
822                         width=2,
823                         dash=u"dash"),
824                     text=[f"Perfect: {y:.2f}Mpps" for y in val[u"ideal"]],
825                     hoverinfo=u"text"
826                 )
827             )
828             cidx += 1
829         except (IndexError, ValueError, KeyError) as err:
830             logging.warning(f"No data for {name}\n{repr(err)}")
831
832     try:
833         # Create plot
834         file_type = plot.get(u"output-file-type", u".html")
835         logging.info(f"    Writing file {plot[u'output-file']}{file_type}.")
836         layout = deepcopy(plot[u"layout"])
837         if layout.get(u"title", None):
838             layout[u"title"] = f"<b>Speedup Multi-core:</b> {layout[u'title']}"
839         layout[u"yaxis"][u"range"] = [0, int(max(y_max) * 1.1)]
840         layout[u"annotations"].extend(annotations)
841         plpl = plgo.Figure(data=traces, layout=layout)
842
843         # Export Plot
844         ploff.plot(
845             plpl,
846             show_link=False,
847             auto_open=False,
848             filename=f"{plot[u'output-file']}{file_type}"
849         )
850     except PlotlyError as err:
851         logging.error(
852             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
853         )
854         return
855
856
857 def plot_http_server_perf_box(plot, input_data):
858     """Generate the plot(s) with algorithm: plot_http_server_perf_box
859     specified in the specification file.
860
861     :param plot: Plot to generate.
862     :param input_data: Data to process.
863     :type plot: pandas.Series
864     :type input_data: InputData
865     """
866
867     # Transform the data
868     logging.info(
869         f"    Creating the data set for the {plot.get(u'type', u'')} "
870         f"{plot.get(u'title', u'')}."
871     )
872     data = input_data.filter_data(plot)
873     if data is None:
874         logging.error(u"No data.")
875         return
876
877     # Prepare the data for the plot
878     y_vals = dict()
879     for job in data:
880         for build in job:
881             for test in build:
882                 if y_vals.get(test[u"name"], None) is None:
883                     y_vals[test[u"name"]] = list()
884                 try:
885                     y_vals[test[u"name"]].append(test[u"result"])
886                 except (KeyError, TypeError):
887                     y_vals[test[u"name"]].append(None)
888
889     # Add None to the lists with missing data
890     max_len = 0
891     nr_of_samples = list()
892     for val in y_vals.values():
893         if len(val) > max_len:
894             max_len = len(val)
895         nr_of_samples.append(len(val))
896     for val in y_vals.values():
897         if len(val) < max_len:
898             val.extend([None for _ in range(max_len - len(val))])
899
900     # Add plot traces
901     traces = list()
902     df_y = pd.DataFrame(y_vals)
903     df_y.head()
904     for i, col in enumerate(df_y.columns):
905         name = \
906             f"{i + 1}. " \
907             f"({nr_of_samples[i]:02d} " \
908             f"run{u's' if nr_of_samples[i] > 1 else u''}) " \
909             f"{col.lower().replace(u'-ndrpdr', u'')}"
910         if len(name) > 50:
911             name_lst = name.split(u'-')
912             name = u""
913             split_name = True
914             for segment in name_lst:
915                 if (len(name) + len(segment) + 1) > 50 and split_name:
916                     name += u"<br>    "
917                     split_name = False
918                 name += segment + u'-'
919             name = name[:-1]
920
921         traces.append(plgo.Box(x=[str(i + 1) + u'.'] * len(df_y[col]),
922                                y=df_y[col],
923                                name=name,
924                                **plot[u"traces"]))
925     try:
926         # Create plot
927         plpl = plgo.Figure(data=traces, layout=plot[u"layout"])
928
929         # Export Plot
930         logging.info(
931             f"    Writing file {plot[u'output-file']}"
932             f"{plot[u'output-file-type']}."
933         )
934         ploff.plot(
935             plpl,
936             show_link=False,
937             auto_open=False,
938             filename=f"{plot[u'output-file']}{plot[u'output-file-type']}"
939         )
940     except PlotlyError as err:
941         logging.error(
942             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
943         )
944         return
945
946
947 def plot_nf_heatmap(plot, input_data):
948     """Generate the plot(s) with algorithm: plot_nf_heatmap
949     specified in the specification file.
950
951     :param plot: Plot to generate.
952     :param input_data: Data to process.
953     :type plot: pandas.Series
954     :type input_data: InputData
955     """
956
957     regex_cn = re.compile(r'^(\d*)R(\d*)C$')
958     regex_test_name = re.compile(r'^.*-(\d+ch|\d+pl)-'
959                                  r'(\d+mif|\d+vh)-'
960                                  r'(\d+vm\d+t|\d+dcr\d+t|\d+dcr\d+c).*$')
961     vals = dict()
962
963     # Transform the data
964     logging.info(
965         f"    Creating the data set for the {plot.get(u'type', u'')} "
966         f"{plot.get(u'title', u'')}."
967     )
968     data = input_data.filter_data(plot, continue_on_error=True)
969     if data is None or data.empty:
970         logging.error(u"No data.")
971         return
972
973     for job in data:
974         for build in job:
975             for test in build:
976                 for tag in test[u"tags"]:
977                     groups = re.search(regex_cn, tag)
978                     if groups:
979                         chain = str(groups.group(1))
980                         node = str(groups.group(2))
981                         break
982                 else:
983                     continue
984                 groups = re.search(regex_test_name, test[u"name"])
985                 if groups and len(groups.groups()) == 3:
986                     hover_name = (
987                         f"{str(groups.group(1))}-"
988                         f"{str(groups.group(2))}-"
989                         f"{str(groups.group(3))}"
990                     )
991                 else:
992                     hover_name = u""
993                 if vals.get(chain, None) is None:
994                     vals[chain] = dict()
995                 if vals[chain].get(node, None) is None:
996                     vals[chain][node] = dict(
997                         name=hover_name,
998                         vals=list(),
999                         nr=None,
1000                         mean=None,
1001                         stdev=None
1002                     )
1003                 try:
1004                     if plot[u"include-tests"] == u"MRR":
1005                         result = test[u"result"][u"receive-rate"]
1006                     elif plot[u"include-tests"] == u"PDR":
1007                         result = test[u"throughput"][u"PDR"][u"LOWER"]
1008                     elif plot[u"include-tests"] == u"NDR":
1009                         result = test[u"throughput"][u"NDR"][u"LOWER"]
1010                     else:
1011                         result = None
1012                 except TypeError:
1013                     result = None
1014
1015                 if result:
1016                     vals[chain][node][u"vals"].append(result)
1017
1018     if not vals:
1019         logging.error(u"No data.")
1020         return
1021
1022     txt_chains = list()
1023     txt_nodes = list()
1024     for key_c in vals:
1025         txt_chains.append(key_c)
1026         for key_n in vals[key_c].keys():
1027             txt_nodes.append(key_n)
1028             if vals[key_c][key_n][u"vals"]:
1029                 vals[key_c][key_n][u"nr"] = len(vals[key_c][key_n][u"vals"])
1030                 vals[key_c][key_n][u"mean"] = \
1031                     round(mean(vals[key_c][key_n][u"vals"]) / 1000000, 1)
1032                 vals[key_c][key_n][u"stdev"] = \
1033                     round(stdev(vals[key_c][key_n][u"vals"]) / 1000000, 1)
1034     txt_nodes = list(set(txt_nodes))
1035
1036     def sort_by_int(value):
1037         """Makes possible to sort a list of strings which represent integers.
1038
1039         :param value: Integer as a string.
1040         :type value: str
1041         :returns: Integer representation of input parameter 'value'.
1042         :rtype: int
1043         """
1044         return int(value)
1045
1046     txt_chains = sorted(txt_chains, key=sort_by_int)
1047     txt_nodes = sorted(txt_nodes, key=sort_by_int)
1048
1049     chains = [i + 1 for i in range(len(txt_chains))]
1050     nodes = [i + 1 for i in range(len(txt_nodes))]
1051
1052     data = [list() for _ in range(len(chains))]
1053     for chain in chains:
1054         for node in nodes:
1055             try:
1056                 val = vals[txt_chains[chain - 1]][txt_nodes[node - 1]][u"mean"]
1057             except (KeyError, IndexError):
1058                 val = None
1059             data[chain - 1].append(val)
1060
1061     # Color scales:
1062     my_green = [[0.0, u"rgb(235, 249, 242)"],
1063                 [1.0, u"rgb(45, 134, 89)"]]
1064
1065     my_blue = [[0.0, u"rgb(236, 242, 248)"],
1066                [1.0, u"rgb(57, 115, 172)"]]
1067
1068     my_grey = [[0.0, u"rgb(230, 230, 230)"],
1069                [1.0, u"rgb(102, 102, 102)"]]
1070
1071     hovertext = list()
1072     annotations = list()
1073
1074     text = (u"Test: {name}<br>"
1075             u"Runs: {nr}<br>"
1076             u"Thput: {val}<br>"
1077             u"StDev: {stdev}")
1078
1079     for chain, _ in enumerate(txt_chains):
1080         hover_line = list()
1081         for node, _ in enumerate(txt_nodes):
1082             if data[chain][node] is not None:
1083                 annotations.append(
1084                     dict(
1085                         x=node+1,
1086                         y=chain+1,
1087                         xref=u"x",
1088                         yref=u"y",
1089                         xanchor=u"center",
1090                         yanchor=u"middle",
1091                         text=str(data[chain][node]),
1092                         font=dict(
1093                             size=14,
1094                         ),
1095                         align=u"center",
1096                         showarrow=False
1097                     )
1098                 )
1099                 hover_line.append(text.format(
1100                     name=vals[txt_chains[chain]][txt_nodes[node]][u"name"],
1101                     nr=vals[txt_chains[chain]][txt_nodes[node]][u"nr"],
1102                     val=data[chain][node],
1103                     stdev=vals[txt_chains[chain]][txt_nodes[node]][u"stdev"]))
1104         hovertext.append(hover_line)
1105
1106     traces = [
1107         plgo.Heatmap(
1108             x=nodes,
1109             y=chains,
1110             z=data,
1111             colorbar=dict(
1112                 title=plot.get(u"z-axis", u""),
1113                 titleside=u"right",
1114                 titlefont=dict(
1115                     size=16
1116                 ),
1117                 tickfont=dict(
1118                     size=16,
1119                 ),
1120                 tickformat=u".1f",
1121                 yanchor=u"bottom",
1122                 y=-0.02,
1123                 len=0.925,
1124             ),
1125             showscale=True,
1126             colorscale=my_green,
1127             text=hovertext,
1128             hoverinfo=u"text"
1129         )
1130     ]
1131
1132     for idx, item in enumerate(txt_nodes):
1133         # X-axis, numbers:
1134         annotations.append(
1135             dict(
1136                 x=idx+1,
1137                 y=0.05,
1138                 xref=u"x",
1139                 yref=u"y",
1140                 xanchor=u"center",
1141                 yanchor=u"top",
1142                 text=item,
1143                 font=dict(
1144                     size=16,
1145                 ),
1146                 align=u"center",
1147                 showarrow=False
1148             )
1149         )
1150     for idx, item in enumerate(txt_chains):
1151         # Y-axis, numbers:
1152         annotations.append(
1153             dict(
1154                 x=0.35,
1155                 y=idx+1,
1156                 xref=u"x",
1157                 yref=u"y",
1158                 xanchor=u"right",
1159                 yanchor=u"middle",
1160                 text=item,
1161                 font=dict(
1162                     size=16,
1163                 ),
1164                 align=u"center",
1165                 showarrow=False
1166             )
1167         )
1168     # X-axis, title:
1169     annotations.append(
1170         dict(
1171             x=0.55,
1172             y=-0.15,
1173             xref=u"paper",
1174             yref=u"y",
1175             xanchor=u"center",
1176             yanchor=u"bottom",
1177             text=plot.get(u"x-axis", u""),
1178             font=dict(
1179                 size=16,
1180             ),
1181             align=u"center",
1182             showarrow=False
1183         )
1184     )
1185     # Y-axis, title:
1186     annotations.append(
1187         dict(
1188             x=-0.1,
1189             y=0.5,
1190             xref=u"x",
1191             yref=u"paper",
1192             xanchor=u"center",
1193             yanchor=u"middle",
1194             text=plot.get(u"y-axis", u""),
1195             font=dict(
1196                 size=16,
1197             ),
1198             align=u"center",
1199             textangle=270,
1200             showarrow=False
1201         )
1202     )
1203     updatemenus = list([
1204         dict(
1205             x=1.0,
1206             y=0.0,
1207             xanchor=u"right",
1208             yanchor=u"bottom",
1209             direction=u"up",
1210             buttons=list([
1211                 dict(
1212                     args=[
1213                         {
1214                             u"colorscale": [my_green, ],
1215                             u"reversescale": False
1216                         }
1217                     ],
1218                     label=u"Green",
1219                     method=u"update"
1220                 ),
1221                 dict(
1222                     args=[
1223                         {
1224                             u"colorscale": [my_blue, ],
1225                             u"reversescale": False
1226                         }
1227                     ],
1228                     label=u"Blue",
1229                     method=u"update"
1230                 ),
1231                 dict(
1232                     args=[
1233                         {
1234                             u"colorscale": [my_grey, ],
1235                             u"reversescale": False
1236                         }
1237                     ],
1238                     label=u"Grey",
1239                     method=u"update"
1240                 )
1241             ])
1242         )
1243     ])
1244
1245     try:
1246         layout = deepcopy(plot[u"layout"])
1247     except KeyError as err:
1248         logging.error(f"Finished with error: No layout defined\n{repr(err)}")
1249         return
1250
1251     layout[u"annotations"] = annotations
1252     layout[u'updatemenus'] = updatemenus
1253
1254     try:
1255         # Create plot
1256         plpl = plgo.Figure(data=traces, layout=layout)
1257
1258         # Export Plot
1259         logging.info(f"    Writing file {plot[u'output-file']}.html")
1260         ploff.plot(
1261             plpl,
1262             show_link=False,
1263             auto_open=False,
1264             filename=f"{plot[u'output-file']}.html"
1265         )
1266     except PlotlyError as err:
1267         logging.error(
1268             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
1269         )
1270         return