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