21dd1a0555568dec126d0ddeac617ec31eac3e53
[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 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 = "{nr}. ({samples:02d} run{plural}) {name}".\
148             format(nr=(i + 1),
149                    samples=nr_of_samples[i],
150                    plural='s' if nr_of_samples[i] > 1 else '',
151                    name=col.lower().replace('-ndrpdr', ''))
152         if len(name) > 50:
153             name_lst = name.split('-')
154             name = ""
155             split_name = True
156             for segment in name_lst:
157                 if (len(name) + len(segment) + 1) > 50 and split_name:
158                     name += "<br>    "
159                     split_name = False
160                 name += segment + '-'
161             name = name[:-1]
162
163         logging.debug(name)
164         traces.append(plgo.Box(x=[str(i + 1) + '.'] * len(df[col]),
165                                y=[y / 1000000 if y else None for y in df[col]],
166                                name=name,
167                                **plot["traces"]))
168         try:
169             val_max = max(df[col])
170         except ValueError as err:
171             logging.error(repr(err))
172             continue
173         if val_max:
174             y_max.append(int(val_max / 1000000) + 1)
175
176     try:
177         # Create plot
178         layout = deepcopy(plot["layout"])
179         if layout.get("title", None):
180             layout["title"] = "<b>Packet Throughput:</b> {0}". \
181                 format(layout["title"])
182         if y_max:
183             layout["yaxis"]["range"] = [0, max(y_max)]
184         plpl = plgo.Figure(data=traces, layout=layout)
185
186         # Export Plot
187         logging.info("    Writing file '{0}{1}'.".
188                      format(plot["output-file"], plot["output-file-type"]))
189         ploff.plot(plpl, show_link=False, auto_open=False,
190                    filename='{0}{1}'.format(plot["output-file"],
191                                             plot["output-file-type"]))
192     except PlotlyError as err:
193         logging.error("   Finished with error: {}".
194                       format(repr(err).replace("\n", " ")))
195         return
196
197
198 def plot_soak_bars(plot, input_data):
199     """Generate the plot(s) with algorithm: plot_soak_bars
200     specified in the specification file.
201
202     :param plot: Plot to generate.
203     :param input_data: Data to process.
204     :type plot: pandas.Series
205     :type input_data: InputData
206     """
207
208     # Transform the data
209     plot_title = plot.get("title", "")
210     logging.info("    Creating the data set for the {0} '{1}'.".
211                  format(plot.get("type", ""), plot_title))
212     data = input_data.filter_data(plot)
213     if data is None:
214         logging.error("No data.")
215         return
216
217     # Prepare the data for the plot
218     y_vals = dict()
219     y_tags = dict()
220     for job in data:
221         for build in job:
222             for test in build:
223                 if y_vals.get(test["parent"], None) is None:
224                     y_tags[test["parent"]] = test.get("tags", None)
225                 try:
226                     if test["type"] in ("SOAK", ):
227                         y_vals[test["parent"]] = test["throughput"]
228                     else:
229                         continue
230                 except (KeyError, TypeError):
231                     y_vals[test["parent"]] = dict()
232
233     # Sort the tests
234     order = plot.get("sort", None)
235     if order and y_tags:
236         y_sorted = OrderedDict()
237         y_tags_l = {s: [t.lower() for t in ts] for s, ts in y_tags.items()}
238         for tag in order:
239             logging.debug(tag)
240             for suite, tags in y_tags_l.items():
241                 if "not " in tag:
242                     tag = tag.split(" ")[-1]
243                     if tag.lower() in tags:
244                         continue
245                 else:
246                     if tag.lower() not in tags:
247                         continue
248                 try:
249                     y_sorted[suite] = y_vals.pop(suite)
250                     y_tags_l.pop(suite)
251                     logging.debug(suite)
252                 except KeyError as err:
253                     logging.error("Not found: {0}".format(repr(err)))
254                 finally:
255                     break
256     else:
257         y_sorted = y_vals
258
259     idx = 0
260     y_max = 0
261     traces = list()
262     for test_name, test_data in y_sorted.items():
263         idx += 1
264         name = "{nr}. {name}".\
265             format(nr=idx, name=test_name.lower().replace('-soak', ''))
266         if len(name) > 50:
267             name_lst = name.split('-')
268             name = ""
269             split_name = True
270             for segment in name_lst:
271                 if (len(name) + len(segment) + 1) > 50 and split_name:
272                     name += "<br>    "
273                     split_name = False
274                 name += segment + '-'
275             name = name[:-1]
276
277         y_val = test_data.get("LOWER", None)
278         if y_val:
279             y_val /= 1000000
280             if y_val > y_max:
281                 y_max = y_val
282
283         time = "No Information"
284         result = "No Information"
285         hovertext = ("{name}<br>"
286                      "Packet Throughput: {val:.2f}Mpps<br>"
287                      "Final Duration: {time}<br>"
288                      "Result: {result}".format(name=name,
289                                                val=y_val,
290                                                time=time,
291                                                result=result))
292         traces.append(plgo.Bar(x=[str(idx) + '.', ],
293                                y=[y_val, ],
294                                name=name,
295                                text=hovertext,
296                                hoverinfo="text"))
297     try:
298         # Create plot
299         layout = deepcopy(plot["layout"])
300         if layout.get("title", None):
301             layout["title"] = "<b>Packet Throughput:</b> {0}". \
302                 format(layout["title"])
303         if y_max:
304             layout["yaxis"]["range"] = [0, y_max + 1]
305         plpl = plgo.Figure(data=traces, layout=layout)
306         # Export Plot
307         logging.info("    Writing file '{0}{1}'.".
308                      format(plot["output-file"], plot["output-file-type"]))
309         ploff.plot(plpl, show_link=False, auto_open=False,
310                    filename='{0}{1}'.format(plot["output-file"],
311                                             plot["output-file-type"]))
312     except PlotlyError as err:
313         logging.error("   Finished with error: {}".
314                       format(repr(err).replace("\n", " ")))
315         return
316
317
318 def plot_soak_boxes(plot, input_data):
319     """Generate the plot(s) with algorithm: plot_soak_boxes
320     specified in the specification file.
321
322     :param plot: Plot to generate.
323     :param input_data: Data to process.
324     :type plot: pandas.Series
325     :type input_data: InputData
326     """
327
328     # Transform the data
329     plot_title = plot.get("title", "")
330     logging.info("    Creating the data set for the {0} '{1}'.".
331                  format(plot.get("type", ""), plot_title))
332     data = input_data.filter_data(plot)
333     if data is None:
334         logging.error("No data.")
335         return
336
337     # Prepare the data for the plot
338     y_vals = dict()
339     y_tags = dict()
340     for job in data:
341         for build in job:
342             for test in build:
343                 if y_vals.get(test["parent"], None) is None:
344                     y_tags[test["parent"]] = test.get("tags", None)
345                 try:
346                     if test["type"] in ("SOAK", ):
347                         y_vals[test["parent"]] = test["throughput"]
348                     else:
349                         continue
350                 except (KeyError, TypeError):
351                     y_vals[test["parent"]] = dict()
352
353     # Sort the tests
354     order = plot.get("sort", None)
355     if order and y_tags:
356         y_sorted = OrderedDict()
357         y_tags_l = {s: [t.lower() for t in ts] for s, ts in y_tags.items()}
358         for tag in order:
359             logging.debug(tag)
360             for suite, tags in y_tags_l.items():
361                 if "not " in tag:
362                     tag = tag.split(" ")[-1]
363                     if tag.lower() in tags:
364                         continue
365                 else:
366                     if tag.lower() not in tags:
367                         continue
368                 try:
369                     y_sorted[suite] = y_vals.pop(suite)
370                     y_tags_l.pop(suite)
371                     logging.debug(suite)
372                 except KeyError as err:
373                     logging.error("Not found: {0}".format(repr(err)))
374                 finally:
375                     break
376     else:
377         y_sorted = y_vals
378
379     idx = 0
380     y_max = 0
381     traces = list()
382     for test_name, test_data in y_sorted.items():
383         idx += 1
384         name = "{nr}. {name}".\
385             format(nr=idx, name=test_name.lower().replace('-soak', ''))
386         if len(name) > 50:
387             name_lst = name.split('-')
388             name = ""
389             split_name = True
390             for segment in name_lst:
391                 if (len(name) + len(segment) + 1) > 50 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 = ("{name}<br>"
408                      "Upper bound: {upper:.2f}Mpps<br>"
409                      "Lower bound: {lower:.2f}Mpps".format(name=name,
410                                                            upper=y_val,
411                                                            lower=y_base))
412         traces.append(plgo.Bar(x=[str(idx) + '.', ],
413                                # +0.05 to see the value in case lower == upper
414                                y=[y_val - y_base + 0.05, ],
415                                base=y_base,
416                                name=name,
417                                text=hovertext,
418                                hoverinfo="text"))
419     try:
420         # Create plot
421         layout = deepcopy(plot["layout"])
422         if layout.get("title", None):
423             layout["title"] = "<b>Soak Tests:</b> {0}". \
424                 format(layout["title"])
425         if y_max:
426             layout["yaxis"]["range"] = [0, y_max + 1]
427         plpl = plgo.Figure(data=traces, layout=layout)
428         # Export Plot
429         logging.info("    Writing file '{0}{1}'.".
430                      format(plot["output-file"], plot["output-file-type"]))
431         ploff.plot(plpl, show_link=False, auto_open=False,
432                    filename='{0}{1}'.format(plot["output-file"],
433                                             plot["output-file-type"]))
434     except PlotlyError as err:
435         logging.error("   Finished with error: {}".
436                       format(repr(err).replace("\n", " ")))
437         return
438
439
440 def plot_latency_error_bars(plot, input_data):
441     """Generate the plot(s) with algorithm: plot_latency_error_bars
442     specified in the specification file.
443
444     :param plot: Plot to generate.
445     :param input_data: Data to process.
446     :type plot: pandas.Series
447     :type input_data: InputData
448     """
449
450     # Transform the data
451     plot_title = plot.get("title", "")
452     logging.info("    Creating the data set for the {0} '{1}'.".
453                  format(plot.get("type", ""), plot_title))
454     data = input_data.filter_data(plot)
455     if data is None:
456         logging.error("No data.")
457         return
458
459     # Prepare the data for the plot
460     y_tmp_vals = dict()
461     y_tags = dict()
462     for job in data:
463         for build in job:
464             for test in build:
465                 try:
466                     logging.debug("test['latency']: {0}\n".
467                                  format(test["latency"]))
468                 except ValueError as err:
469                     logging.warning(repr(err))
470                 if y_tmp_vals.get(test["parent"], None) is None:
471                     y_tmp_vals[test["parent"]] = [
472                         list(),  # direction1, min
473                         list(),  # direction1, avg
474                         list(),  # direction1, max
475                         list(),  # direction2, min
476                         list(),  # direction2, avg
477                         list()   # direction2, max
478                     ]
479                     y_tags[test["parent"]] = test.get("tags", None)
480                 try:
481                     if test["type"] in ("NDRPDR", ):
482                         if "-pdr" in plot_title.lower():
483                             ttype = "PDR"
484                         elif "-ndr" in plot_title.lower():
485                             ttype = "NDR"
486                         else:
487                             logging.warning("Invalid test type: {0}".
488                                             format(test["type"]))
489                             continue
490                         y_tmp_vals[test["parent"]][0].append(
491                             test["latency"][ttype]["direction1"]["min"])
492                         y_tmp_vals[test["parent"]][1].append(
493                             test["latency"][ttype]["direction1"]["avg"])
494                         y_tmp_vals[test["parent"]][2].append(
495                             test["latency"][ttype]["direction1"]["max"])
496                         y_tmp_vals[test["parent"]][3].append(
497                             test["latency"][ttype]["direction2"]["min"])
498                         y_tmp_vals[test["parent"]][4].append(
499                             test["latency"][ttype]["direction2"]["avg"])
500                         y_tmp_vals[test["parent"]][5].append(
501                             test["latency"][ttype]["direction2"]["max"])
502                     else:
503                         logging.warning("Invalid test type: {0}".
504                                         format(test["type"]))
505                         continue
506                 except (KeyError, TypeError) as err:
507                     logging.warning(repr(err))
508     logging.debug("y_tmp_vals: {0}\n".format(y_tmp_vals))
509
510     # Sort the tests
511     order = plot.get("sort", None)
512     if order and y_tags:
513         y_sorted = OrderedDict()
514         y_tags_l = {s: [t.lower() for t in ts] for s, ts in y_tags.items()}
515         for tag in order:
516             logging.debug(tag)
517             for suite, tags in y_tags_l.items():
518                 if "not " in tag:
519                     tag = tag.split(" ")[-1]
520                     if tag.lower() in tags:
521                         continue
522                 else:
523                     if tag.lower() not in tags:
524                         continue
525                 try:
526                     y_sorted[suite] = y_tmp_vals.pop(suite)
527                     y_tags_l.pop(suite)
528                     logging.debug(suite)
529                 except KeyError as err:
530                     logging.error("Not found: {0}".format(repr(err)))
531                 finally:
532                     break
533     else:
534         y_sorted = y_tmp_vals
535
536     logging.debug("y_sorted: {0}\n".format(y_sorted))
537     x_vals = list()
538     y_vals = list()
539     y_mins = list()
540     y_maxs = list()
541     nr_of_samples = list()
542     for key, val in y_sorted.items():
543         name = "-".join(key.split("-")[1:-1])
544         if len(name) > 50:
545             name_lst = name.split('-')
546             name = ""
547             split_name = True
548             for segment in name_lst:
549                 if (len(name) + len(segment) + 1) > 50 and split_name:
550                     name += "<br>"
551                     split_name = False
552                 name += segment + '-'
553             name = name[:-1]
554         x_vals.append(name)  # dir 1
555         y_vals.append(mean(val[1]) if val[1] else None)
556         y_mins.append(mean(val[0]) if val[0] else None)
557         y_maxs.append(mean(val[2]) if val[2] else None)
558         nr_of_samples.append(len(val[1]) if val[1] else 0)
559         x_vals.append(name)  # dir 2
560         y_vals.append(mean(val[4]) if val[4] else None)
561         y_mins.append(mean(val[3]) if val[3] else None)
562         y_maxs.append(mean(val[5]) if val[5] else None)
563         nr_of_samples.append(len(val[3]) if val[3] else 0)
564
565     logging.debug("x_vals :{0}\n".format(x_vals))
566     logging.debug("y_vals :{0}\n".format(y_vals))
567     logging.debug("y_mins :{0}\n".format(y_mins))
568     logging.debug("y_maxs :{0}\n".format(y_maxs))
569     logging.debug("nr_of_samples :{0}\n".format(nr_of_samples))
570     traces = list()
571     annotations = list()
572
573     for idx in range(len(x_vals)):
574         if not bool(int(idx % 2)):
575             direction = "West-East"
576         else:
577             direction = "East-West"
578         hovertext = ("No. of Runs: {nr}<br>"
579                      "Test: {test}<br>"
580                      "Direction: {dir}<br>".format(test=x_vals[idx],
581                                                    dir=direction,
582                                                    nr=nr_of_samples[idx]))
583         if isinstance(y_maxs[idx], float):
584             hovertext += "Max: {max:.2f}uSec<br>".format(max=y_maxs[idx])
585         if isinstance(y_vals[idx], float):
586             hovertext += "Mean: {avg:.2f}uSec<br>".format(avg=y_vals[idx])
587         if isinstance(y_mins[idx], float):
588             hovertext += "Min: {min:.2f}uSec".format(min=y_mins[idx])
589
590         if isinstance(y_maxs[idx], float) and isinstance(y_vals[idx], float):
591             array = [y_maxs[idx] - y_vals[idx], ]
592         else:
593             array = [None, ]
594         if isinstance(y_mins[idx], float) and isinstance(y_vals[idx], float):
595             arrayminus = [y_vals[idx] - y_mins[idx], ]
596         else:
597             arrayminus = [None, ]
598         logging.debug("y_vals[{1}] :{0}\n".format(y_vals[idx], idx))
599         logging.debug("array :{0}\n".format(array))
600         logging.debug("arrayminus :{0}\n".format(arrayminus))
601         traces.append(plgo.Scatter(
602             x=[idx, ],
603             y=[y_vals[idx], ],
604             name=x_vals[idx],
605             legendgroup=x_vals[idx],
606             showlegend=bool(int(idx % 2)),
607             mode="markers",
608             error_y=dict(
609                 type='data',
610                 symmetric=False,
611                 array=array,
612                 arrayminus=arrayminus,
613                 color=COLORS[int(idx / 2)]
614             ),
615             marker=dict(
616                 size=10,
617                 color=COLORS[int(idx / 2)],
618             ),
619             text=hovertext,
620             hoverinfo="text",
621         ))
622         annotations.append(dict(
623             x=idx,
624             y=0,
625             xref="x",
626             yref="y",
627             xanchor="center",
628             yanchor="top",
629             text="E-W" if bool(int(idx % 2)) else "W-E",
630             font=dict(
631                 size=16,
632             ),
633             align="center",
634             showarrow=False
635         ))
636
637     try:
638         # Create plot
639         logging.info("    Writing file '{0}{1}'.".
640                      format(plot["output-file"], plot["output-file-type"]))
641         layout = deepcopy(plot["layout"])
642         if layout.get("title", None):
643             layout["title"] = "<b>Packet Latency:</b> {0}".\
644                 format(layout["title"])
645         layout["annotations"] = annotations
646         plpl = plgo.Figure(data=traces, layout=layout)
647
648         # Export Plot
649         ploff.plot(plpl,
650                    show_link=False, auto_open=False,
651                    filename='{0}{1}'.format(plot["output-file"],
652                                             plot["output-file-type"]))
653     except PlotlyError as err:
654         logging.error("   Finished with error: {}".
655                       format(str(err).replace("\n", " ")))
656         return
657
658
659 def plot_throughput_speedup_analysis(plot, input_data):
660     """Generate the plot(s) with algorithm:
661     plot_throughput_speedup_analysis
662     specified in the specification file.
663
664     :param plot: Plot to generate.
665     :param input_data: Data to process.
666     :type plot: pandas.Series
667     :type input_data: InputData
668     """
669
670     # Transform the data
671     plot_title = plot.get("title", "")
672     logging.info("    Creating the data set for the {0} '{1}'.".
673                  format(plot.get("type", ""), plot_title))
674     data = input_data.filter_data(plot)
675     if data is None:
676         logging.error("No data.")
677         return
678
679     y_vals = dict()
680     y_tags = dict()
681     for job in data:
682         for build in job:
683             for test in build:
684                 if y_vals.get(test["parent"], None) is None:
685                     y_vals[test["parent"]] = {"1": list(),
686                                               "2": list(),
687                                               "4": list()}
688                     y_tags[test["parent"]] = test.get("tags", None)
689                 try:
690                     if test["type"] in ("NDRPDR",):
691                         if "-pdr" in plot_title.lower():
692                             ttype = "PDR"
693                         elif "-ndr" in plot_title.lower():
694                             ttype = "NDR"
695                         else:
696                             continue
697                         if "1C" in test["tags"]:
698                             y_vals[test["parent"]]["1"]. \
699                                 append(test["throughput"][ttype]["LOWER"])
700                         elif "2C" in test["tags"]:
701                             y_vals[test["parent"]]["2"]. \
702                                 append(test["throughput"][ttype]["LOWER"])
703                         elif "4C" in test["tags"]:
704                             y_vals[test["parent"]]["4"]. \
705                                 append(test["throughput"][ttype]["LOWER"])
706                 except (KeyError, TypeError):
707                     pass
708
709     if not y_vals:
710         logging.warning("No data for the plot '{}'".
711                         format(plot.get("title", "")))
712         return
713
714     y_1c_max = dict()
715     for test_name, test_vals in y_vals.items():
716         for key, test_val in test_vals.items():
717             if test_val:
718                 avg_val = sum(test_val) / len(test_val)
719                 y_vals[test_name][key] = (avg_val, len(test_val))
720                 ideal = avg_val / (int(key) * 1000000.0)
721                 if test_name not in y_1c_max or ideal > y_1c_max[test_name]:
722                     y_1c_max[test_name] = ideal
723
724     vals = dict()
725     y_max = list()
726     nic_limit = 0
727     lnk_limit = 0
728     pci_limit = plot["limits"]["pci"]["pci-g3-x8"]
729     for test_name, test_vals in y_vals.items():
730         try:
731             if test_vals["1"][1]:
732                 name = "-".join(test_name.split('-')[1:-1])
733                 if len(name) > 50:
734                     name_lst = name.split('-')
735                     name = ""
736                     split_name = True
737                     for segment in name_lst:
738                         if (len(name) + len(segment) + 1) > 50 and split_name:
739                             name += "<br>"
740                             split_name = False
741                         name += segment + '-'
742                     name = name[:-1]
743
744                 vals[name] = dict()
745                 y_val_1 = test_vals["1"][0] / 1000000.0
746                 y_val_2 = test_vals["2"][0] / 1000000.0 if test_vals["2"][0] \
747                     else None
748                 y_val_4 = test_vals["4"][0] / 1000000.0 if test_vals["4"][0] \
749                     else None
750
751                 vals[name]["val"] = [y_val_1, y_val_2, y_val_4]
752                 vals[name]["rel"] = [1.0, None, None]
753                 vals[name]["ideal"] = [y_1c_max[test_name],
754                                        y_1c_max[test_name] * 2,
755                                        y_1c_max[test_name] * 4]
756                 vals[name]["diff"] = [(y_val_1 - y_1c_max[test_name]) * 100 /
757                                       y_val_1, None, None]
758                 vals[name]["count"] = [test_vals["1"][1],
759                                        test_vals["2"][1],
760                                        test_vals["4"][1]]
761
762                 try:
763                     val_max = max(max(vals[name]["val"], vals[name]["ideal"]))
764                 except ValueError as err:
765                     logging.error(err)
766                     continue
767                 if val_max:
768                     y_max.append(int((val_max / 10) + 1) * 10)
769
770                 if y_val_2:
771                     vals[name]["rel"][1] = round(y_val_2 / y_val_1, 2)
772                     vals[name]["diff"][1] = \
773                         (y_val_2 - vals[name]["ideal"][1]) * 100 / y_val_2
774                 if y_val_4:
775                     vals[name]["rel"][2] = round(y_val_4 / y_val_1, 2)
776                     vals[name]["diff"][2] = \
777                         (y_val_4 - vals[name]["ideal"][2]) * 100 / y_val_4
778         except IndexError as err:
779             logging.warning("No data for '{0}'".format(test_name))
780             logging.warning(repr(err))
781
782         # Limits:
783         if "x520" in test_name:
784             limit = plot["limits"]["nic"]["x520"]
785         elif "x710" in test_name:
786             limit = plot["limits"]["nic"]["x710"]
787         elif "xxv710" in test_name:
788             limit = plot["limits"]["nic"]["xxv710"]
789         elif "xl710" in test_name:
790             limit = plot["limits"]["nic"]["xl710"]
791         elif "x553" in test_name:
792             limit = plot["limits"]["nic"]["x553"]
793         else:
794             limit = 0
795         if limit > nic_limit:
796             nic_limit = limit
797
798         mul = 2 if "ge2p" in test_name else 1
799         if "10ge" in test_name:
800             limit = plot["limits"]["link"]["10ge"] * mul
801         elif "25ge" in test_name:
802             limit = plot["limits"]["link"]["25ge"] * mul
803         elif "40ge" in test_name:
804             limit = plot["limits"]["link"]["40ge"] * mul
805         elif "100ge" in test_name:
806             limit = plot["limits"]["link"]["100ge"] * mul
807         else:
808             limit = 0
809         if limit > lnk_limit:
810             lnk_limit = limit
811
812     # Sort the tests
813     order = plot.get("sort", None)
814     if order and y_tags:
815         y_sorted = OrderedDict()
816         y_tags_l = {s: [t.lower() for t in ts] for s, ts in y_tags.items()}
817         for tag in order:
818             for test, tags in y_tags_l.items():
819                 if tag.lower() in tags:
820                     name = "-".join(test.split('-')[1:-1])
821                     try:
822                         y_sorted[name] = vals.pop(name)
823                         y_tags_l.pop(test)
824                     except KeyError as err:
825                         logging.error("Not found: {0}".format(err))
826                     finally:
827                         break
828     else:
829         y_sorted = vals
830
831     traces = list()
832     annotations = list()
833     x_vals = [1, 2, 4]
834
835     # Limits:
836     try:
837         threshold = 1.1 * max(y_max)  # 10%
838     except ValueError as err:
839         logging.error(err)
840         return
841     nic_limit /= 1000000.0
842     if nic_limit < threshold:
843         traces.append(plgo.Scatter(
844             x=x_vals,
845             y=[nic_limit, ] * len(x_vals),
846             name="NIC: {0:.2f}Mpps".format(nic_limit),
847             showlegend=False,
848             mode="lines",
849             line=dict(
850                 dash="dot",
851                 color=COLORS[-1],
852                 width=1),
853             hoverinfo="none"
854         ))
855         annotations.append(dict(
856             x=1,
857             y=nic_limit,
858             xref="x",
859             yref="y",
860             xanchor="left",
861             yanchor="bottom",
862             text="NIC: {0:.2f}Mpps".format(nic_limit),
863             font=dict(
864                 size=14,
865                 color=COLORS[-1],
866             ),
867             align="left",
868             showarrow=False
869         ))
870         y_max.append(int((nic_limit / 10) + 1) * 10)
871
872     lnk_limit /= 1000000.0
873     if lnk_limit < threshold:
874         traces.append(plgo.Scatter(
875             x=x_vals,
876             y=[lnk_limit, ] * len(x_vals),
877             name="Link: {0:.2f}Mpps".format(lnk_limit),
878             showlegend=False,
879             mode="lines",
880             line=dict(
881                 dash="dot",
882                 color=COLORS[-2],
883                 width=1),
884             hoverinfo="none"
885         ))
886         annotations.append(dict(
887             x=1,
888             y=lnk_limit,
889             xref="x",
890             yref="y",
891             xanchor="left",
892             yanchor="bottom",
893             text="Link: {0:.2f}Mpps".format(lnk_limit),
894             font=dict(
895                 size=14,
896                 color=COLORS[-2],
897             ),
898             align="left",
899             showarrow=False
900         ))
901         y_max.append(int((lnk_limit / 10) + 1) * 10)
902
903     pci_limit /= 1000000.0
904     if pci_limit < threshold:
905         traces.append(plgo.Scatter(
906             x=x_vals,
907             y=[pci_limit, ] * len(x_vals),
908             name="PCIe: {0:.2f}Mpps".format(pci_limit),
909             showlegend=False,
910             mode="lines",
911             line=dict(
912                 dash="dot",
913                 color=COLORS[-3],
914                 width=1),
915             hoverinfo="none"
916         ))
917         annotations.append(dict(
918             x=1,
919             y=pci_limit,
920             xref="x",
921             yref="y",
922             xanchor="left",
923             yanchor="bottom",
924             text="PCIe: {0:.2f}Mpps".format(pci_limit),
925             font=dict(
926                 size=14,
927                 color=COLORS[-3],
928             ),
929             align="left",
930             showarrow=False
931         ))
932         y_max.append(int((pci_limit / 10) + 1) * 10)
933
934     # Perfect and measured:
935     cidx = 0
936     for name, val in y_sorted.iteritems():
937         hovertext = list()
938         try:
939             for idx in range(len(val["val"])):
940                 htext = ""
941                 if isinstance(val["val"][idx], float):
942                     htext += "No. of Runs: {1}<br>" \
943                              "Mean: {0:.2f}Mpps<br>".format(val["val"][idx],
944                                                             val["count"][idx])
945                 if isinstance(val["diff"][idx], float):
946                     htext += "Diff: {0:.0f}%<br>".format(round(val["diff"][idx]))
947                 if isinstance(val["rel"][idx], float):
948                     htext += "Speedup: {0:.2f}".format(val["rel"][idx])
949                 hovertext.append(htext)
950             traces.append(plgo.Scatter(x=x_vals,
951                                        y=val["val"],
952                                        name=name,
953                                        legendgroup=name,
954                                        mode="lines+markers",
955                                        line=dict(
956                                            color=COLORS[cidx],
957                                            width=2),
958                                        marker=dict(
959                                            symbol="circle",
960                                            size=10
961                                        ),
962                                        text=hovertext,
963                                        hoverinfo="text+name"
964                                        ))
965             traces.append(plgo.Scatter(x=x_vals,
966                                        y=val["ideal"],
967                                        name="{0} perfect".format(name),
968                                        legendgroup=name,
969                                        showlegend=False,
970                                        mode="lines",
971                                        line=dict(
972                                            color=COLORS[cidx],
973                                            width=2,
974                                            dash="dash"),
975                                        text=["Perfect: {0:.2f}Mpps".format(y)
976                                              for y in val["ideal"]],
977                                        hoverinfo="text"
978                                        ))
979             cidx += 1
980         except (IndexError, ValueError, KeyError) as err:
981             logging.warning("No data for '{0}'".format(name))
982             logging.warning(repr(err))
983
984     try:
985         # Create plot
986         logging.info("    Writing file '{0}{1}'.".
987                      format(plot["output-file"], plot["output-file-type"]))
988         layout = deepcopy(plot["layout"])
989         if layout.get("title", None):
990             layout["title"] = "<b>Speedup Multi-core:</b> {0}". \
991                 format(layout["title"])
992         layout["annotations"].extend(annotations)
993         plpl = plgo.Figure(data=traces, layout=layout)
994
995         # Export Plot
996         ploff.plot(plpl,
997                    show_link=False, auto_open=False,
998                    filename='{0}{1}'.format(plot["output-file"],
999                                             plot["output-file-type"]))
1000     except PlotlyError as err:
1001         logging.error("   Finished with error: {}".
1002                       format(str(err).replace("\n", " ")))
1003         return
1004
1005
1006 def plot_http_server_performance_box(plot, input_data):
1007     """Generate the plot(s) with algorithm: plot_http_server_performance_box
1008     specified in the specification file.
1009
1010     :param plot: Plot to generate.
1011     :param input_data: Data to process.
1012     :type plot: pandas.Series
1013     :type input_data: InputData
1014     """
1015
1016     # Transform the data
1017     logging.info("    Creating the data set for the {0} '{1}'.".
1018                  format(plot.get("type", ""), plot.get("title", "")))
1019     data = input_data.filter_data(plot)
1020     if data is None:
1021         logging.error("No data.")
1022         return
1023
1024     # Prepare the data for the plot
1025     y_vals = dict()
1026     for job in data:
1027         for build in job:
1028             for test in build:
1029                 if y_vals.get(test["name"], None) is None:
1030                     y_vals[test["name"]] = list()
1031                 try:
1032                     y_vals[test["name"]].append(test["result"])
1033                 except (KeyError, TypeError):
1034                     y_vals[test["name"]].append(None)
1035
1036     # Add None to the lists with missing data
1037     max_len = 0
1038     nr_of_samples = list()
1039     for val in y_vals.values():
1040         if len(val) > max_len:
1041             max_len = len(val)
1042         nr_of_samples.append(len(val))
1043     for key, val in y_vals.items():
1044         if len(val) < max_len:
1045             val.extend([None for _ in range(max_len - len(val))])
1046
1047     # Add plot traces
1048     traces = list()
1049     df = pd.DataFrame(y_vals)
1050     df.head()
1051     for i, col in enumerate(df.columns):
1052         name = "{nr}. ({samples:02d} run{plural}) {name}".\
1053             format(nr=(i + 1),
1054                    samples=nr_of_samples[i],
1055                    plural='s' if nr_of_samples[i] > 1 else '',
1056                    name=col.lower().replace('-ndrpdr', ''))
1057         if len(name) > 50:
1058             name_lst = name.split('-')
1059             name = ""
1060             split_name = True
1061             for segment in name_lst:
1062                 if (len(name) + len(segment) + 1) > 50 and split_name:
1063                     name += "<br>    "
1064                     split_name = False
1065                 name += segment + '-'
1066             name = name[:-1]
1067
1068         traces.append(plgo.Box(x=[str(i + 1) + '.'] * len(df[col]),
1069                                y=df[col],
1070                                name=name,
1071                                **plot["traces"]))
1072     try:
1073         # Create plot
1074         plpl = plgo.Figure(data=traces, layout=plot["layout"])
1075
1076         # Export Plot
1077         logging.info("    Writing file '{0}{1}'.".
1078                      format(plot["output-file"], plot["output-file-type"]))
1079         ploff.plot(plpl, show_link=False, auto_open=False,
1080                    filename='{0}{1}'.format(plot["output-file"],
1081                                             plot["output-file-type"]))
1082     except PlotlyError as err:
1083         logging.error("   Finished with error: {}".
1084                       format(str(err).replace("\n", " ")))
1085         return