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