dda519600835121b1ee05f74b9f4b560a1d7a8ad
[csit.git] / resources / tools / presentation / generator_plots.py
1 # Copyright (c) 2019 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 pandas as pd
25 import plotly.offline as ploff
26 import plotly.graph_objs as plgo
27
28 from plotly.exceptions import PlotlyError
29
30 from pal_utils import mean, stdev
31
32
33 COLORS = [u"SkyBlue", u"Olive", u"Purple", u"Coral", u"Indigo", u"Pink",
34           u"Chocolate", u"Brown", u"Magenta", u"Cyan", u"Orange", u"Black",
35           u"Violet", u"Blue", u"Yellow", u"BurlyWood", u"CadetBlue", u"Crimson",
36           u"DarkBlue", u"DarkCyan", u"DarkGreen", u"Green", u"GoldenRod",
37           u"LightGreen", u"LightSeaGreen", u"LightSkyBlue", u"Maroon",
38           u"MediumSeaGreen", u"SeaGreen", u"LightSlateGrey"]
39
40 REGEX_NIC = re.compile(r'\d*ge\dp\d\D*\d*-')
41
42
43 def generate_plots(spec, data):
44     """Generate all plots specified in the specification file.
45
46     :param spec: Specification read from the specification file.
47     :param data: Data to process.
48     :type spec: Specification
49     :type data: InputData
50     """
51
52     generator = {
53         u"plot_nf_reconf_box_name": plot_nf_reconf_box_name,
54         u"plot_perf_box_name": plot_perf_box_name,
55         u"plot_lat_err_bars_name": plot_lat_err_bars_name,
56         u"plot_tsa_name": plot_tsa_name,
57         u"plot_http_server_perf_box": plot_http_server_perf_box,
58         u"plot_nf_heatmap": plot_nf_heatmap
59     }
60
61     logging.info(u"Generating the plots ...")
62     for index, plot in enumerate(spec.plots):
63         try:
64             logging.info(f"  Plot nr {index + 1}: {plot.get(u'title', u'')}")
65             plot[u"limits"] = spec.configuration[u"limits"]
66             generator[plot[u"algorithm"]](plot, data)
67             logging.info(u"  Done.")
68         except NameError as err:
69             logging.error(
70                 f"Probably algorithm {plot[u'algorithm']} is not defined: "
71                 f"{repr(err)}"
72             )
73     logging.info(u"Done.")
74
75
76 def plot_nf_reconf_box_name(plot, input_data):
77     """Generate the plot(s) with algorithm: plot_nf_reconf_box_name
78     specified in the specification file.
79
80     :param plot: Plot to generate.
81     :param input_data: Data to process.
82     :type plot: pandas.Series
83     :type input_data: InputData
84     """
85
86     # Transform the data
87     logging.info(
88         f"    Creating the data set for the {plot.get(u'type', u'')} "
89         f"{plot.get(u'title', u'')}."
90     )
91     data = input_data.filter_tests_by_name(
92         plot, params=[u"result", u"parent", u"tags", u"type"]
93     )
94     if data is None:
95         logging.error(u"No data.")
96         return
97
98     # Prepare the data for the plot
99     y_vals = OrderedDict()
100     loss = dict()
101     for job in data:
102         for build in job:
103             for test in build:
104                 if y_vals.get(test[u"parent"], None) is None:
105                     y_vals[test[u"parent"]] = list()
106                     loss[test[u"parent"]] = list()
107                 try:
108                     y_vals[test[u"parent"]].append(test[u"result"][u"time"])
109                     loss[test[u"parent"]].append(test[u"result"][u"loss"])
110                 except (KeyError, TypeError):
111                     y_vals[test[u"parent"]].append(None)
112
113     # Add None to the lists with missing data
114     max_len = 0
115     nr_of_samples = list()
116     for val in y_vals.values():
117         if len(val) > max_len:
118             max_len = len(val)
119         nr_of_samples.append(len(val))
120     for val in y_vals.values():
121         if len(val) < max_len:
122             val.extend([None for _ in range(max_len - len(val))])
123
124     # Add plot traces
125     traces = list()
126     df_y = pd.DataFrame(y_vals)
127     df_y.head()
128     for i, col in enumerate(df_y.columns):
129         tst_name = re.sub(REGEX_NIC, u"",
130                           col.lower().replace(u'-ndrpdr', u'').
131                           replace(u'2n1l-', u''))
132
133         traces.append(plgo.Box(
134             x=[str(i + 1) + u'.'] * len(df_y[col]),
135             y=[y if y else None for y in df_y[col]],
136             name=(
137                 f"{i + 1}. "
138                 f"({nr_of_samples[i]:02d} "
139                 f"run{u's' if nr_of_samples[i] > 1 else u''}, "
140                 f"packets lost average: {mean(loss[col]):.1f}) "
141                 f"{u'-'.join(tst_name.split(u'-')[3:-2])}"
142             ),
143             hoverinfo=u"y+name"
144         ))
145     try:
146         # Create plot
147         layout = deepcopy(plot[u"layout"])
148         layout[u"title"] = f"<b>Time Lost:</b> {layout[u'title']}"
149         layout[u"yaxis"][u"title"] = u"<b>Implied Time Lost [s]</b>"
150         layout[u"legend"][u"font"][u"size"] = 14
151         layout[u"yaxis"].pop(u"range")
152         plpl = plgo.Figure(data=traces, layout=layout)
153
154         # Export Plot
155         file_type = plot.get(u"output-file-type", u".html")
156         logging.info(f"    Writing file {plot[u'output-file']}{file_type}.")
157         ploff.plot(
158             plpl,
159             show_link=False,
160             auto_open=False,
161             filename=f"{plot[u'output-file']}{file_type}"
162         )
163     except PlotlyError as err:
164         logging.error(
165             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
166         )
167         return
168
169
170 def plot_perf_box_name(plot, input_data):
171     """Generate the plot(s) with algorithm: plot_perf_box_name
172     specified in the specification file.
173
174     :param plot: Plot to generate.
175     :param input_data: Data to process.
176     :type plot: pandas.Series
177     :type input_data: InputData
178     """
179
180     # Transform the data
181     logging.info(
182         f"    Creating data set for the {plot.get(u'type', u'')} "
183         f"{plot.get(u'title', u'')}."
184     )
185     data = input_data.filter_tests_by_name(
186         plot, params=[u"throughput", u"parent", u"tags", u"type"])
187     if data is None:
188         logging.error(u"No data.")
189         return
190
191     # Prepare the data for the plot
192     y_vals = OrderedDict()
193     for job in data:
194         for build in job:
195             for test in build:
196                 if y_vals.get(test[u"parent"], None) is None:
197                     y_vals[test[u"parent"]] = list()
198                 try:
199                     if (test[u"type"] in (u"NDRPDR", ) and
200                             u"-pdr" in plot.get(u"title", u"").lower()):
201                         y_vals[test[u"parent"]].\
202                             append(test[u"throughput"][u"PDR"][u"LOWER"])
203                     elif (test[u"type"] in (u"NDRPDR", ) and
204                           u"-ndr" in plot.get(u"title", u"").lower()):
205                         y_vals[test[u"parent"]]. \
206                             append(test[u"throughput"][u"NDR"][u"LOWER"])
207                     elif test[u"type"] in (u"SOAK", ):
208                         y_vals[test[u"parent"]].\
209                             append(test[u"throughput"][u"LOWER"])
210                     else:
211                         continue
212                 except (KeyError, TypeError):
213                     y_vals[test[u"parent"]].append(None)
214
215     # Add None to the lists with missing data
216     max_len = 0
217     nr_of_samples = list()
218     for val in y_vals.values():
219         if len(val) > max_len:
220             max_len = len(val)
221         nr_of_samples.append(len(val))
222     for val in y_vals.values():
223         if len(val) < max_len:
224             val.extend([None for _ in range(max_len - len(val))])
225
226     # Add plot traces
227     traces = list()
228     df_y = pd.DataFrame(y_vals)
229     df_y.head()
230     y_max = list()
231     for i, col in enumerate(df_y.columns):
232         tst_name = re.sub(REGEX_NIC, u"",
233                           col.lower().replace(u'-ndrpdr', u'').
234                           replace(u'2n1l-', u''))
235         traces.append(
236             plgo.Box(
237                 x=[str(i + 1) + u'.'] * len(df_y[col]),
238                 y=[y / 1000000 if y else None for y in df_y[col]],
239                 name=(
240                     f"{i + 1}. "
241                     f"({nr_of_samples[i]:02d} "
242                     f"run{u's' if nr_of_samples[i] > 1 else u''}) "
243                     f"{tst_name}"
244                 ),
245                 hoverinfo=u"y+name"
246             )
247         )
248         try:
249             val_max = max(df_y[col])
250             if val_max:
251                 y_max.append(int(val_max / 1000000) + 2)
252         except (ValueError, TypeError) as err:
253             logging.error(repr(err))
254             continue
255
256     try:
257         # Create plot
258         layout = deepcopy(plot[u"layout"])
259         if layout.get(u"title", None):
260             layout[u"title"] = f"<b>Throughput:</b> {layout[u'title']}"
261         if y_max:
262             layout[u"yaxis"][u"range"] = [0, max(y_max)]
263         plpl = plgo.Figure(data=traces, layout=layout)
264
265         # Export Plot
266         logging.info(f"    Writing file {plot[u'output-file']}.html.")
267         ploff.plot(
268             plpl,
269             show_link=False,
270             auto_open=False,
271             filename=f"{plot[u'output-file']}.html"
272         )
273     except PlotlyError as err:
274         logging.error(
275             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
276         )
277         return
278
279
280 def plot_lat_err_bars_name(plot, input_data):
281     """Generate the plot(s) with algorithm: plot_lat_err_bars_name
282     specified in the specification file.
283
284     :param plot: Plot to generate.
285     :param input_data: Data to process.
286     :type plot: pandas.Series
287     :type input_data: InputData
288     """
289
290     # Transform the data
291     plot_title = plot.get(u"title", u"")
292     logging.info(
293         f"    Creating data set for the {plot.get(u'type', u'')} {plot_title}."
294     )
295     data = input_data.filter_tests_by_name(
296         plot, params=[u"latency", u"parent", u"tags", u"type"])
297     if data is None:
298         logging.error(u"No data.")
299         return
300
301     # Prepare the data for the plot
302     y_tmp_vals = OrderedDict()
303     for job in data:
304         for build in job:
305             for test in build:
306                 try:
307                     logging.debug(f"test[u'latency']: {test[u'latency']}\n")
308                 except ValueError as err:
309                     logging.warning(repr(err))
310                 if y_tmp_vals.get(test[u"parent"], None) is None:
311                     y_tmp_vals[test[u"parent"]] = [
312                         list(),  # direction1, min
313                         list(),  # direction1, avg
314                         list(),  # direction1, max
315                         list(),  # direction2, min
316                         list(),  # direction2, avg
317                         list()   # direction2, max
318                     ]
319                 try:
320                     if test[u"type"] not in (u"NDRPDR", ):
321                         logging.warning(f"Invalid test type: {test[u'type']}")
322                         continue
323                     if u"-pdr" in plot_title.lower():
324                         ttype = u"PDR"
325                     elif u"-ndr" in plot_title.lower():
326                         ttype = u"NDR"
327                     else:
328                         logging.warning(
329                             f"Invalid test type: {test[u'type']}"
330                         )
331                         continue
332                     y_tmp_vals[test[u"parent"]][0].append(
333                         test[u"latency"][ttype][u"direction1"][u"min"])
334                     y_tmp_vals[test[u"parent"]][1].append(
335                         test[u"latency"][ttype][u"direction1"][u"avg"])
336                     y_tmp_vals[test[u"parent"]][2].append(
337                         test[u"latency"][ttype][u"direction1"][u"max"])
338                     y_tmp_vals[test[u"parent"]][3].append(
339                         test[u"latency"][ttype][u"direction2"][u"min"])
340                     y_tmp_vals[test[u"parent"]][4].append(
341                         test[u"latency"][ttype][u"direction2"][u"avg"])
342                     y_tmp_vals[test[u"parent"]][5].append(
343                         test[u"latency"][ttype][u"direction2"][u"max"])
344                 except (KeyError, TypeError) as err:
345                     logging.warning(repr(err))
346
347     x_vals = list()
348     y_vals = list()
349     y_mins = list()
350     y_maxs = list()
351     nr_of_samples = list()
352     for key, val in y_tmp_vals.items():
353         name = re.sub(REGEX_NIC, u"", key.replace(u'-ndrpdr', u'').
354                       replace(u'2n1l-', u''))
355         x_vals.append(name)  # dir 1
356         y_vals.append(mean(val[1]) if val[1] else None)
357         y_mins.append(mean(val[0]) if val[0] else None)
358         y_maxs.append(mean(val[2]) if val[2] else None)
359         nr_of_samples.append(len(val[1]) if val[1] else 0)
360         x_vals.append(name)  # dir 2
361         y_vals.append(mean(val[4]) if val[4] else None)
362         y_mins.append(mean(val[3]) if val[3] else None)
363         y_maxs.append(mean(val[5]) if val[5] else None)
364         nr_of_samples.append(len(val[3]) if val[3] else 0)
365
366     traces = list()
367     annotations = list()
368
369     for idx, _ in enumerate(x_vals):
370         if not bool(int(idx % 2)):
371             direction = u"West-East"
372         else:
373             direction = u"East-West"
374         hovertext = (
375             f"No. of Runs: {nr_of_samples[idx]}<br>"
376             f"Test: {x_vals[idx]}<br>"
377             f"Direction: {direction}<br>"
378         )
379         if isinstance(y_maxs[idx], float):
380             hovertext += f"Max: {y_maxs[idx]:.2f}uSec<br>"
381         if isinstance(y_vals[idx], float):
382             hovertext += f"Mean: {y_vals[idx]:.2f}uSec<br>"
383         if isinstance(y_mins[idx], float):
384             hovertext += f"Min: {y_mins[idx]:.2f}uSec"
385
386         if isinstance(y_maxs[idx], float) and isinstance(y_vals[idx], float):
387             array = [y_maxs[idx] - y_vals[idx], ]
388         else:
389             array = [None, ]
390         if isinstance(y_mins[idx], float) and isinstance(y_vals[idx], float):
391             arrayminus = [y_vals[idx] - y_mins[idx], ]
392         else:
393             arrayminus = [None, ]
394         traces.append(plgo.Scatter(
395             x=[idx, ],
396             y=[y_vals[idx], ],
397             name=x_vals[idx],
398             legendgroup=x_vals[idx],
399             showlegend=bool(int(idx % 2)),
400             mode=u"markers",
401             error_y=dict(
402                 type=u"data",
403                 symmetric=False,
404                 array=array,
405                 arrayminus=arrayminus,
406                 color=COLORS[int(idx / 2)]
407             ),
408             marker=dict(
409                 size=10,
410                 color=COLORS[int(idx / 2)],
411             ),
412             text=hovertext,
413             hoverinfo=u"text",
414         ))
415         annotations.append(dict(
416             x=idx,
417             y=0,
418             xref=u"x",
419             yref=u"y",
420             xanchor=u"center",
421             yanchor=u"top",
422             text=u"E-W" if bool(int(idx % 2)) else u"W-E",
423             font=dict(
424                 size=16,
425             ),
426             align=u"center",
427             showarrow=False
428         ))
429
430     try:
431         # Create plot
432         file_type = plot.get(u"output-file-type", u".html")
433         logging.info(f"    Writing file {plot[u'output-file']}{file_type}.")
434         layout = deepcopy(plot[u"layout"])
435         if layout.get(u"title", None):
436             layout[u"title"] = f"<b>Latency:</b> {layout[u'title']}"
437         layout[u"annotations"] = annotations
438         plpl = plgo.Figure(data=traces, layout=layout)
439
440         # Export Plot
441         ploff.plot(
442             plpl,
443             show_link=False, auto_open=False,
444             filename=f"{plot[u'output-file']}{file_type}"
445         )
446     except PlotlyError as err:
447         logging.error(
448             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
449         )
450         return
451
452
453 def plot_tsa_name(plot, input_data):
454     """Generate the plot(s) with algorithm:
455     plot_tsa_name
456     specified in the specification file.
457
458     :param plot: Plot to generate.
459     :param input_data: Data to process.
460     :type plot: pandas.Series
461     :type input_data: InputData
462     """
463
464     # Transform the data
465     plot_title = plot.get(u"title", u"")
466     logging.info(
467         f"    Creating data set for the {plot.get(u'type', u'')} {plot_title}."
468     )
469     data = input_data.filter_tests_by_name(
470         plot, params=[u"throughput", u"parent", u"tags", u"type"])
471     if data is None:
472         logging.error(u"No data.")
473         return
474
475     y_vals = OrderedDict()
476     for job in data:
477         for build in job:
478             for test in build:
479                 if y_vals.get(test[u"parent"], None) is None:
480                     y_vals[test[u"parent"]] = {
481                         u"1": list(),
482                         u"2": list(),
483                         u"4": list()
484                     }
485                 try:
486                     if test[u"type"] not in (u"NDRPDR",):
487                         continue
488
489                     if u"-pdr" in plot_title.lower():
490                         ttype = u"PDR"
491                     elif u"-ndr" in plot_title.lower():
492                         ttype = u"NDR"
493                     else:
494                         continue
495
496                     if u"1C" in test[u"tags"]:
497                         y_vals[test[u"parent"]][u"1"]. \
498                             append(test[u"throughput"][ttype][u"LOWER"])
499                     elif u"2C" in test[u"tags"]:
500                         y_vals[test[u"parent"]][u"2"]. \
501                             append(test[u"throughput"][ttype][u"LOWER"])
502                     elif u"4C" in test[u"tags"]:
503                         y_vals[test[u"parent"]][u"4"]. \
504                             append(test[u"throughput"][ttype][u"LOWER"])
505                 except (KeyError, TypeError):
506                     pass
507
508     if not y_vals:
509         logging.warning(f"No data for the plot {plot.get(u'title', u'')}")
510         return
511
512     y_1c_max = dict()
513     for test_name, test_vals in y_vals.items():
514         for key, test_val in test_vals.items():
515             if test_val:
516                 avg_val = sum(test_val) / len(test_val)
517                 y_vals[test_name][key] = [avg_val, len(test_val)]
518                 ideal = avg_val / (int(key) * 1000000.0)
519                 if test_name not in y_1c_max or ideal > y_1c_max[test_name]:
520                     y_1c_max[test_name] = ideal
521
522     vals = OrderedDict()
523     y_max = list()
524     nic_limit = 0
525     lnk_limit = 0
526     pci_limit = plot[u"limits"][u"pci"][u"pci-g3-x8"]
527     for test_name, test_vals in y_vals.items():
528         try:
529             if test_vals[u"1"][1]:
530                 name = re.sub(
531                     REGEX_NIC,
532                     u"",
533                     test_name.replace(u'-ndrpdr', u'').replace(u'2n1l-', u'')
534                 )
535                 vals[name] = OrderedDict()
536                 y_val_1 = test_vals[u"1"][0] / 1000000.0
537                 y_val_2 = test_vals[u"2"][0] / 1000000.0 if test_vals[u"2"][0] \
538                     else None
539                 y_val_4 = test_vals[u"4"][0] / 1000000.0 if test_vals[u"4"][0] \
540                     else None
541
542                 vals[name][u"val"] = [y_val_1, y_val_2, y_val_4]
543                 vals[name][u"rel"] = [1.0, None, None]
544                 vals[name][u"ideal"] = [
545                     y_1c_max[test_name],
546                     y_1c_max[test_name] * 2,
547                     y_1c_max[test_name] * 4
548                 ]
549                 vals[name][u"diff"] = [
550                     (y_val_1 - y_1c_max[test_name]) * 100 / y_val_1, None, None
551                 ]
552                 vals[name][u"count"] = [
553                     test_vals[u"1"][1],
554                     test_vals[u"2"][1],
555                     test_vals[u"4"][1]
556                 ]
557
558                 try:
559                     val_max = max(vals[name][u"val"])
560                 except ValueError as err:
561                     logging.error(repr(err))
562                     continue
563                 if val_max:
564                     y_max.append(val_max)
565
566                 if y_val_2:
567                     vals[name][u"rel"][1] = round(y_val_2 / y_val_1, 2)
568                     vals[name][u"diff"][1] = \
569                         (y_val_2 - vals[name][u"ideal"][1]) * 100 / y_val_2
570                 if y_val_4:
571                     vals[name][u"rel"][2] = round(y_val_4 / y_val_1, 2)
572                     vals[name][u"diff"][2] = \
573                         (y_val_4 - vals[name][u"ideal"][2]) * 100 / y_val_4
574         except IndexError as err:
575             logging.warning(f"No data for {test_name}")
576             logging.warning(repr(err))
577
578         # Limits:
579         if u"x520" in test_name:
580             limit = plot[u"limits"][u"nic"][u"x520"]
581         elif u"x710" in test_name:
582             limit = plot[u"limits"][u"nic"][u"x710"]
583         elif u"xxv710" in test_name:
584             limit = plot[u"limits"][u"nic"][u"xxv710"]
585         elif u"xl710" in test_name:
586             limit = plot[u"limits"][u"nic"][u"xl710"]
587         elif u"x553" in test_name:
588             limit = plot[u"limits"][u"nic"][u"x553"]
589         else:
590             limit = 0
591         if limit > nic_limit:
592             nic_limit = limit
593
594         mul = 2 if u"ge2p" in test_name else 1
595         if u"10ge" in test_name:
596             limit = plot[u"limits"][u"link"][u"10ge"] * mul
597         elif u"25ge" in test_name:
598             limit = plot[u"limits"][u"link"][u"25ge"] * mul
599         elif u"40ge" in test_name:
600             limit = plot[u"limits"][u"link"][u"40ge"] * mul
601         elif u"100ge" in test_name:
602             limit = plot[u"limits"][u"link"][u"100ge"] * mul
603         else:
604             limit = 0
605         if limit > lnk_limit:
606             lnk_limit = limit
607
608     traces = list()
609     annotations = list()
610     x_vals = [1, 2, 4]
611
612     # Limits:
613     try:
614         threshold = 1.1 * max(y_max)  # 10%
615     except ValueError as err:
616         logging.error(err)
617         return
618     nic_limit /= 1000000.0
619     traces.append(plgo.Scatter(
620         x=x_vals,
621         y=[nic_limit, ] * len(x_vals),
622         name=f"NIC: {nic_limit:.2f}Mpps",
623         showlegend=False,
624         mode=u"lines",
625         line=dict(
626             dash=u"dot",
627             color=COLORS[-1],
628             width=1),
629         hoverinfo=u"none"
630     ))
631     annotations.append(dict(
632         x=1,
633         y=nic_limit,
634         xref=u"x",
635         yref=u"y",
636         xanchor=u"left",
637         yanchor=u"bottom",
638         text=f"NIC: {nic_limit:.2f}Mpps",
639         font=dict(
640             size=14,
641             color=COLORS[-1],
642         ),
643         align=u"left",
644         showarrow=False
645     ))
646     y_max.append(nic_limit)
647
648     lnk_limit /= 1000000.0
649     if lnk_limit < threshold:
650         traces.append(plgo.Scatter(
651             x=x_vals,
652             y=[lnk_limit, ] * len(x_vals),
653             name=f"Link: {lnk_limit:.2f}Mpps",
654             showlegend=False,
655             mode=u"lines",
656             line=dict(
657                 dash=u"dot",
658                 color=COLORS[-2],
659                 width=1),
660             hoverinfo=u"none"
661         ))
662         annotations.append(dict(
663             x=1,
664             y=lnk_limit,
665             xref=u"x",
666             yref=u"y",
667             xanchor=u"left",
668             yanchor=u"bottom",
669             text=f"Link: {lnk_limit:.2f}Mpps",
670             font=dict(
671                 size=14,
672                 color=COLORS[-2],
673             ),
674             align=u"left",
675             showarrow=False
676         ))
677         y_max.append(lnk_limit)
678
679     pci_limit /= 1000000.0
680     if (pci_limit < threshold and
681             (pci_limit < lnk_limit * 0.95 or lnk_limit > lnk_limit * 1.05)):
682         traces.append(plgo.Scatter(
683             x=x_vals,
684             y=[pci_limit, ] * len(x_vals),
685             name=f"PCIe: {pci_limit:.2f}Mpps",
686             showlegend=False,
687             mode=u"lines",
688             line=dict(
689                 dash=u"dot",
690                 color=COLORS[-3],
691                 width=1),
692             hoverinfo=u"none"
693         ))
694         annotations.append(dict(
695             x=1,
696             y=pci_limit,
697             xref=u"x",
698             yref=u"y",
699             xanchor=u"left",
700             yanchor=u"bottom",
701             text=f"PCIe: {pci_limit:.2f}Mpps",
702             font=dict(
703                 size=14,
704                 color=COLORS[-3],
705             ),
706             align=u"left",
707             showarrow=False
708         ))
709         y_max.append(pci_limit)
710
711     # Perfect and measured:
712     cidx = 0
713     for name, val in vals.items():
714         hovertext = list()
715         try:
716             for idx in range(len(val[u"val"])):
717                 htext = ""
718                 if isinstance(val[u"val"][idx], float):
719                     htext += (
720                         f"No. of Runs: {val[u'count'][idx]}<br>"
721                         f"Mean: {val[u'val'][idx]:.2f}Mpps<br>"
722                     )
723                 if isinstance(val[u"diff"][idx], float):
724                     htext += f"Diff: {round(val[u'diff'][idx]):.0f}%<br>"
725                 if isinstance(val[u"rel"][idx], float):
726                     htext += f"Speedup: {val[u'rel'][idx]:.2f}"
727                 hovertext.append(htext)
728             traces.append(
729                 plgo.Scatter(
730                     x=x_vals,
731                     y=val[u"val"],
732                     name=name,
733                     legendgroup=name,
734                     mode=u"lines+markers",
735                     line=dict(
736                         color=COLORS[cidx],
737                         width=2),
738                     marker=dict(
739                         symbol=u"circle",
740                         size=10
741                     ),
742                     text=hovertext,
743                     hoverinfo=u"text+name"
744                 )
745             )
746             traces.append(
747                 plgo.Scatter(
748                     x=x_vals,
749                     y=val[u"ideal"],
750                     name=f"{name} perfect",
751                     legendgroup=name,
752                     showlegend=False,
753                     mode=u"lines",
754                     line=dict(
755                         color=COLORS[cidx],
756                         width=2,
757                         dash=u"dash"),
758                     text=[f"Perfect: {y:.2f}Mpps" for y in val[u"ideal"]],
759                     hoverinfo=u"text"
760                 )
761             )
762             cidx += 1
763         except (IndexError, ValueError, KeyError) as err:
764             logging.warning(f"No data for {name}\n{repr(err)}")
765
766     try:
767         # Create plot
768         file_type = plot.get(u"output-file-type", u".html")
769         logging.info(f"    Writing file {plot[u'output-file']}{file_type}.")
770         layout = deepcopy(plot[u"layout"])
771         if layout.get(u"title", None):
772             layout[u"title"] = f"<b>Speedup Multi-core:</b> {layout[u'title']}"
773         layout[u"yaxis"][u"range"] = [0, int(max(y_max) * 1.1)]
774         layout[u"annotations"].extend(annotations)
775         plpl = plgo.Figure(data=traces, layout=layout)
776
777         # Export Plot
778         ploff.plot(
779             plpl,
780             show_link=False,
781             auto_open=False,
782             filename=f"{plot[u'output-file']}{file_type}"
783         )
784     except PlotlyError as err:
785         logging.error(
786             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
787         )
788         return
789
790
791 def plot_http_server_perf_box(plot, input_data):
792     """Generate the plot(s) with algorithm: plot_http_server_perf_box
793     specified in the specification file.
794
795     :param plot: Plot to generate.
796     :param input_data: Data to process.
797     :type plot: pandas.Series
798     :type input_data: InputData
799     """
800
801     # Transform the data
802     logging.info(
803         f"    Creating the data set for the {plot.get(u'type', u'')} "
804         f"{plot.get(u'title', u'')}."
805     )
806     data = input_data.filter_data(plot)
807     if data is None:
808         logging.error(u"No data.")
809         return
810
811     # Prepare the data for the plot
812     y_vals = dict()
813     for job in data:
814         for build in job:
815             for test in build:
816                 if y_vals.get(test[u"name"], None) is None:
817                     y_vals[test[u"name"]] = list()
818                 try:
819                     y_vals[test[u"name"]].append(test[u"result"])
820                 except (KeyError, TypeError):
821                     y_vals[test[u"name"]].append(None)
822
823     # Add None to the lists with missing data
824     max_len = 0
825     nr_of_samples = list()
826     for val in y_vals.values():
827         if len(val) > max_len:
828             max_len = len(val)
829         nr_of_samples.append(len(val))
830     for val in y_vals.values():
831         if len(val) < max_len:
832             val.extend([None for _ in range(max_len - len(val))])
833
834     # Add plot traces
835     traces = list()
836     df_y = pd.DataFrame(y_vals)
837     df_y.head()
838     for i, col in enumerate(df_y.columns):
839         name = \
840             f"{i + 1}. " \
841             f"({nr_of_samples[i]:02d} " \
842             f"run{u's' if nr_of_samples[i] > 1 else u''}) " \
843             f"{col.lower().replace(u'-ndrpdr', u'')}"
844         if len(name) > 50:
845             name_lst = name.split(u'-')
846             name = u""
847             split_name = True
848             for segment in name_lst:
849                 if (len(name) + len(segment) + 1) > 50 and split_name:
850                     name += u"<br>    "
851                     split_name = False
852                 name += segment + u'-'
853             name = name[:-1]
854
855         traces.append(plgo.Box(x=[str(i + 1) + u'.'] * len(df_y[col]),
856                                y=df_y[col],
857                                name=name,
858                                **plot[u"traces"]))
859     try:
860         # Create plot
861         plpl = plgo.Figure(data=traces, layout=plot[u"layout"])
862
863         # Export Plot
864         logging.info(
865             f"    Writing file {plot[u'output-file']}"
866             f"{plot[u'output-file-type']}."
867         )
868         ploff.plot(
869             plpl,
870             show_link=False,
871             auto_open=False,
872             filename=f"{plot[u'output-file']}{plot[u'output-file-type']}"
873         )
874     except PlotlyError as err:
875         logging.error(
876             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
877         )
878         return
879
880
881 def plot_nf_heatmap(plot, input_data):
882     """Generate the plot(s) with algorithm: plot_nf_heatmap
883     specified in the specification file.
884
885     :param plot: Plot to generate.
886     :param input_data: Data to process.
887     :type plot: pandas.Series
888     :type input_data: InputData
889     """
890
891     regex_cn = re.compile(r'^(\d*)R(\d*)C$')
892     regex_test_name = re.compile(r'^.*-(\d+ch|\d+pl)-'
893                                  r'(\d+mif|\d+vh)-'
894                                  r'(\d+vm\d+t|\d+dcr\d+t).*$')
895     vals = dict()
896
897     # Transform the data
898     logging.info(
899         f"    Creating the data set for the {plot.get(u'type', u'')} "
900         f"{plot.get(u'title', u'')}."
901     )
902     data = input_data.filter_data(plot, continue_on_error=True)
903     if data is None or data.empty:
904         logging.error(u"No data.")
905         return
906
907     for job in data:
908         for build in job:
909             for test in build:
910                 for tag in test[u"tags"]:
911                     groups = re.search(regex_cn, tag)
912                     if groups:
913                         chain = str(groups.group(1))
914                         node = str(groups.group(2))
915                         break
916                 else:
917                     continue
918                 groups = re.search(regex_test_name, test[u"name"])
919                 if groups and len(groups.groups()) == 3:
920                     hover_name = (
921                         f"{str(groups.group(1))}-"
922                         f"{str(groups.group(2))}-"
923                         f"{str(groups.group(3))}"
924                     )
925                 else:
926                     hover_name = u""
927                 if vals.get(chain, None) is None:
928                     vals[chain] = dict()
929                 if vals[chain].get(node, None) is None:
930                     vals[chain][node] = dict(
931                         name=hover_name,
932                         vals=list(),
933                         nr=None,
934                         mean=None,
935                         stdev=None
936                     )
937                 try:
938                     if plot[u"include-tests"] == u"MRR":
939                         result = test[u"result"][u"receive-rate"]
940                     elif plot[u"include-tests"] == u"PDR":
941                         result = test[u"throughput"][u"PDR"][u"LOWER"]
942                     elif plot[u"include-tests"] == u"NDR":
943                         result = test[u"throughput"][u"NDR"][u"LOWER"]
944                     else:
945                         result = None
946                 except TypeError:
947                     result = None
948
949                 if result:
950                     vals[chain][node][u"vals"].append(result)
951
952     if not vals:
953         logging.error(u"No data.")
954         return
955
956     txt_chains = list()
957     txt_nodes = list()
958     for key_c in vals:
959         txt_chains.append(key_c)
960         for key_n in vals[key_c].keys():
961             txt_nodes.append(key_n)
962             if vals[key_c][key_n][u"vals"]:
963                 vals[key_c][key_n][u"nr"] = len(vals[key_c][key_n][u"vals"])
964                 vals[key_c][key_n][u"mean"] = \
965                     round(mean(vals[key_c][key_n][u"vals"]) / 1000000, 1)
966                 vals[key_c][key_n][u"stdev"] = \
967                     round(stdev(vals[key_c][key_n][u"vals"]) / 1000000, 1)
968     txt_nodes = list(set(txt_nodes))
969
970     def sort_by_int(value):
971         """Makes possible to sort a list of strings which represent integers.
972
973         :param value: Integer as a string.
974         :type value: str
975         :returns: Integer representation of input parameter 'value'.
976         :rtype: int
977         """
978         return int(value)
979
980     txt_chains = sorted(txt_chains, key=sort_by_int)
981     txt_nodes = sorted(txt_nodes, key=sort_by_int)
982
983     chains = [i + 1 for i in range(len(txt_chains))]
984     nodes = [i + 1 for i in range(len(txt_nodes))]
985
986     data = [list() for _ in range(len(chains))]
987     for chain in chains:
988         for node in nodes:
989             try:
990                 val = vals[txt_chains[chain - 1]][txt_nodes[node - 1]][u"mean"]
991             except (KeyError, IndexError):
992                 val = None
993             data[chain - 1].append(val)
994
995     # Color scales:
996     my_green = [[0.0, u"rgb(235, 249, 242)"],
997                 [1.0, u"rgb(45, 134, 89)"]]
998
999     my_blue = [[0.0, u"rgb(236, 242, 248)"],
1000                [1.0, u"rgb(57, 115, 172)"]]
1001
1002     my_grey = [[0.0, u"rgb(230, 230, 230)"],
1003                [1.0, u"rgb(102, 102, 102)"]]
1004
1005     hovertext = list()
1006     annotations = list()
1007
1008     text = (u"Test: {name}<br>"
1009             u"Runs: {nr}<br>"
1010             u"Thput: {val}<br>"
1011             u"StDev: {stdev}")
1012
1013     for chain, _ in enumerate(txt_chains):
1014         hover_line = list()
1015         for node, _ in enumerate(txt_nodes):
1016             if data[chain][node] is not None:
1017                 annotations.append(
1018                     dict(
1019                         x=node+1,
1020                         y=chain+1,
1021                         xref=u"x",
1022                         yref=u"y",
1023                         xanchor=u"center",
1024                         yanchor=u"middle",
1025                         text=str(data[chain][node]),
1026                         font=dict(
1027                             size=14,
1028                         ),
1029                         align=u"center",
1030                         showarrow=False
1031                     )
1032                 )
1033                 hover_line.append(text.format(
1034                     name=vals[txt_chains[chain]][txt_nodes[node]][u"name"],
1035                     nr=vals[txt_chains[chain]][txt_nodes[node]][u"nr"],
1036                     val=data[chain][node],
1037                     stdev=vals[txt_chains[chain]][txt_nodes[node]][u"stdev"]))
1038         hovertext.append(hover_line)
1039
1040     traces = [
1041         plgo.Heatmap(
1042             x=nodes,
1043             y=chains,
1044             z=data,
1045             colorbar=dict(
1046                 title=plot.get(u"z-axis", u""),
1047                 titleside=u"right",
1048                 titlefont=dict(
1049                     size=16
1050                 ),
1051                 tickfont=dict(
1052                     size=16,
1053                 ),
1054                 tickformat=u".1f",
1055                 yanchor=u"bottom",
1056                 y=-0.02,
1057                 len=0.925,
1058             ),
1059             showscale=True,
1060             colorscale=my_green,
1061             text=hovertext,
1062             hoverinfo=u"text"
1063         )
1064     ]
1065
1066     for idx, item in enumerate(txt_nodes):
1067         # X-axis, numbers:
1068         annotations.append(
1069             dict(
1070                 x=idx+1,
1071                 y=0.05,
1072                 xref=u"x",
1073                 yref=u"y",
1074                 xanchor=u"center",
1075                 yanchor=u"top",
1076                 text=item,
1077                 font=dict(
1078                     size=16,
1079                 ),
1080                 align=u"center",
1081                 showarrow=False
1082             )
1083         )
1084     for idx, item in enumerate(txt_chains):
1085         # Y-axis, numbers:
1086         annotations.append(
1087             dict(
1088                 x=0.35,
1089                 y=idx+1,
1090                 xref=u"x",
1091                 yref=u"y",
1092                 xanchor=u"right",
1093                 yanchor=u"middle",
1094                 text=item,
1095                 font=dict(
1096                     size=16,
1097                 ),
1098                 align=u"center",
1099                 showarrow=False
1100             )
1101         )
1102     # X-axis, title:
1103     annotations.append(
1104         dict(
1105             x=0.55,
1106             y=-0.15,
1107             xref=u"paper",
1108             yref=u"y",
1109             xanchor=u"center",
1110             yanchor=u"bottom",
1111             text=plot.get(u"x-axis", u""),
1112             font=dict(
1113                 size=16,
1114             ),
1115             align=u"center",
1116             showarrow=False
1117         )
1118     )
1119     # Y-axis, title:
1120     annotations.append(
1121         dict(
1122             x=-0.1,
1123             y=0.5,
1124             xref=u"x",
1125             yref=u"paper",
1126             xanchor=u"center",
1127             yanchor=u"middle",
1128             text=plot.get(u"y-axis", u""),
1129             font=dict(
1130                 size=16,
1131             ),
1132             align=u"center",
1133             textangle=270,
1134             showarrow=False
1135         )
1136     )
1137     updatemenus = list([
1138         dict(
1139             x=1.0,
1140             y=0.0,
1141             xanchor=u"right",
1142             yanchor=u"bottom",
1143             direction=u"up",
1144             buttons=list([
1145                 dict(
1146                     args=[
1147                         {
1148                             u"colorscale": [my_green, ],
1149                             u"reversescale": False
1150                         }
1151                     ],
1152                     label=u"Green",
1153                     method=u"update"
1154                 ),
1155                 dict(
1156                     args=[
1157                         {
1158                             u"colorscale": [my_blue, ],
1159                             u"reversescale": False
1160                         }
1161                     ],
1162                     label=u"Blue",
1163                     method=u"update"
1164                 ),
1165                 dict(
1166                     args=[
1167                         {
1168                             u"colorscale": [my_grey, ],
1169                             u"reversescale": False
1170                         }
1171                     ],
1172                     label=u"Grey",
1173                     method=u"update"
1174                 )
1175             ])
1176         )
1177     ])
1178
1179     try:
1180         layout = deepcopy(plot[u"layout"])
1181     except KeyError as err:
1182         logging.error(f"Finished with error: No layout defined\n{repr(err)}")
1183         return
1184
1185     layout[u"annotations"] = annotations
1186     layout[u'updatemenus'] = updatemenus
1187
1188     try:
1189         # Create plot
1190         plpl = plgo.Figure(data=traces, layout=layout)
1191
1192         # Export Plot
1193         logging.info(
1194             f"    Writing file {plot[u'output-file']}"
1195             f"{plot[u'output-file-type']}."
1196         )
1197         ploff.plot(
1198             plpl,
1199             show_link=False,
1200             auto_open=False,
1201             filename=f"{plot[u'output-file']}{plot[u'output-file-type']}"
1202         )
1203     except PlotlyError as err:
1204         logging.error(
1205             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
1206         )
1207         return