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