1 # Copyright (c) 2018 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:
6 # http://www.apache.org/licenses/LICENSE-2.0
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.
14 """Algorithms to generate plots.
20 import plotly.offline as ploff
21 import plotly.graph_objs as plgo
23 from plotly.exceptions import PlotlyError
24 from collections import OrderedDict
25 from copy import deepcopy
27 from utils import mean
30 COLORS = ["SkyBlue", "Olive", "Purple", "Coral", "Indigo", "Pink",
31 "Chocolate", "Brown", "Magenta", "Cyan", "Orange", "Black",
32 "Violet", "Blue", "Yellow", "BurlyWood", "CadetBlue", "Crimson",
33 "DarkBlue", "DarkCyan", "DarkGreen", "Green", "GoldenRod",
34 "LightGreen", "LightSeaGreen", "LightSkyBlue", "Maroon",
35 "MediumSeaGreen", "SeaGreen", "LightSlateGrey"]
38 def generate_plots(spec, data):
39 """Generate all plots specified in the specification file.
41 :param spec: Specification read from the specification file.
42 :param data: Data to process.
43 :type spec: Specification
47 logging.info("Generating the plots ...")
48 for index, plot in enumerate(spec.plots):
50 logging.info(" Plot nr {0}: {1}".format(index + 1,
51 plot.get("title", "")))
52 plot["limits"] = spec.configuration["limits"]
53 eval(plot["algorithm"])(plot, data)
54 logging.info(" Done.")
55 except NameError as err:
56 logging.error("Probably algorithm '{alg}' is not defined: {err}".
57 format(alg=plot["algorithm"], err=repr(err)))
61 def plot_performance_box(plot, input_data):
62 """Generate the plot(s) with algorithm: plot_performance_box
63 specified in the specification file.
65 :param plot: Plot to generate.
66 :param input_data: Data to process.
67 :type plot: pandas.Series
68 :type input_data: InputData
72 plot_title = plot.get("title", "")
73 logging.info(" Creating the data set for the {0} '{1}'.".
74 format(plot.get("type", ""), plot_title))
75 data = input_data.filter_data(plot)
77 logging.error("No data.")
80 # Prepare the data for the plot
86 if y_vals.get(test["parent"], None) is None:
87 y_vals[test["parent"]] = list()
88 y_tags[test["parent"]] = test.get("tags", None)
90 if test["type"] in ("NDRPDR", ):
91 if "-pdr" in plot_title.lower():
92 y_vals[test["parent"]].\
93 append(test["throughput"]["PDR"]["LOWER"])
94 elif "-ndr" in plot_title.lower():
95 y_vals[test["parent"]]. \
96 append(test["throughput"]["NDR"]["LOWER"])
101 except (KeyError, TypeError):
102 y_vals[test["parent"]].append(None)
105 order = plot.get("sort", None)
107 y_sorted = OrderedDict()
108 y_tags_l = {s: [t.lower() for t in ts] for s, ts in y_tags.items()}
111 for suite, tags in y_tags_l.items():
113 tag = tag.split(" ")[-1]
114 if tag.lower() in tags:
117 if tag.lower() not in tags:
120 y_sorted[suite] = y_vals.pop(suite)
123 except KeyError as err:
124 logging.error("Not found: {0}".format(repr(err)))
130 # Add None to the lists with missing data
132 nr_of_samples = list()
133 for val in y_sorted.values():
134 if len(val) > max_len:
136 nr_of_samples.append(len(val))
137 for key, val in y_sorted.items():
138 if len(val) < max_len:
139 val.extend([None for _ in range(max_len - len(val))])
143 df = pd.DataFrame(y_sorted)
146 for i, col in enumerate(df.columns):
147 name = "{0}. {1}".format(i + 1, col.lower().replace('-ndrpdr', ''))
149 name_lst = name.split('-')
152 for segment in name_lst:
153 if (len(name) + len(segment) + 1) > 60 and split_name:
156 name += segment + '-'
158 name = "{name} ({samples} run{plural})".\
160 samples=nr_of_samples[i],
161 plural='s' if nr_of_samples[i] > 1 else '')
163 traces.append(plgo.Box(x=[str(i + 1) + '.'] * len(df[col]),
164 y=[y / 1000000 if y else None for y in df[col]],
168 val_max = max(df[col])
169 except ValueError as err:
170 logging.error(repr(err))
173 y_max.append(int(val_max / 1000000) + 1)
177 layout = deepcopy(plot["layout"])
178 if layout.get("title", None):
179 layout["title"] = "<b>Packet Throughput:</b> {0}". \
180 format(layout["title"])
182 layout["yaxis"]["range"] = [0, max(y_max)]
183 plpl = plgo.Figure(data=traces, layout=layout)
186 logging.info(" Writing file '{0}{1}'.".
187 format(plot["output-file"], plot["output-file-type"]))
188 ploff.plot(plpl, show_link=False, auto_open=False,
189 filename='{0}{1}'.format(plot["output-file"],
190 plot["output-file-type"]))
191 except PlotlyError as err:
192 logging.error(" Finished with error: {}".
193 format(repr(err).replace("\n", " ")))
197 def plot_latency_error_bars(plot, input_data):
198 """Generate the plot(s) with algorithm: plot_latency_error_bars
199 specified in the specification file.
201 :param plot: Plot to generate.
202 :param input_data: Data to process.
203 :type plot: pandas.Series
204 :type input_data: InputData
208 plot_title = plot.get("title", "")
209 logging.info(" Creating the data set for the {0} '{1}'.".
210 format(plot.get("type", ""), plot_title))
211 data = input_data.filter_data(plot)
213 logging.error("No data.")
216 # Prepare the data for the plot
223 logging.debug("test['latency']: {0}\n".
224 format(test["latency"]))
225 except ValueError as err:
226 logging.warning(repr(err))
227 if y_tmp_vals.get(test["parent"], None) is None:
228 y_tmp_vals[test["parent"]] = [
229 list(), # direction1, min
230 list(), # direction1, avg
231 list(), # direction1, max
232 list(), # direction2, min
233 list(), # direction2, avg
234 list() # direction2, max
236 y_tags[test["parent"]] = test.get("tags", None)
238 if test["type"] in ("NDRPDR", ):
239 if "-pdr" in plot_title.lower():
241 elif "-ndr" in plot_title.lower():
244 logging.warning("Invalid test type: {0}".
245 format(test["type"]))
247 y_tmp_vals[test["parent"]][0].append(
248 test["latency"][ttype]["direction1"]["min"])
249 y_tmp_vals[test["parent"]][1].append(
250 test["latency"][ttype]["direction1"]["avg"])
251 y_tmp_vals[test["parent"]][2].append(
252 test["latency"][ttype]["direction1"]["max"])
253 y_tmp_vals[test["parent"]][3].append(
254 test["latency"][ttype]["direction2"]["min"])
255 y_tmp_vals[test["parent"]][4].append(
256 test["latency"][ttype]["direction2"]["avg"])
257 y_tmp_vals[test["parent"]][5].append(
258 test["latency"][ttype]["direction2"]["max"])
260 logging.warning("Invalid test type: {0}".
261 format(test["type"]))
263 except (KeyError, TypeError) as err:
264 logging.warning(repr(err))
265 logging.debug("y_tmp_vals: {0}\n".format(y_tmp_vals))
268 order = plot.get("sort", None)
270 y_sorted = OrderedDict()
271 y_tags_l = {s: [t.lower() for t in ts] for s, ts in y_tags.items()}
274 for suite, tags in y_tags_l.items():
276 tag = tag.split(" ")[-1]
277 if tag.lower() in tags:
280 if tag.lower() not in tags:
283 y_sorted[suite] = y_tmp_vals.pop(suite)
286 except KeyError as err:
287 logging.error("Not found: {0}".format(repr(err)))
291 y_sorted = y_tmp_vals
293 logging.debug("y_sorted: {0}\n".format(y_sorted))
298 nr_of_samples = list()
299 for key, val in y_sorted.items():
300 name = "-".join(key.split("-")[1:-1])
302 name_lst = name.split('-')
305 for segment in name_lst:
306 if (len(name) + len(segment) + 1) > 60 and split_name:
309 name += segment + '-'
311 x_vals.append(name) # dir 1
312 y_vals.append(mean(val[1]) if val[1] else None)
313 y_mins.append(mean(val[0]) if val[0] else None)
314 y_maxs.append(mean(val[2]) if val[2] else None)
315 nr_of_samples.append(len(val[1]) if val[1] else 0)
316 x_vals.append(name) # dir 2
317 y_vals.append(mean(val[4]) if val[4] else None)
318 y_mins.append(mean(val[3]) if val[3] else None)
319 y_maxs.append(mean(val[5]) if val[5] else None)
320 nr_of_samples.append(len(val[3]) if val[3] else 0)
322 logging.debug("x_vals :{0}\n".format(x_vals))
323 logging.debug("y_vals :{0}\n".format(y_vals))
324 logging.debug("y_mins :{0}\n".format(y_mins))
325 logging.debug("y_maxs :{0}\n".format(y_maxs))
326 logging.debug("nr_of_samples :{0}\n".format(nr_of_samples))
330 for idx in range(len(x_vals)):
331 if not bool(int(idx % 2)):
332 direction = "West-East"
334 direction = "East-West"
335 hovertext = ("Test: {test}<br>"
336 "Direction: {dir}<br>"
337 "No. of Runs: {nr}<br>".format(test=x_vals[idx],
339 nr=nr_of_samples[idx]))
340 if isinstance(y_maxs[idx], float):
341 hovertext += "Max: {max:.2f}uSec<br>".format(max=y_maxs[idx])
342 if isinstance(y_vals[idx], float):
343 hovertext += "Mean: {avg:.2f}uSec<br>".format(avg=y_vals[idx])
344 if isinstance(y_mins[idx], float):
345 hovertext += "Min: {min:.2f}uSec".format(min=y_mins[idx])
347 if isinstance(y_maxs[idx], float) and isinstance(y_vals[idx], float):
348 array = [y_maxs[idx] - y_vals[idx], ]
351 if isinstance(y_mins[idx], float) and isinstance(y_vals[idx], float):
352 arrayminus = [y_vals[idx] - y_mins[idx], ]
354 arrayminus = [None, ]
355 logging.debug("y_vals[{1}] :{0}\n".format(y_vals[idx], idx))
356 logging.debug("array :{0}\n".format(array))
357 logging.debug("arrayminus :{0}\n".format(arrayminus))
358 traces.append(plgo.Scatter(
362 legendgroup=x_vals[idx],
363 showlegend=bool(int(idx % 2)),
369 arrayminus=arrayminus,
370 color=COLORS[int(idx / 2)]
374 color=COLORS[int(idx / 2)],
379 annotations.append(dict(
386 text="E-W" if bool(int(idx % 2)) else "W-E",
396 logging.info(" Writing file '{0}{1}'.".
397 format(plot["output-file"], plot["output-file-type"]))
398 layout = deepcopy(plot["layout"])
399 if layout.get("title", None):
400 layout["title"] = "<b>Packet Latency:</b> {0}".\
401 format(layout["title"])
402 layout["annotations"] = annotations
403 plpl = plgo.Figure(data=traces, layout=layout)
407 show_link=False, auto_open=False,
408 filename='{0}{1}'.format(plot["output-file"],
409 plot["output-file-type"]))
410 except PlotlyError as err:
411 logging.error(" Finished with error: {}".
412 format(str(err).replace("\n", " ")))
416 def plot_throughput_speedup_analysis(plot, input_data):
417 """Generate the plot(s) with algorithm:
418 plot_throughput_speedup_analysis
419 specified in the specification file.
421 :param plot: Plot to generate.
422 :param input_data: Data to process.
423 :type plot: pandas.Series
424 :type input_data: InputData
428 plot_title = plot.get("title", "")
429 logging.info(" Creating the data set for the {0} '{1}'.".
430 format(plot.get("type", ""), plot_title))
431 data = input_data.filter_data(plot)
433 logging.error("No data.")
441 if y_vals.get(test["parent"], None) is None:
442 y_vals[test["parent"]] = {"1": list(),
445 y_tags[test["parent"]] = test.get("tags", None)
447 if test["type"] in ("NDRPDR",):
448 if "-pdr" in plot_title.lower():
450 elif "-ndr" in plot_title.lower():
454 if "1C" in test["tags"]:
455 y_vals[test["parent"]]["1"]. \
456 append(test["throughput"][ttype]["LOWER"])
457 elif "2C" in test["tags"]:
458 y_vals[test["parent"]]["2"]. \
459 append(test["throughput"][ttype]["LOWER"])
460 elif "4C" in test["tags"]:
461 y_vals[test["parent"]]["4"]. \
462 append(test["throughput"][ttype]["LOWER"])
463 except (KeyError, TypeError):
467 logging.warning("No data for the plot '{}'".
468 format(plot.get("title", "")))
472 for test_name, test_vals in y_vals.items():
473 for key, test_val in test_vals.items():
475 avg_val = sum(test_val) / len(test_val)
476 y_vals[test_name][key] = (avg_val, len(test_val))
477 ideal = avg_val / (int(key) * 1000000.0)
478 if test_name not in y_1c_max or ideal > y_1c_max[test_name]:
479 y_1c_max[test_name] = ideal
485 pci_limit = plot["limits"]["pci"]["pci-g3-x8"]
486 for test_name, test_vals in y_vals.items():
488 if test_vals["1"][1]:
489 name = "-".join(test_name.split('-')[1:-1])
491 name_lst = name.split('-')
494 for segment in name_lst:
495 if (len(name) + len(segment) + 1) > 60 and split_name:
498 name += segment + '-'
502 y_val_1 = test_vals["1"][0] / 1000000.0
503 y_val_2 = test_vals["2"][0] / 1000000.0 if test_vals["2"][0] \
505 y_val_4 = test_vals["4"][0] / 1000000.0 if test_vals["4"][0] \
508 vals[name]["val"] = [y_val_1, y_val_2, y_val_4]
509 vals[name]["rel"] = [1.0, None, None]
510 vals[name]["ideal"] = [y_1c_max[test_name],
511 y_1c_max[test_name] * 2,
512 y_1c_max[test_name] * 4]
513 vals[name]["diff"] = [(y_val_1 - y_1c_max[test_name]) * 100 /
515 vals[name]["count"] = [test_vals["1"][1],
520 val_max = max(max(vals[name]["val"], vals[name]["ideal"]))
521 except ValueError as err:
525 y_max.append(int((val_max / 10) + 1) * 10)
528 vals[name]["rel"][1] = round(y_val_2 / y_val_1, 2)
529 vals[name]["diff"][1] = \
530 (y_val_2 - vals[name]["ideal"][1]) * 100 / y_val_2
532 vals[name]["rel"][2] = round(y_val_4 / y_val_1, 2)
533 vals[name]["diff"][2] = \
534 (y_val_4 - vals[name]["ideal"][2]) * 100 / y_val_4
535 except IndexError as err:
536 logging.warning("No data for '{0}'".format(test_name))
537 logging.warning(repr(err))
540 if "x520" in test_name:
541 limit = plot["limits"]["nic"]["x520"]
542 elif "x710" in test_name:
543 limit = plot["limits"]["nic"]["x710"]
544 elif "xxv710" in test_name:
545 limit = plot["limits"]["nic"]["xxv710"]
546 elif "xl710" in test_name:
547 limit = plot["limits"]["nic"]["xl710"]
550 if limit > nic_limit:
553 mul = 2 if "ge2p" in test_name else 1
554 if "10ge" in test_name:
555 limit = plot["limits"]["link"]["10ge"] * mul
556 elif "25ge" in test_name:
557 limit = plot["limits"]["link"]["25ge"] * mul
558 elif "40ge" in test_name:
559 limit = plot["limits"]["link"]["40ge"] * mul
560 elif "100ge" in test_name:
561 limit = plot["limits"]["link"]["100ge"] * mul
564 if limit > lnk_limit:
568 order = plot.get("sort", None)
570 y_sorted = OrderedDict()
571 y_tags_l = {s: [t.lower() for t in ts] for s, ts in y_tags.items()}
573 for test, tags in y_tags_l.items():
574 if tag.lower() in tags:
575 name = "-".join(test.split('-')[1:-1])
577 y_sorted[name] = vals.pop(name)
579 except KeyError as err:
580 logging.error("Not found: {0}".format(err))
592 threshold = 1.1 * max(y_max) # 10%
593 except ValueError as err:
596 nic_limit /= 1000000.0
597 if nic_limit < threshold:
598 traces.append(plgo.Scatter(
600 y=[nic_limit, ] * len(x_vals),
601 name="NIC: {0:.2f}Mpps".format(nic_limit),
610 annotations.append(dict(
617 text="NIC: {0:.2f}Mpps".format(nic_limit),
625 y_max.append(int((nic_limit / 10) + 1) * 10)
627 lnk_limit /= 1000000.0
628 if lnk_limit < threshold:
629 traces.append(plgo.Scatter(
631 y=[lnk_limit, ] * len(x_vals),
632 name="Link: {0:.2f}Mpps".format(lnk_limit),
641 annotations.append(dict(
648 text="Link: {0:.2f}Mpps".format(lnk_limit),
656 y_max.append(int((lnk_limit / 10) + 1) * 10)
658 pci_limit /= 1000000.0
659 if pci_limit < threshold:
660 traces.append(plgo.Scatter(
662 y=[pci_limit, ] * len(x_vals),
663 name="PCIe: {0:.2f}Mpps".format(pci_limit),
672 annotations.append(dict(
679 text="PCIe: {0:.2f}Mpps".format(pci_limit),
687 y_max.append(int((pci_limit / 10) + 1) * 10)
689 # Perfect and measured:
691 for name, val in y_sorted.iteritems():
694 for idx in range(len(val["val"])):
696 if isinstance(val["val"][idx], float):
697 htext += "Mean: {0:.2f}Mpps<br>" \
698 "No. of Runs: {1}<br>".format(val["val"][idx],
700 if isinstance(val["diff"][idx], float):
701 htext += "Diff: {0:.0f}%<br>".format(round(val["diff"][idx]))
702 if isinstance(val["rel"][idx], float):
703 htext += "Speedup: {0:.2f}".format(val["rel"][idx])
704 hovertext.append(htext)
705 traces.append(plgo.Scatter(x=x_vals,
709 mode="lines+markers",
718 hoverinfo="text+name"
720 traces.append(plgo.Scatter(x=x_vals,
722 name="{0} perfect".format(name),
730 text=["Perfect: {0:.2f}Mpps".format(y)
731 for y in val["ideal"]],
735 except (IndexError, ValueError, KeyError) as err:
736 logging.warning("No data for '{0}'".format(name))
737 logging.warning(repr(err))
741 logging.info(" Writing file '{0}{1}'.".
742 format(plot["output-file"], plot["output-file-type"]))
743 layout = deepcopy(plot["layout"])
744 if layout.get("title", None):
745 layout["title"] = "<b>Speedup Multi-core:</b> {0}". \
746 format(layout["title"])
747 layout["annotations"].extend(annotations)
748 plpl = plgo.Figure(data=traces, layout=layout)
752 show_link=False, auto_open=False,
753 filename='{0}{1}'.format(plot["output-file"],
754 plot["output-file-type"]))
755 except PlotlyError as err:
756 logging.error(" Finished with error: {}".
757 format(str(err).replace("\n", " ")))
761 def plot_http_server_performance_box(plot, input_data):
762 """Generate the plot(s) with algorithm: plot_http_server_performance_box
763 specified in the specification file.
765 :param plot: Plot to generate.
766 :param input_data: Data to process.
767 :type plot: pandas.Series
768 :type input_data: InputData
772 logging.info(" Creating the data set for the {0} '{1}'.".
773 format(plot.get("type", ""), plot.get("title", "")))
774 data = input_data.filter_data(plot)
776 logging.error("No data.")
779 # Prepare the data for the plot
784 if y_vals.get(test["name"], None) is None:
785 y_vals[test["name"]] = list()
787 y_vals[test["name"]].append(test["result"])
788 except (KeyError, TypeError):
789 y_vals[test["name"]].append(None)
791 # Add None to the lists with missing data
793 nr_of_samples = list()
794 for val in y_vals.values():
795 if len(val) > max_len:
797 nr_of_samples.append(len(val))
798 for key, val in y_vals.items():
799 if len(val) < max_len:
800 val.extend([None for _ in range(max_len - len(val))])
804 df = pd.DataFrame(y_vals)
806 for i, col in enumerate(df.columns):
807 name = "{0}. {1}".format(i + 1, col.lower().replace('-ndrpdr', ''))
809 name_lst = name.split('-')
812 for segment in name_lst:
813 if (len(name) + len(segment) + 1) > 60 and split_name:
816 name += segment + '-'
818 name = "{name} ({samples} run{plural})".\
820 samples=nr_of_samples[i],
821 plural='s' if nr_of_samples[i] > 1 else '')
823 traces.append(plgo.Box(x=[str(i + 1) + '.'] * len(df[col]),
829 plpl = plgo.Figure(data=traces, layout=plot["layout"])
832 logging.info(" Writing file '{0}{1}'.".
833 format(plot["output-file"], plot["output-file-type"]))
834 ploff.plot(plpl, show_link=False, auto_open=False,
835 filename='{0}{1}'.format(plot["output-file"],
836 plot["output-file-type"]))
837 except PlotlyError as err:
838 logging.error(" Finished with error: {}".
839 format(str(err).replace("\n", " ")))