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