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