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