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