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