PAL: Fix plots generator
[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.environment[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             elif u"e810cq" in test_name:
1050                 limit = plot[u"limits"][u"nic"][u"e810cq"]
1051             else:
1052                 limit = 0
1053             if limit > nic_limit:
1054                 nic_limit = limit
1055
1056             mul = 2 if u"ge2p" in test_name else 1
1057             if u"10ge" in test_name:
1058                 limit = plot[u"limits"][u"link"][u"10ge"] * mul
1059             elif u"25ge" in test_name:
1060                 limit = plot[u"limits"][u"link"][u"25ge"] * mul
1061             elif u"40ge" in test_name:
1062                 limit = plot[u"limits"][u"link"][u"40ge"] * mul
1063             elif u"100ge" in test_name:
1064                 limit = plot[u"limits"][u"link"][u"100ge"] * mul
1065             else:
1066                 limit = 0
1067             if limit > lnk_limit:
1068                 lnk_limit = limit
1069
1070             if u"cx556a" in test_name:
1071                 limit = plot[u"limits"][u"pci"][u"pci-g3-x8"]
1072             else:
1073                 limit = plot[u"limits"][u"pci"][u"pci-g3-x16"]
1074             if limit > pci_limit:
1075                 pci_limit = limit
1076
1077         traces = list()
1078         annotations = list()
1079         x_vals = [1, 2, 4]
1080
1081         # Limits:
1082         if u"-gbps" not in plot_title and u"-cps-" not in plot_title:
1083             nic_limit /= 1e6
1084             lnk_limit /= 1e6
1085             pci_limit /= 1e6
1086             min_limit = min((nic_limit, lnk_limit, pci_limit))
1087             if nic_limit == min_limit:
1088                 traces.append(plgo.Scatter(
1089                     x=x_vals,
1090                     y=[nic_limit, ] * len(x_vals),
1091                     name=f"NIC: {nic_limit:.2f}Mpps",
1092                     showlegend=False,
1093                     mode=u"lines",
1094                     line=dict(
1095                         dash=u"dot",
1096                         color=COLORS[-1],
1097                         width=1),
1098                     hoverinfo=u"none"
1099                 ))
1100                 annotations.append(dict(
1101                     x=1,
1102                     y=nic_limit,
1103                     xref=u"x",
1104                     yref=u"y",
1105                     xanchor=u"left",
1106                     yanchor=u"bottom",
1107                     text=f"NIC: {nic_limit:.2f}Mpps",
1108                     font=dict(
1109                         size=14,
1110                         color=COLORS[-1],
1111                     ),
1112                     align=u"left",
1113                     showarrow=False
1114                 ))
1115                 y_max.append(nic_limit)
1116             elif lnk_limit == min_limit:
1117                 traces.append(plgo.Scatter(
1118                     x=x_vals,
1119                     y=[lnk_limit, ] * len(x_vals),
1120                     name=f"Link: {lnk_limit:.2f}Mpps",
1121                     showlegend=False,
1122                     mode=u"lines",
1123                     line=dict(
1124                         dash=u"dot",
1125                         color=COLORS[-1],
1126                         width=1),
1127                     hoverinfo=u"none"
1128                 ))
1129                 annotations.append(dict(
1130                     x=1,
1131                     y=lnk_limit,
1132                     xref=u"x",
1133                     yref=u"y",
1134                     xanchor=u"left",
1135                     yanchor=u"bottom",
1136                     text=f"Link: {lnk_limit:.2f}Mpps",
1137                     font=dict(
1138                         size=14,
1139                         color=COLORS[-1],
1140                     ),
1141                     align=u"left",
1142                     showarrow=False
1143                 ))
1144                 y_max.append(lnk_limit)
1145             elif pci_limit == min_limit:
1146                 traces.append(plgo.Scatter(
1147                     x=x_vals,
1148                     y=[pci_limit, ] * len(x_vals),
1149                     name=f"PCIe: {pci_limit:.2f}Mpps",
1150                     showlegend=False,
1151                     mode=u"lines",
1152                     line=dict(
1153                         dash=u"dot",
1154                         color=COLORS[-1],
1155                         width=1),
1156                     hoverinfo=u"none"
1157                 ))
1158                 annotations.append(dict(
1159                     x=1,
1160                     y=pci_limit,
1161                     xref=u"x",
1162                     yref=u"y",
1163                     xanchor=u"left",
1164                     yanchor=u"bottom",
1165                     text=f"PCIe: {pci_limit:.2f}Mpps",
1166                     font=dict(
1167                         size=14,
1168                         color=COLORS[-1],
1169                     ),
1170                     align=u"left",
1171                     showarrow=False
1172                 ))
1173                 y_max.append(pci_limit)
1174
1175         # Perfect and measured:
1176         cidx = 0
1177         for name, val in vals.items():
1178             hovertext = list()
1179             try:
1180                 for idx in range(len(val[u"val"])):
1181                     htext = ""
1182                     if isinstance(val[u"val"][idx], float):
1183                         htext += (
1184                             f"No. of Runs: {val[u'count'][idx]}<br>"
1185                             f"Mean: {val[u'val'][idx]:.2f}{h_unit}<br>"
1186                         )
1187                     if isinstance(val[u"diff"][idx], float):
1188                         htext += f"Diff: {round(val[u'diff'][idx]):.0f}%<br>"
1189                     if isinstance(val[u"rel"][idx], float):
1190                         htext += f"Speedup: {val[u'rel'][idx]:.2f}"
1191                     hovertext.append(htext)
1192                 traces.append(
1193                     plgo.Scatter(
1194                         x=x_vals,
1195                         y=val[u"val"],
1196                         name=name,
1197                         legendgroup=name,
1198                         mode=u"lines+markers",
1199                         line=dict(
1200                             color=COLORS[cidx],
1201                             width=2),
1202                         marker=dict(
1203                             symbol=u"circle",
1204                             size=10
1205                         ),
1206                         text=hovertext,
1207                         hoverinfo=u"text+name"
1208                     )
1209                 )
1210                 traces.append(
1211                     plgo.Scatter(
1212                         x=x_vals,
1213                         y=val[u"ideal"],
1214                         name=f"{name} perfect",
1215                         legendgroup=name,
1216                         showlegend=False,
1217                         mode=u"lines",
1218                         line=dict(
1219                             color=COLORS[cidx],
1220                             width=2,
1221                             dash=u"dash"),
1222                         text=[f"Perfect: {y:.2f}Mpps" for y in val[u"ideal"]],
1223                         hoverinfo=u"text"
1224                     )
1225                 )
1226                 cidx += 1
1227             except (IndexError, ValueError, KeyError) as err:
1228                 logging.warning(f"No data for {name}\n{repr(err)}")
1229
1230         try:
1231             # Create plot
1232             file_name = f"{plot[u'output-file'].format(test_type=ttype)}.html"
1233             logging.info(f"    Writing file {file_name}")
1234             layout = deepcopy(plot[u"layout"])
1235             if layout.get(u"title", None):
1236                 layout[u"title"] = (
1237                     f"<b>Speedup Multi-core:</b> "
1238                     f"{layout[u'title'].format(test_type=ttype)}"
1239                 )
1240             layout[u"yaxis"][u"range"] = [0, int(max(y_max) * 1.1)]
1241             layout[u"annotations"].extend(annotations)
1242             plpl = plgo.Figure(data=traces, layout=layout)
1243
1244             # Export Plot
1245             ploff.plot(
1246                 plpl,
1247                 show_link=False,
1248                 auto_open=False,
1249                 filename=file_name
1250             )
1251         except PlotlyError as err:
1252             logging.error(
1253                 f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
1254             )
1255
1256
1257 def plot_http_server_perf_box(plot, input_data):
1258     """Generate the plot(s) with algorithm: plot_http_server_perf_box
1259     specified in the specification file.
1260
1261     :param plot: Plot to generate.
1262     :param input_data: Data to process.
1263     :type plot: pandas.Series
1264     :type input_data: InputData
1265     """
1266
1267     # Transform the data
1268     logging.info(
1269         f"    Creating the data set for the {plot.get(u'type', u'')} "
1270         f"{plot.get(u'title', u'')}."
1271     )
1272     data = input_data.filter_data(plot)
1273     if data is None:
1274         logging.error(u"No data.")
1275         return
1276
1277     # Prepare the data for the plot
1278     y_vals = dict()
1279     for job in data:
1280         for build in job:
1281             for test in build:
1282                 if y_vals.get(test[u"name"], None) is None:
1283                     y_vals[test[u"name"]] = list()
1284                 try:
1285                     y_vals[test[u"name"]].append(test[u"result"])
1286                 except (KeyError, TypeError):
1287                     y_vals[test[u"name"]].append(None)
1288
1289     # Add None to the lists with missing data
1290     max_len = 0
1291     nr_of_samples = list()
1292     for val in y_vals.values():
1293         if len(val) > max_len:
1294             max_len = len(val)
1295         nr_of_samples.append(len(val))
1296     for val in y_vals.values():
1297         if len(val) < max_len:
1298             val.extend([None for _ in range(max_len - len(val))])
1299
1300     # Add plot traces
1301     traces = list()
1302     df_y = pd.DataFrame(y_vals)
1303     df_y.head()
1304     for i, col in enumerate(df_y.columns):
1305         name = \
1306             f"{i + 1}. " \
1307             f"({nr_of_samples[i]:02d} " \
1308             f"run{u's' if nr_of_samples[i] > 1 else u''}) " \
1309             f"{col.lower().replace(u'-ndrpdr', u'')}"
1310         if len(name) > 50:
1311             name_lst = name.split(u'-')
1312             name = u""
1313             split_name = True
1314             for segment in name_lst:
1315                 if (len(name) + len(segment) + 1) > 50 and split_name:
1316                     name += u"<br>    "
1317                     split_name = False
1318                 name += segment + u'-'
1319             name = name[:-1]
1320
1321         traces.append(plgo.Box(x=[str(i + 1) + u'.'] * len(df_y[col]),
1322                                y=df_y[col],
1323                                name=name,
1324                                **plot[u"traces"]))
1325     try:
1326         # Create plot
1327         plpl = plgo.Figure(data=traces, layout=plot[u"layout"])
1328
1329         # Export Plot
1330         logging.info(
1331             f"    Writing file {plot[u'output-file']}"
1332             f"{plot[u'output-file-type']}."
1333         )
1334         ploff.plot(
1335             plpl,
1336             show_link=False,
1337             auto_open=False,
1338             filename=f"{plot[u'output-file']}{plot[u'output-file-type']}"
1339         )
1340     except PlotlyError as err:
1341         logging.error(
1342             f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
1343         )
1344         return
1345
1346
1347 def plot_nf_heatmap(plot, input_data):
1348     """Generate the plot(s) with algorithm: plot_nf_heatmap
1349     specified in the specification file.
1350
1351     :param plot: Plot to generate.
1352     :param input_data: Data to process.
1353     :type plot: pandas.Series
1354     :type input_data: InputData
1355     """
1356
1357     def sort_by_int(value):
1358         """Makes possible to sort a list of strings which represent integers.
1359
1360         :param value: Integer as a string.
1361         :type value: str
1362         :returns: Integer representation of input parameter 'value'.
1363         :rtype: int
1364         """
1365         return int(value)
1366
1367     regex_cn = re.compile(r'^(\d*)R(\d*)C$')
1368     regex_test_name = re.compile(r'^.*-(\d+ch|\d+pl)-'
1369                                  r'(\d+mif|\d+vh)-'
1370                                  r'(\d+vm\d+t|\d+dcr\d+t|\d+dcr\d+c).*$')
1371     vals = dict()
1372
1373     # Transform the data
1374     logging.info(
1375         f"    Creating the data set for the {plot.get(u'type', u'')} "
1376         f"{plot.get(u'title', u'')}."
1377     )
1378     in_data = input_data.filter_tests_by_name(
1379         plot,
1380         continue_on_error=True,
1381         params=[u"throughput", u"result", u"name", u"tags", u"type"]
1382     )
1383     if in_data is None or in_data.empty:
1384         logging.error(u"No data.")
1385         return
1386
1387     for ttype in plot.get(u"test-type", (u"ndr", u"pdr")):
1388         for core in plot.get(u"core", tuple()):
1389             for item in plot.get(u"include", tuple()):
1390                 reg_ex = re.compile(str(item.format(core=core)).lower())
1391                 for job in in_data:
1392                     for build in job:
1393                         for test_id, test in build.iteritems():
1394                             if not re.match(reg_ex, str(test_id).lower()):
1395                                 continue
1396                             for tag in test[u"tags"]:
1397                                 groups = re.search(regex_cn, tag)
1398                                 if groups:
1399                                     chain = str(groups.group(1))
1400                                     node = str(groups.group(2))
1401                                     break
1402                             else:
1403                                 continue
1404                             groups = re.search(regex_test_name, test[u"name"])
1405                             if groups and len(groups.groups()) == 3:
1406                                 hover_name = (
1407                                     f"{str(groups.group(1))}-"
1408                                     f"{str(groups.group(2))}-"
1409                                     f"{str(groups.group(3))}"
1410                                 )
1411                             else:
1412                                 hover_name = u""
1413                             if vals.get(chain, None) is None:
1414                                 vals[chain] = dict()
1415                             if vals[chain].get(node, None) is None:
1416                                 vals[chain][node] = dict(
1417                                     name=hover_name,
1418                                     vals=list(),
1419                                     nr=None,
1420                                     mean=None,
1421                                     stdev=None
1422                                 )
1423                             try:
1424                                 if ttype == u"mrr":
1425                                     result = test[u"result"][u"receive-rate"]
1426                                 elif ttype == u"pdr":
1427                                     result = \
1428                                         test[u"throughput"][u"PDR"][u"LOWER"]
1429                                 elif ttype == u"ndr":
1430                                     result = \
1431                                         test[u"throughput"][u"NDR"][u"LOWER"]
1432                                 else:
1433                                     result = None
1434                             except TypeError:
1435                                 result = None
1436
1437                             if result:
1438                                 vals[chain][node][u"vals"].append(result)
1439
1440             if not vals:
1441                 logging.error(u"No data.")
1442                 return
1443
1444             txt_chains = list()
1445             txt_nodes = list()
1446             for key_c in vals:
1447                 txt_chains.append(key_c)
1448                 for key_n in vals[key_c].keys():
1449                     txt_nodes.append(key_n)
1450                     if vals[key_c][key_n][u"vals"]:
1451                         vals[key_c][key_n][u"nr"] = \
1452                             len(vals[key_c][key_n][u"vals"])
1453                         vals[key_c][key_n][u"mean"] = \
1454                             round(mean(vals[key_c][key_n][u"vals"]) / 1e6, 1)
1455                         vals[key_c][key_n][u"stdev"] = \
1456                             round(stdev(vals[key_c][key_n][u"vals"]) / 1e6, 1)
1457             txt_nodes = list(set(txt_nodes))
1458
1459             txt_chains = sorted(txt_chains, key=sort_by_int)
1460             txt_nodes = sorted(txt_nodes, key=sort_by_int)
1461
1462             chains = [i + 1 for i in range(len(txt_chains))]
1463             nodes = [i + 1 for i in range(len(txt_nodes))]
1464
1465             data = [list() for _ in range(len(chains))]
1466             for chain in chains:
1467                 for node in nodes:
1468                     try:
1469                         val = vals[txt_chains[chain - 1]] \
1470                             [txt_nodes[node - 1]][u"mean"]
1471                     except (KeyError, IndexError):
1472                         val = None
1473                     data[chain - 1].append(val)
1474
1475             # Color scales:
1476             my_green = [[0.0, u"rgb(235, 249, 242)"],
1477                         [1.0, u"rgb(45, 134, 89)"]]
1478
1479             my_blue = [[0.0, u"rgb(236, 242, 248)"],
1480                        [1.0, u"rgb(57, 115, 172)"]]
1481
1482             my_grey = [[0.0, u"rgb(230, 230, 230)"],
1483                        [1.0, u"rgb(102, 102, 102)"]]
1484
1485             hovertext = list()
1486             annotations = list()
1487
1488             text = (u"Test: {name}<br>"
1489                     u"Runs: {nr}<br>"
1490                     u"Thput: {val}<br>"
1491                     u"StDev: {stdev}")
1492
1493             for chain, _ in enumerate(txt_chains):
1494                 hover_line = list()
1495                 for node, _ in enumerate(txt_nodes):
1496                     if data[chain][node] is not None:
1497                         annotations.append(
1498                             dict(
1499                                 x=node+1,
1500                                 y=chain+1,
1501                                 xref=u"x",
1502                                 yref=u"y",
1503                                 xanchor=u"center",
1504                                 yanchor=u"middle",
1505                                 text=str(data[chain][node]),
1506                                 font=dict(
1507                                     size=14,
1508                                 ),
1509                                 align=u"center",
1510                                 showarrow=False
1511                             )
1512                         )
1513                         hover_line.append(text.format(
1514                             name=vals[txt_chains[chain]][txt_nodes[node]]
1515                             [u"name"],
1516                             nr=vals[txt_chains[chain]][txt_nodes[node]][u"nr"],
1517                             val=data[chain][node],
1518                             stdev=vals[txt_chains[chain]][txt_nodes[node]]
1519                             [u"stdev"]
1520                         ))
1521                 hovertext.append(hover_line)
1522
1523             traces = [
1524                 plgo.Heatmap(
1525                     x=nodes,
1526                     y=chains,
1527                     z=data,
1528                     colorbar=dict(
1529                         title=plot.get(u"z-axis", u"{test_type}").
1530                         format(test_type=ttype.upper()),
1531                         titleside=u"right",
1532                         titlefont=dict(
1533                             size=16
1534                         ),
1535                         tickfont=dict(
1536                             size=16,
1537                         ),
1538                         tickformat=u".1f",
1539                         yanchor=u"bottom",
1540                         y=-0.02,
1541                         len=0.925,
1542                     ),
1543                     showscale=True,
1544                     colorscale=my_green,
1545                     text=hovertext,
1546                     hoverinfo=u"text"
1547                 )
1548             ]
1549
1550             for idx, item in enumerate(txt_nodes):
1551                 # X-axis, numbers:
1552                 annotations.append(
1553                     dict(
1554                         x=idx+1,
1555                         y=0.05,
1556                         xref=u"x",
1557                         yref=u"y",
1558                         xanchor=u"center",
1559                         yanchor=u"top",
1560                         text=item,
1561                         font=dict(
1562                             size=16,
1563                         ),
1564                         align=u"center",
1565                         showarrow=False
1566                     )
1567                 )
1568             for idx, item in enumerate(txt_chains):
1569                 # Y-axis, numbers:
1570                 annotations.append(
1571                     dict(
1572                         x=0.35,
1573                         y=idx+1,
1574                         xref=u"x",
1575                         yref=u"y",
1576                         xanchor=u"right",
1577                         yanchor=u"middle",
1578                         text=item,
1579                         font=dict(
1580                             size=16,
1581                         ),
1582                         align=u"center",
1583                         showarrow=False
1584                     )
1585                 )
1586             # X-axis, title:
1587             annotations.append(
1588                 dict(
1589                     x=0.55,
1590                     y=-0.15,
1591                     xref=u"paper",
1592                     yref=u"y",
1593                     xanchor=u"center",
1594                     yanchor=u"bottom",
1595                     text=plot.get(u"x-axis", u""),
1596                     font=dict(
1597                         size=16,
1598                     ),
1599                     align=u"center",
1600                     showarrow=False
1601                 )
1602             )
1603             # Y-axis, title:
1604             annotations.append(
1605                 dict(
1606                     x=-0.1,
1607                     y=0.5,
1608                     xref=u"x",
1609                     yref=u"paper",
1610                     xanchor=u"center",
1611                     yanchor=u"middle",
1612                     text=plot.get(u"y-axis", u""),
1613                     font=dict(
1614                         size=16,
1615                     ),
1616                     align=u"center",
1617                     textangle=270,
1618                     showarrow=False
1619                 )
1620             )
1621             updatemenus = list([
1622                 dict(
1623                     x=1.0,
1624                     y=0.0,
1625                     xanchor=u"right",
1626                     yanchor=u"bottom",
1627                     direction=u"up",
1628                     buttons=list([
1629                         dict(
1630                             args=[
1631                                 {
1632                                     u"colorscale": [my_green, ],
1633                                     u"reversescale": False
1634                                 }
1635                             ],
1636                             label=u"Green",
1637                             method=u"update"
1638                         ),
1639                         dict(
1640                             args=[
1641                                 {
1642                                     u"colorscale": [my_blue, ],
1643                                     u"reversescale": False
1644                                 }
1645                             ],
1646                             label=u"Blue",
1647                             method=u"update"
1648                         ),
1649                         dict(
1650                             args=[
1651                                 {
1652                                     u"colorscale": [my_grey, ],
1653                                     u"reversescale": False
1654                                 }
1655                             ],
1656                             label=u"Grey",
1657                             method=u"update"
1658                         )
1659                     ])
1660                 )
1661             ])
1662
1663             try:
1664                 layout = deepcopy(plot[u"layout"])
1665             except KeyError as err:
1666                 logging.error(
1667                     f"Finished with error: No layout defined\n{repr(err)}"
1668                 )
1669                 return
1670
1671             layout[u"annotations"] = annotations
1672             layout[u'updatemenus'] = updatemenus
1673             if layout.get(u"title", None):
1674                 layout[u"title"] = layout[u'title'].replace(u"test_type", ttype)
1675
1676             try:
1677                 # Create plot
1678                 plpl = plgo.Figure(data=traces, layout=layout)
1679
1680                 # Export Plot
1681                 file_name = (
1682                     f"{plot[u'output-file'].format(core=core, test_type=ttype)}"
1683                     f".html"
1684                 )
1685                 logging.info(f"    Writing file {file_name}")
1686                 ploff.plot(
1687                     plpl,
1688                     show_link=False,
1689                     auto_open=False,
1690                     filename=file_name
1691                 )
1692             except PlotlyError as err:
1693                 logging.error(
1694                     f"   Finished with error: {repr(err)}".replace(u"\n", u" ")
1695                 )