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