CSIT-1354: Show number of used samples in graphs in report
[csit.git] / resources / tools / presentation / generator_plots.py
1 # Copyright (c) 2018 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 logging
19 import pandas as pd
20 import plotly.offline as ploff
21 import plotly.graph_objs as plgo
22
23 from plotly.exceptions import PlotlyError
24 from collections import OrderedDict
25 from copy import deepcopy
26
27 from utils import mean
28
29
30 COLORS = ["SkyBlue", "Olive", "Purple", "Coral", "Indigo", "Pink",
31           "Chocolate", "Brown", "Magenta", "Cyan", "Orange", "Black",
32           "Violet", "Blue", "Yellow", "BurlyWood", "CadetBlue", "Crimson",
33           "DarkBlue", "DarkCyan", "DarkGreen", "Green", "GoldenRod",
34           "LightGreen", "LightSeaGreen", "LightSkyBlue", "Maroon",
35           "MediumSeaGreen", "SeaGreen", "LightSlateGrey"]
36
37
38 def generate_plots(spec, data):
39     """Generate all plots specified in the specification file.
40
41     :param spec: Specification read from the specification file.
42     :param data: Data to process.
43     :type spec: Specification
44     :type data: InputData
45     """
46
47     logging.info("Generating the plots ...")
48     for index, plot in enumerate(spec.plots):
49         try:
50             logging.info("  Plot nr {0}: {1}".format(index + 1,
51                                                      plot.get("title", "")))
52             plot["limits"] = spec.configuration["limits"]
53             eval(plot["algorithm"])(plot, data)
54             logging.info("  Done.")
55         except NameError as err:
56             logging.error("Probably algorithm '{alg}' is not defined: {err}".
57                           format(alg=plot["algorithm"], err=repr(err)))
58     logging.info("Done.")
59
60
61 def plot_performance_box(plot, input_data):
62     """Generate the plot(s) with algorithm: plot_performance_box
63     specified in the specification file.
64
65     :param plot: Plot to generate.
66     :param input_data: Data to process.
67     :type plot: pandas.Series
68     :type input_data: InputData
69     """
70
71     # Transform the data
72     plot_title = plot.get("title", "")
73     logging.info("    Creating the data set for the {0} '{1}'.".
74                  format(plot.get("type", ""), plot_title))
75     data = input_data.filter_data(plot)
76     if data is None:
77         logging.error("No data.")
78         return
79
80     # Prepare the data for the plot
81     y_vals = dict()
82     y_tags = dict()
83     for job in data:
84         for build in job:
85             for test in build:
86                 if y_vals.get(test["parent"], None) is None:
87                     y_vals[test["parent"]] = list()
88                     y_tags[test["parent"]] = test.get("tags", None)
89                 try:
90                     if test["type"] in ("NDRPDR", ):
91                         if "-pdr" in plot_title.lower():
92                             y_vals[test["parent"]].\
93                                 append(test["throughput"]["PDR"]["LOWER"])
94                         elif "-ndr" in plot_title.lower():
95                             y_vals[test["parent"]]. \
96                                 append(test["throughput"]["NDR"]["LOWER"])
97                         else:
98                             continue
99                     else:
100                         continue
101                 except (KeyError, TypeError):
102                     y_vals[test["parent"]].append(None)
103
104     # Sort the tests
105     order = plot.get("sort", None)
106     if order and y_tags:
107         y_sorted = OrderedDict()
108         y_tags_l = {s: [t.lower() for t in ts] for s, ts in y_tags.items()}
109         for tag in order:
110             logging.debug(tag)
111             for suite, tags in y_tags_l.items():
112                 if "not " in tag:
113                     tag = tag.split(" ")[-1]
114                     if tag.lower() in tags:
115                         continue
116                 else:
117                     if tag.lower() not in tags:
118                         continue
119                 try:
120                     y_sorted[suite] = y_vals.pop(suite)
121                     y_tags_l.pop(suite)
122                     logging.debug(suite)
123                 except KeyError as err:
124                     logging.error("Not found: {0}".format(repr(err)))
125                 finally:
126                     break
127     else:
128         y_sorted = y_vals
129
130     # Add None to the lists with missing data
131     max_len = 0
132     nr_of_samples = list()
133     for val in y_sorted.values():
134         if len(val) > max_len:
135             max_len = len(val)
136         nr_of_samples.append(len(val))
137     for key, val in y_sorted.items():
138         if len(val) < max_len:
139             val.extend([None for _ in range(max_len - len(val))])
140
141     # Add plot traces
142     traces = list()
143     df = pd.DataFrame(y_sorted)
144     df.head()
145     y_max = list()
146     for i, col in enumerate(df.columns):
147         name = "{0}. {1} ({2} run{3})".\
148             format(i + 1,
149                    col.lower().replace('-ndrpdr', ''),
150                    nr_of_samples[i],
151                    's' if nr_of_samples[i] > 1 else '')
152         logging.debug(name)
153         traces.append(plgo.Box(x=[str(i + 1) + '.'] * len(df[col]),
154                                y=[y / 1000000 if y else None for y in df[col]],
155                                name=name,
156                                **plot["traces"]))
157         try:
158             val_max = max(df[col])
159         except ValueError as err:
160             logging.error(repr(err))
161             continue
162         if val_max:
163             y_max.append(int(val_max / 1000000) + 1)
164
165     try:
166         # Create plot
167         layout = deepcopy(plot["layout"])
168         if layout.get("title", None):
169             layout["title"] = "<b>Packet Throughput:</b> {0}". \
170                 format(layout["title"])
171         if y_max:
172             layout["yaxis"]["range"] = [0, max(y_max)]
173         plpl = plgo.Figure(data=traces, layout=layout)
174
175         # Export Plot
176         logging.info("    Writing file '{0}{1}'.".
177                      format(plot["output-file"], plot["output-file-type"]))
178         ploff.plot(plpl, show_link=False, auto_open=False,
179                    filename='{0}{1}'.format(plot["output-file"],
180                                             plot["output-file-type"]))
181     except PlotlyError as err:
182         logging.error("   Finished with error: {}".
183                       format(repr(err).replace("\n", " ")))
184         return
185
186
187 def plot_latency_error_bars(plot, input_data):
188     """Generate the plot(s) with algorithm: plot_latency_error_bars
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     plot_title = plot.get("title", "")
199     logging.info("    Creating the data set for the {0} '{1}'.".
200                  format(plot.get("type", ""), plot_title))
201     data = input_data.filter_data(plot)
202     if data is None:
203         logging.error("No data.")
204         return
205
206     # Prepare the data for the plot
207     y_tmp_vals = dict()
208     y_tags = dict()
209     for job in data:
210         for build in job:
211             for test in build:
212                 try:
213                     logging.debug("test['latency']: {0}\n".
214                                  format(test["latency"]))
215                 except ValueError as err:
216                     logging.warning(repr(err))
217                 if y_tmp_vals.get(test["parent"], None) is None:
218                     y_tmp_vals[test["parent"]] = [
219                         list(),  # direction1, min
220                         list(),  # direction1, avg
221                         list(),  # direction1, max
222                         list(),  # direction2, min
223                         list(),  # direction2, avg
224                         list()   # direction2, max
225                     ]
226                     y_tags[test["parent"]] = test.get("tags", None)
227                 try:
228                     if test["type"] in ("NDRPDR", ):
229                         if "-pdr" in plot_title.lower():
230                             ttype = "PDR"
231                         elif "-ndr" in plot_title.lower():
232                             ttype = "NDR"
233                         else:
234                             logging.warning("Invalid test type: {0}".
235                                             format(test["type"]))
236                             continue
237                         y_tmp_vals[test["parent"]][0].append(
238                             test["latency"][ttype]["direction1"]["min"])
239                         y_tmp_vals[test["parent"]][1].append(
240                             test["latency"][ttype]["direction1"]["avg"])
241                         y_tmp_vals[test["parent"]][2].append(
242                             test["latency"][ttype]["direction1"]["max"])
243                         y_tmp_vals[test["parent"]][3].append(
244                             test["latency"][ttype]["direction2"]["min"])
245                         y_tmp_vals[test["parent"]][4].append(
246                             test["latency"][ttype]["direction2"]["avg"])
247                         y_tmp_vals[test["parent"]][5].append(
248                             test["latency"][ttype]["direction2"]["max"])
249                     else:
250                         logging.warning("Invalid test type: {0}".
251                                         format(test["type"]))
252                         continue
253                 except (KeyError, TypeError) as err:
254                     logging.warning(repr(err))
255     logging.debug("y_tmp_vals: {0}\n".format(y_tmp_vals))
256
257     # Sort the tests
258     order = plot.get("sort", None)
259     if order and y_tags:
260         y_sorted = OrderedDict()
261         y_tags_l = {s: [t.lower() for t in ts] for s, ts in y_tags.items()}
262         for tag in order:
263             logging.debug(tag)
264             for suite, tags in y_tags_l.items():
265                 if "not " in tag:
266                     tag = tag.split(" ")[-1]
267                     if tag.lower() in tags:
268                         continue
269                 else:
270                     if tag.lower() not in tags:
271                         continue
272                 try:
273                     y_sorted[suite] = y_tmp_vals.pop(suite)
274                     y_tags_l.pop(suite)
275                     logging.debug(suite)
276                 except KeyError as err:
277                     logging.error("Not found: {0}".format(repr(err)))
278                 finally:
279                     break
280     else:
281         y_sorted = y_tmp_vals
282
283     logging.debug("y_sorted: {0}\n".format(y_sorted))
284     x_vals = list()
285     y_vals = list()
286     y_mins = list()
287     y_maxs = list()
288     nr_of_samples = list()
289     for key, val in y_sorted.items():
290         key = "-".join(key.split("-")[1:-1])
291         x_vals.append(key)  # dir 1
292         y_vals.append(mean(val[1]) if val[1] else None)
293         y_mins.append(mean(val[0]) if val[0] else None)
294         y_maxs.append(mean(val[2]) if val[2] else None)
295         nr_of_samples.append(len(val[1]) if val[1] else 0)
296         x_vals.append(key)  # dir 2
297         y_vals.append(mean(val[4]) if val[4] else None)
298         y_mins.append(mean(val[3]) if val[3] else None)
299         y_maxs.append(mean(val[5]) if val[5] else None)
300         nr_of_samples.append(len(val[3]) if val[3] else 0)
301
302     logging.debug("x_vals :{0}\n".format(x_vals))
303     logging.debug("y_vals :{0}\n".format(y_vals))
304     logging.debug("y_mins :{0}\n".format(y_mins))
305     logging.debug("y_maxs :{0}\n".format(y_maxs))
306     logging.debug("nr_of_samples :{0}\n".format(nr_of_samples))
307     traces = list()
308     annotations = list()
309
310     for idx in range(len(x_vals)):
311         if not bool(int(idx % 2)):
312             direction = "West - East"
313         else:
314             direction = "East - West"
315         hovertext = ("Test: {test}<br>"
316                      "Direction: {dir}<br>"
317                      "No. of Runs: {nr}<br>".format(test=x_vals[idx],
318                                                     dir=direction,
319                                                     nr=nr_of_samples[idx]))
320         if isinstance(y_maxs[idx], float):
321             hovertext += "Max: {max:.2f}uSec<br>".format(max=y_maxs[idx])
322         if isinstance(y_vals[idx], float):
323             hovertext += "Avg: {avg:.2f}uSec<br>".format(avg=y_vals[idx])
324         if isinstance(y_mins[idx], float):
325             hovertext += "Min: {min:.2f}uSec".format(min=y_mins[idx])
326
327         if isinstance(y_maxs[idx], float) and isinstance(y_vals[idx], float):
328             array = [y_maxs[idx] - y_vals[idx], ]
329         else:
330             array = [None, ]
331         if isinstance(y_mins[idx], float) and isinstance(y_vals[idx], float):
332             arrayminus = [y_vals[idx] - y_mins[idx], ]
333         else:
334             arrayminus = [None, ]
335         logging.debug("y_vals[{1}] :{0}\n".format(y_vals[idx], idx))
336         logging.debug("array :{0}\n".format(array))
337         logging.debug("arrayminus :{0}\n".format(arrayminus))
338         traces.append(plgo.Scatter(
339             x=[idx, ],
340             y=[y_vals[idx], ],
341             name=x_vals[idx],
342             legendgroup=x_vals[idx],
343             showlegend=bool(int(idx % 2)),
344             mode="markers",
345             error_y=dict(
346                 type='data',
347                 symmetric=False,
348                 array=array,
349                 arrayminus=arrayminus,
350                 color=COLORS[int(idx / 2)]
351             ),
352             marker=dict(
353                 size=10,
354                 color=COLORS[int(idx / 2)],
355             ),
356             text=hovertext,
357             hoverinfo="text",
358         ))
359         annotations.append(dict(
360             x=idx,
361             y=0,
362             xref="x",
363             yref="y",
364             xanchor="center",
365             yanchor="top",
366             text="E-W" if bool(int(idx % 2)) else "W-E",
367             font=dict(
368                 size=16,
369             ),
370             align="center",
371             showarrow=False
372         ))
373
374     try:
375         # Create plot
376         logging.info("    Writing file '{0}{1}'.".
377                      format(plot["output-file"], plot["output-file-type"]))
378         layout = deepcopy(plot["layout"])
379         if layout.get("title", None):
380             layout["title"] = "<b>Packet Latency:</b> {0}".\
381                 format(layout["title"])
382         layout["annotations"] = annotations
383         plpl = plgo.Figure(data=traces, layout=layout)
384
385         # Export Plot
386         ploff.plot(plpl,
387                    show_link=False, auto_open=False,
388                    filename='{0}{1}'.format(plot["output-file"],
389                                             plot["output-file-type"]))
390     except PlotlyError as err:
391         logging.error("   Finished with error: {}".
392                       format(str(err).replace("\n", " ")))
393         return
394
395
396 def plot_throughput_speedup_analysis(plot, input_data):
397     """Generate the plot(s) with algorithm:
398     plot_throughput_speedup_analysis
399     specified in the specification file.
400
401     :param plot: Plot to generate.
402     :param input_data: Data to process.
403     :type plot: pandas.Series
404     :type input_data: InputData
405     """
406
407     # Transform the data
408     plot_title = plot.get("title", "")
409     logging.info("    Creating the data set for the {0} '{1}'.".
410                  format(plot.get("type", ""), plot_title))
411     data = input_data.filter_data(plot)
412     if data is None:
413         logging.error("No data.")
414         return
415
416     y_vals = dict()
417     y_tags = dict()
418     for job in data:
419         for build in job:
420             for test in build:
421                 if y_vals.get(test["parent"], None) is None:
422                     y_vals[test["parent"]] = {"1": list(),
423                                               "2": list(),
424                                               "4": list()}
425                     y_tags[test["parent"]] = test.get("tags", None)
426                 try:
427                     if test["type"] in ("NDRPDR",):
428                         if "-pdr" in plot_title.lower():
429                             ttype = "PDR"
430                         elif "-ndr" in plot_title.lower():
431                             ttype = "NDR"
432                         else:
433                             continue
434                         if "1C" in test["tags"]:
435                             y_vals[test["parent"]]["1"]. \
436                                 append(test["throughput"][ttype]["LOWER"])
437                         elif "2C" in test["tags"]:
438                             y_vals[test["parent"]]["2"]. \
439                                 append(test["throughput"][ttype]["LOWER"])
440                         elif "4C" in test["tags"]:
441                             y_vals[test["parent"]]["4"]. \
442                                 append(test["throughput"][ttype]["LOWER"])
443                 except (KeyError, TypeError):
444                     pass
445
446     if not y_vals:
447         logging.warning("No data for the plot '{}'".
448                         format(plot.get("title", "")))
449         return
450
451     y_1c_max = dict()
452     for test_name, test_vals in y_vals.items():
453         for key, test_val in test_vals.items():
454             if test_val:
455                 avg_val = sum(test_val) / len(test_val)
456                 y_vals[test_name][key] = (avg_val, len(test_val))
457                 ideal = avg_val / (int(key) * 1000000.0)
458                 if test_name not in y_1c_max or ideal > y_1c_max[test_name]:
459                     y_1c_max[test_name] = ideal
460
461     vals = dict()
462     y_max = list()
463     nic_limit = 0
464     lnk_limit = 0
465     pci_limit = plot["limits"]["pci"]["pci-g3-x8"]
466     for test_name, test_vals in y_vals.items():
467         try:
468             if test_vals["1"][1]:
469                 name = "-".join(test_name.split('-')[1:-1])
470
471                 vals[name] = dict()
472                 y_val_1 = test_vals["1"][0] / 1000000.0
473                 y_val_2 = test_vals["2"][0] / 1000000.0 if test_vals["2"][0] \
474                     else None
475                 y_val_4 = test_vals["4"][0] / 1000000.0 if test_vals["4"][0] \
476                     else None
477
478                 vals[name]["val"] = [y_val_1, y_val_2, y_val_4]
479                 vals[name]["rel"] = [1.0, None, None]
480                 vals[name]["ideal"] = [y_1c_max[test_name],
481                                        y_1c_max[test_name] * 2,
482                                        y_1c_max[test_name] * 4]
483                 vals[name]["diff"] = [(y_val_1 - y_1c_max[test_name]) * 100 /
484                                       y_val_1, None, None]
485                 vals[name]["count"] = [test_vals["1"][1],
486                                        test_vals["2"][1],
487                                        test_vals["4"][1]]
488
489                 try:
490                     val_max = max(max(vals[name]["val"], vals[name]["ideal"]))
491                 except ValueError as err:
492                     logging.error(err)
493                     continue
494                 if val_max:
495                     y_max.append(int((val_max / 10) + 1) * 10)
496
497                 if y_val_2:
498                     vals[name]["rel"][1] = round(y_val_2 / y_val_1, 2)
499                     vals[name]["diff"][1] = \
500                         (y_val_2 - vals[name]["ideal"][1]) * 100 / y_val_2
501                 if y_val_4:
502                     vals[name]["rel"][2] = round(y_val_4 / y_val_1, 2)
503                     vals[name]["diff"][2] = \
504                         (y_val_4 - vals[name]["ideal"][2]) * 100 / y_val_4
505         except IndexError as err:
506             logging.warning("No data for '{0}'".format(test_name))
507             logging.warning(repr(err))
508
509         # Limits:
510         if "x520" in test_name:
511             limit = plot["limits"]["nic"]["x520"]
512         elif "x710" in test_name:
513             limit = plot["limits"]["nic"]["x710"]
514         elif "xxv710" in test_name:
515             limit = plot["limits"]["nic"]["xxv710"]
516         elif "xl710" in test_name:
517             limit = plot["limits"]["nic"]["xl710"]
518         else:
519             limit = 0
520         if limit > nic_limit:
521             nic_limit = limit
522
523         mul = 2 if "ge2p" in test_name else 1
524         if "10ge" in test_name:
525             limit = plot["limits"]["link"]["10ge"] * mul
526         elif "25ge" in test_name:
527             limit = plot["limits"]["link"]["25ge"] * mul
528         elif "40ge" in test_name:
529             limit = plot["limits"]["link"]["40ge"] * mul
530         elif "100ge" in test_name:
531             limit = plot["limits"]["link"]["100ge"] * mul
532         else:
533             limit = 0
534         if limit > lnk_limit:
535             lnk_limit = limit
536
537     # Sort the tests
538     order = plot.get("sort", None)
539     if order and y_tags:
540         y_sorted = OrderedDict()
541         y_tags_l = {s: [t.lower() for t in ts] for s, ts in y_tags.items()}
542         for tag in order:
543             for test, tags in y_tags_l.items():
544                 if tag.lower() in tags:
545                     name = "-".join(test.split('-')[1:-1])
546                     try:
547                         y_sorted[name] = vals.pop(name)
548                         y_tags_l.pop(test)
549                     except KeyError as err:
550                         logging.error("Not found: {0}".format(err))
551                     finally:
552                         break
553     else:
554         y_sorted = vals
555
556     traces = list()
557     annotations = list()
558     x_vals = [1, 2, 4]
559
560     # Limits:
561     try:
562         threshold = 1.1 * max(y_max)  # 10%
563     except ValueError as err:
564         logging.error(err)
565         return
566     nic_limit /= 1000000.0
567     if nic_limit < threshold:
568         traces.append(plgo.Scatter(
569             x=x_vals,
570             y=[nic_limit, ] * len(x_vals),
571             name="NIC: {0:.2f}Mpps".format(nic_limit),
572             showlegend=False,
573             mode="lines",
574             line=dict(
575                 dash="dot",
576                 color=COLORS[-1],
577                 width=1),
578             hoverinfo="none"
579         ))
580         annotations.append(dict(
581             x=1,
582             y=nic_limit,
583             xref="x",
584             yref="y",
585             xanchor="left",
586             yanchor="bottom",
587             text="NIC: {0:.2f}Mpps".format(nic_limit),
588             font=dict(
589                 size=14,
590                 color=COLORS[-1],
591             ),
592             align="left",
593             showarrow=False
594         ))
595         y_max.append(int((nic_limit / 10) + 1) * 10)
596
597     lnk_limit /= 1000000.0
598     if lnk_limit < threshold:
599         traces.append(plgo.Scatter(
600             x=x_vals,
601             y=[lnk_limit, ] * len(x_vals),
602             name="Link: {0:.2f}Mpps".format(lnk_limit),
603             showlegend=False,
604             mode="lines",
605             line=dict(
606                 dash="dot",
607                 color=COLORS[-2],
608                 width=1),
609             hoverinfo="none"
610         ))
611         annotations.append(dict(
612             x=1,
613             y=lnk_limit,
614             xref="x",
615             yref="y",
616             xanchor="left",
617             yanchor="bottom",
618             text="Link: {0:.2f}Mpps".format(lnk_limit),
619             font=dict(
620                 size=14,
621                 color=COLORS[-2],
622             ),
623             align="left",
624             showarrow=False
625         ))
626         y_max.append(int((lnk_limit / 10) + 1) * 10)
627
628     pci_limit /= 1000000.0
629     if pci_limit < threshold:
630         traces.append(plgo.Scatter(
631             x=x_vals,
632             y=[pci_limit, ] * len(x_vals),
633             name="PCIe: {0:.2f}Mpps".format(pci_limit),
634             showlegend=False,
635             mode="lines",
636             line=dict(
637                 dash="dot",
638                 color=COLORS[-3],
639                 width=1),
640             hoverinfo="none"
641         ))
642         annotations.append(dict(
643             x=1,
644             y=pci_limit,
645             xref="x",
646             yref="y",
647             xanchor="left",
648             yanchor="bottom",
649             text="PCIe: {0:.2f}Mpps".format(pci_limit),
650             font=dict(
651                 size=14,
652                 color=COLORS[-3],
653             ),
654             align="left",
655             showarrow=False
656         ))
657         y_max.append(int((pci_limit / 10) + 1) * 10)
658
659     # Perfect and measured:
660     cidx = 0
661     for name, val in y_sorted.iteritems():
662         hovertext = list()
663         try:
664             for idx in range(len(val["val"])):
665                 htext = ""
666                 if isinstance(val["val"][idx], float):
667                     htext += "Value: {0:.2f}Mpps<br>" \
668                              "No. of Runs: {1}<br>".format(val["val"][idx],
669                                                         val["count"][idx])
670                 if isinstance(val["diff"][idx], float):
671                     htext += "Diff: {0:.0f}%<br>".format(round(val["diff"][idx]))
672                 if isinstance(val["rel"][idx], float):
673                     htext += "Speedup: {0:.2f}".format(val["rel"][idx])
674                 hovertext.append(htext)
675             traces.append(plgo.Scatter(x=x_vals,
676                                        y=val["val"],
677                                        name=name,
678                                        legendgroup=name,
679                                        mode="lines+markers",
680                                        line=dict(
681                                            color=COLORS[cidx],
682                                            width=2),
683                                        marker=dict(
684                                            symbol="circle",
685                                            size=10
686                                        ),
687                                        text=hovertext,
688                                        hoverinfo="text+name"
689                                        ))
690             traces.append(plgo.Scatter(x=x_vals,
691                                        y=val["ideal"],
692                                        name="{0} perfect".format(name),
693                                        legendgroup=name,
694                                        showlegend=False,
695                                        mode="lines",
696                                        line=dict(
697                                            color=COLORS[cidx],
698                                            width=2,
699                                            dash="dash"),
700                                        text=["perfect: {0:.2f}Mpps".format(y)
701                                              for y in val["ideal"]],
702                                        hoverinfo="text"
703                                        ))
704             cidx += 1
705         except (IndexError, ValueError, KeyError) as err:
706             logging.warning("No data for '{0}'".format(name))
707             logging.warning(repr(err))
708
709     try:
710         # Create plot
711         logging.info("    Writing file '{0}{1}'.".
712                      format(plot["output-file"], plot["output-file-type"]))
713         layout = deepcopy(plot["layout"])
714         if layout.get("title", None):
715             layout["title"] = "<b>Speedup Multi-core:</b> {0}". \
716                 format(layout["title"])
717         layout["annotations"].extend(annotations)
718         plpl = plgo.Figure(data=traces, layout=layout)
719
720         # Export Plot
721         ploff.plot(plpl,
722                    show_link=False, auto_open=False,
723                    filename='{0}{1}'.format(plot["output-file"],
724                                             plot["output-file-type"]))
725     except PlotlyError as err:
726         logging.error("   Finished with error: {}".
727                       format(str(err).replace("\n", " ")))
728         return
729
730
731 def plot_http_server_performance_box(plot, input_data):
732     """Generate the plot(s) with algorithm: plot_http_server_performance_box
733     specified in the specification file.
734
735     :param plot: Plot to generate.
736     :param input_data: Data to process.
737     :type plot: pandas.Series
738     :type input_data: InputData
739     """
740
741     # Transform the data
742     logging.info("    Creating the data set for the {0} '{1}'.".
743                  format(plot.get("type", ""), plot.get("title", "")))
744     data = input_data.filter_data(plot)
745     if data is None:
746         logging.error("No data.")
747         return
748
749     # Prepare the data for the plot
750     y_vals = dict()
751     for job in data:
752         for build in job:
753             for test in build:
754                 if y_vals.get(test["name"], None) is None:
755                     y_vals[test["name"]] = list()
756                 try:
757                     y_vals[test["name"]].append(test["result"])
758                 except (KeyError, TypeError):
759                     y_vals[test["name"]].append(None)
760
761     # Add None to the lists with missing data
762     max_len = 0
763     nr_of_samples = list()
764     for val in y_vals.values():
765         if len(val) > max_len:
766             max_len = len(val)
767         nr_of_samples.append(len(val))
768     for key, val in y_vals.items():
769         if len(val) < max_len:
770             val.extend([None for _ in range(max_len - len(val))])
771
772     # Add plot traces
773     traces = list()
774     df = pd.DataFrame(y_vals)
775     df.head()
776     for i, col in enumerate(df.columns):
777         name = "{0}. {1} ({2} run{3})".\
778             format(i + 1,
779                    col.lower().replace('-cps', '').replace('-rps', ''),
780                    nr_of_samples[i],
781                    's' if nr_of_samples[i] > 1 else '')
782         traces.append(plgo.Box(x=[str(i + 1) + '.'] * len(df[col]),
783                                y=df[col],
784                                name=name,
785                                **plot["traces"]))
786     try:
787         # Create plot
788         plpl = plgo.Figure(data=traces, layout=plot["layout"])
789
790         # Export Plot
791         logging.info("    Writing file '{0}{1}'.".
792                      format(plot["output-file"], plot["output-file-type"]))
793         ploff.plot(plpl, show_link=False, auto_open=False,
794                    filename='{0}{1}'.format(plot["output-file"],
795                                             plot["output-file-type"]))
796     except PlotlyError as err:
797         logging.error("   Finished with error: {}".
798                       format(str(err).replace("\n", " ")))
799         return