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