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