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