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