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