From cbfa26dc0f5334bcd367c161b4eaad342355bbde Mon Sep 17 00:00:00 2001 From: Tibor Frank Date: Wed, 20 Nov 2019 11:43:44 +0100 Subject: Python3: PAL - files renamed: - utils.py --> pal_utils.py - errors.py --> pal_errors.py - functions/methods renamed: - plot_service_density_reconf_box_name --> plot_nf_reconf_box_name - plot_performance_box_name --> plot_perf_box_name - plot_latency_error_bars_name --> plot_lat_err_bars_name - plot_throughput_speedup_analysis_name --> plot_tsa_name - plot_service_density_heatmap --> plot_nf_heatmap - table_performance_comparison --> table_perf_comparison - table_performance_comparison_nic --> table_perf_comparison_nic - table_performance_trending_dashboard_html --> table_perf_trending_dash_html - functions/methods removed: - plot_service_density_heatmap_compare - plot_throughput_speedup_analysis - plot_latency_error_bars - plot_soak_boxes - plot_soak_bars Change-Id: Icddc01d3ccb451abb92b9e5d912b642d01866033 Signed-off-by: Tibor Frank --- resources/tools/presentation/generator_cpta.py | 633 +++++++++++++++++++++++++ 1 file changed, 633 insertions(+) create mode 100644 resources/tools/presentation/generator_cpta.py (limited to 'resources/tools/presentation/generator_cpta.py') diff --git a/resources/tools/presentation/generator_cpta.py b/resources/tools/presentation/generator_cpta.py new file mode 100644 index 0000000000..3003557696 --- /dev/null +++ b/resources/tools/presentation/generator_cpta.py @@ -0,0 +1,633 @@ +# Copyright (c) 2019 Cisco and/or its affiliates. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Generation of Continuous Performance Trending and Analysis. +""" + +import logging +import csv + +from collections import OrderedDict +from datetime import datetime +from copy import deepcopy + +import prettytable +import plotly.offline as ploff +import plotly.graph_objs as plgo +import plotly.exceptions as plerr + +from pal_utils import archive_input_data, execute_command, classify_anomalies + + +# Command to build the html format of the report +HTML_BUILDER = u'sphinx-build -v -c conf_cpta -a ' \ + u'-b html -E ' \ + u'-t html ' \ + u'-D version="{date}" ' \ + u'{working_dir} ' \ + u'{build_dir}/' + +# .css file for the html format of the report +THEME_OVERRIDES = u"""/* override table width restrictions */ +.wy-nav-content { + max-width: 1200px !important; +} +.rst-content blockquote { + margin-left: 0px; + line-height: 18px; + margin-bottom: 0px; +} +.wy-menu-vertical a { + display: inline-block; + line-height: 18px; + padding: 0 2em; + display: block; + position: relative; + font-size: 90%; + color: #d9d9d9 +} +.wy-menu-vertical li.current a { + color: gray; + border-right: solid 1px #c9c9c9; + padding: 0 3em; +} +.wy-menu-vertical li.toctree-l2.current > a { + background: #c9c9c9; + padding: 0 3em; +} +.wy-menu-vertical li.toctree-l2.current li.toctree-l3 > a { + display: block; + background: #c9c9c9; + padding: 0 4em; +} +.wy-menu-vertical li.toctree-l3.current li.toctree-l4 > a { + display: block; + background: #bdbdbd; + padding: 0 5em; +} +.wy-menu-vertical li.on a, .wy-menu-vertical li.current > a { + color: #404040; + padding: 0 2em; + font-weight: bold; + position: relative; + background: #fcfcfc; + border: none; + border-top-width: medium; + border-bottom-width: medium; + border-top-style: none; + border-bottom-style: none; + border-top-color: currentcolor; + border-bottom-color: currentcolor; + padding-left: 2em -4px; +} +""" + +COLORS = [ + u"SkyBlue", u"Olive", u"Purple", u"Coral", u"Indigo", u"Pink", + u"Chocolate", u"Brown", u"Magenta", u"Cyan", u"Orange", u"Black", + u"Violet", u"Blue", u"Yellow", u"BurlyWood", u"CadetBlue", u"Crimson", + u"DarkBlue", u"DarkCyan", u"DarkGreen", u"Green", u"GoldenRod", + u"LightGreen", u"LightSeaGreen", u"LightSkyBlue", u"Maroon", + u"MediumSeaGreen", u"SeaGreen", u"LightSlateGrey", + u"SkyBlue", u"Olive", u"Purple", u"Coral", u"Indigo", u"Pink", + u"Chocolate", u"Brown", u"Magenta", u"Cyan", u"Orange", u"Black", + u"Violet", u"Blue", u"Yellow", u"BurlyWood", u"CadetBlue", u"Crimson", + u"DarkBlue", u"DarkCyan", u"DarkGreen", u"Green", u"GoldenRod", + u"LightGreen", u"LightSeaGreen", u"LightSkyBlue", u"Maroon", + u"MediumSeaGreen", u"SeaGreen", u"LightSlateGrey" +] + + +def generate_cpta(spec, data): + """Generate all formats and versions of the Continuous Performance Trending + and Analysis. + + :param spec: Specification read from the specification file. + :param data: Full data set. + :type spec: Specification + :type data: InputData + """ + + logging.info(u"Generating the Continuous Performance Trending and Analysis " + u"...") + + ret_code = _generate_all_charts(spec, data) + + cmd = HTML_BUILDER.format( + date=datetime.utcnow().strftime(u'%Y-%m-%d %H:%M UTC'), + working_dir=spec.environment[u'paths'][u'DIR[WORKING,SRC]'], + build_dir=spec.environment[u'paths'][u'DIR[BUILD,HTML]']) + execute_command(cmd) + + with open(spec.environment[u'paths'][u'DIR[CSS_PATCH_FILE]'], u'w') as \ + css_file: + css_file.write(THEME_OVERRIDES) + + with open(spec.environment[u'paths'][u'DIR[CSS_PATCH_FILE2]'], u'w') as \ + css_file: + css_file.write(THEME_OVERRIDES) + + if spec.configuration.get(u"archive-inputs", True): + archive_input_data(spec) + + logging.info(u"Done.") + + return ret_code + + +def _generate_trending_traces(in_data, job_name, build_info, + show_trend_line=True, name=u"", color=u""): + """Generate the trending traces: + - samples, + - outliers, regress, progress + - average of normal samples (trending line) + + :param in_data: Full data set. + :param job_name: The name of job which generated the data. + :param build_info: Information about the builds. + :param show_trend_line: Show moving median (trending plot). + :param name: Name of the plot + :param color: Name of the color for the plot. + :type in_data: OrderedDict + :type job_name: str + :type build_info: dict + :type show_trend_line: bool + :type name: str + :type color: str + :returns: Generated traces (list) and the evaluated result. + :rtype: tuple(traces, result) + """ + + data_x = list(in_data.keys()) + data_y = list(in_data.values()) + + hover_text = list() + xaxis = list() + for idx in data_x: + date = build_info[job_name][str(idx)][0] + hover_str = (u"date: {date}
" + u"value: {value:,}
" + u"{sut}-ref: {build}
" + u"csit-ref: mrr-{period}-build-{build_nr}
" + u"testbed: {testbed}") + if u"dpdk" in job_name: + hover_text.append(hover_str.format( + date=date, + value=int(in_data[idx]), + sut=u"dpdk", + build=build_info[job_name][str(idx)][1].rsplit(u'~', 1)[0], + period=u"weekly", + build_nr=idx, + testbed=build_info[job_name][str(idx)][2])) + elif u"vpp" in job_name: + hover_text.append(hover_str.format( + date=date, + value=int(in_data[idx]), + sut=u"vpp", + build=build_info[job_name][str(idx)][1].rsplit(u'~', 1)[0], + period=u"daily", + build_nr=idx, + testbed=build_info[job_name][str(idx)][2])) + + xaxis.append(datetime(int(date[0:4]), int(date[4:6]), int(date[6:8]), + int(date[9:11]), int(date[12:]))) + + data_pd = OrderedDict() + for key, value in zip(xaxis, data_y): + data_pd[key] = value + + anomaly_classification, avgs = classify_anomalies(data_pd) + + anomalies = OrderedDict() + anomalies_colors = list() + anomalies_avgs = list() + anomaly_color = { + u"regression": 0.0, + u"normal": 0.5, + u"progression": 1.0 + } + if anomaly_classification: + for idx, (key, value) in enumerate(data_pd.items()): + if anomaly_classification[idx] in \ + (u"outlier", u"regression", u"progression"): + anomalies[key] = value + anomalies_colors.append( + anomaly_color[anomaly_classification[idx]]) + anomalies_avgs.append(avgs[idx]) + anomalies_colors.extend([0.0, 0.5, 1.0]) + + # Create traces + + trace_samples = plgo.Scatter( + x=xaxis, + y=data_y, + mode=u"markers", + line={ + u"width": 1 + }, + showlegend=True, + legendgroup=name, + name=f"{name}", + marker={ + u"size": 5, + u"color": color, + u"symbol": u"circle", + }, + text=hover_text, + hoverinfo=u"text" + ) + traces = [trace_samples, ] + + if show_trend_line: + trace_trend = plgo.Scatter( + x=xaxis, + y=avgs, + mode=u"lines", + line={ + u"shape": u"linear", + u"width": 1, + u"color": color, + }, + showlegend=False, + legendgroup=name, + name=f"{name}", + text=[f"trend: {int(avg):,}" for avg in avgs], + hoverinfo=u"text+name" + ) + traces.append(trace_trend) + + trace_anomalies = plgo.Scatter( + x=list(anomalies.keys()), + y=anomalies_avgs, + mode=u"markers", + hoverinfo=u"none", + showlegend=False, + legendgroup=name, + name=f"{name}-anomalies", + marker={ + u"size": 15, + u"symbol": u"circle-open", + u"color": anomalies_colors, + u"colorscale": [ + [0.00, u"red"], + [0.33, u"red"], + [0.33, u"white"], + [0.66, u"white"], + [0.66, u"green"], + [1.00, u"green"] + ], + u"showscale": True, + u"line": { + u"width": 2 + }, + u"colorbar": { + u"y": 0.5, + u"len": 0.8, + u"title": u"Circles Marking Data Classification", + u"titleside": u"right", + u"titlefont": { + u"size": 14 + }, + u"tickmode": u"array", + u"tickvals": [0.167, 0.500, 0.833], + u"ticktext": [u"Regression", u"Normal", u"Progression"], + u"ticks": u"", + u"ticklen": 0, + u"tickangle": -90, + u"thickness": 10 + } + } + ) + traces.append(trace_anomalies) + + if anomaly_classification: + return traces, anomaly_classification[-1] + + return traces, None + + +def _generate_all_charts(spec, input_data): + """Generate all charts specified in the specification file. + + :param spec: Specification. + :param input_data: Full data set. + :type spec: Specification + :type input_data: InputData + """ + + def _generate_chart(graph): + """Generates the chart. + + :param graph: The graph to be generated + :type graph: dict + :returns: Dictionary with the job name, csv table with results and + list of tests classification results. + :rtype: dict + """ + + logs = list() + + logs.append( + (u"INFO", f" Generating the chart {graph.get(u'title', u'')} ...") + ) + + job_name = list(graph[u"data"].keys())[0] + + csv_tbl = list() + res = dict() + + # Transform the data + logs.append( + (u"INFO", + f" Creating the data set for the {graph.get(u'type', u'')} " + f"{graph.get(u'title', u'')}." + ) + ) + data = input_data.filter_data(graph, continue_on_error=True) + if data is None: + logging.error(u"No data.") + return dict() + + chart_data = dict() + chart_tags = dict() + for job, job_data in data.items(): + if job != job_name: + continue + for index, bld in job_data.items(): + for test_name, test in bld.items(): + if chart_data.get(test_name, None) is None: + chart_data[test_name] = OrderedDict() + try: + chart_data[test_name][int(index)] = \ + test[u"result"][u"receive-rate"] + chart_tags[test_name] = test.get(u"tags", None) + except (KeyError, TypeError): + pass + + # Add items to the csv table: + for tst_name, tst_data in chart_data.items(): + tst_lst = list() + for bld in builds_dict[job_name]: + itm = tst_data.get(int(bld), u'') + # CSIT-1180: Itm will be list, compute stats. + tst_lst.append(str(itm)) + csv_tbl.append(f"{tst_name}," + u",".join(tst_lst) + u'\n') + + # Generate traces: + traces = list() + index = 0 + groups = graph.get(u"groups", None) + visibility = list() + + if groups: + for group in groups: + visible = list() + for tag in group: + for tst_name, test_data in chart_data.items(): + if not test_data: + logs.append( + (u"WARNING", f"No data for the test {tst_name}") + ) + continue + if tag not in chart_tags[tst_name]: + continue + message = f"index: {index}, test: {tst_name}" + try: + trace, rslt = _generate_trending_traces( + test_data, + job_name=job_name, + build_info=build_info, + name=u'-'.join(tst_name.split(u'.')[-1]. + split(u'-')[2:-1]), + color=COLORS[index]) + except IndexError: + logs.append( + (u"ERROR", f"Out of colors: {message}") + ) + logging.error(f"Out of colors: {message}") + index += 1 + continue + traces.extend(trace) + visible.extend([True for _ in range(len(trace))]) + res[tst_name] = rslt + index += 1 + break + visibility.append(visible) + else: + for tst_name, test_data in chart_data.items(): + if not test_data: + logs.append( + (u"WARNING", f"No data for the test {tst_name}") + ) + continue + message = f"index: {index}, test: {tst_name}" + try: + trace, rslt = _generate_trending_traces( + test_data, + job_name=job_name, + build_info=build_info, + name=u'-'.join( + tst_name.split(u'.')[-1].split(u'-')[2:-1]), + color=COLORS[index]) + except IndexError: + logs.append((u"ERROR", f"Out of colors: {message}")) + logging.error(f"Out of colors: {message}") + index += 1 + continue + traces.extend(trace) + res[tst_name] = rslt + index += 1 + + if traces: + # Generate the chart: + try: + layout = deepcopy(graph[u"layout"]) + except KeyError as err: + logging.error(u"Finished with error: No layout defined") + logging.error(repr(err)) + return dict() + if groups: + show = list() + for i in range(len(visibility)): + visible = list() + for vis_idx, _ in enumerate(visibility): + for _ in range(len(visibility[vis_idx])): + visible.append(i == vis_idx) + show.append(visible) + + buttons = list() + buttons.append(dict( + label=u"All", + method=u"update", + args=[{u"visible": [True for _ in range(len(show[0]))]}, ] + )) + for i in range(len(groups)): + try: + label = graph[u"group-names"][i] + except (IndexError, KeyError): + label = f"Group {i + 1}" + buttons.append(dict( + label=label, + method=u"update", + args=[{u"visible": show[i]}, ] + )) + + layout[u"updatemenus"] = list([ + dict( + active=0, + type=u"dropdown", + direction=u"down", + xanchor=u"left", + yanchor=u"bottom", + x=-0.12, + y=1.0, + buttons=buttons + ) + ]) + + name_file = ( + f"{spec.cpta[u'output-file']}-{graph[u'output-file-name']}" + f"{spec.cpta[u'output-file-type']}") + + logs.append((u"INFO", f" Writing the file {name_file} ...")) + plpl = plgo.Figure(data=traces, layout=layout) + try: + ploff.plot(plpl, show_link=False, auto_open=False, + filename=name_file) + except plerr.PlotlyEmptyDataError: + logs.append((u"WARNING", u"No data for the plot. Skipped.")) + + for level, line in logs: + if level == u"INFO": + logging.info(line) + elif level == u"ERROR": + logging.error(line) + elif level == u"DEBUG": + logging.debug(line) + elif level == u"CRITICAL": + logging.critical(line) + elif level == u"WARNING": + logging.warning(line) + + return {u"job_name": job_name, u"csv_table": csv_tbl, u"results": res} + + builds_dict = dict() + for job in spec.input[u"builds"].keys(): + if builds_dict.get(job, None) is None: + builds_dict[job] = list() + for build in spec.input[u"builds"][job]: + status = build[u"status"] + if status not in (u"failed", u"not found", u"removed"): + builds_dict[job].append(str(build[u"build"])) + + # Create "build ID": "date" dict: + build_info = dict() + tb_tbl = spec.environment.get(u"testbeds", None) + for job_name, job_data in builds_dict.items(): + if build_info.get(job_name, None) is None: + build_info[job_name] = OrderedDict() + for build in job_data: + testbed = u"" + tb_ip = input_data.metadata(job_name, build).get(u"testbed", u"") + if tb_ip and tb_tbl: + testbed = tb_tbl.get(tb_ip, u"") + build_info[job_name][build] = ( + input_data.metadata(job_name, build).get(u"generated", u""), + input_data.metadata(job_name, build).get(u"version", u""), + testbed + ) + + anomaly_classifications = dict() + + # Create the header: + csv_tables = dict() + for job_name in builds_dict: + if csv_tables.get(job_name, None) is None: + csv_tables[job_name] = list() + header = u"Build Number:," + u",".join(builds_dict[job_name]) + u'\n' + csv_tables[job_name].append(header) + build_dates = [x[0] for x in build_info[job_name].values()] + header = u"Build Date:," + u",".join(build_dates) + u'\n' + csv_tables[job_name].append(header) + versions = [x[1] for x in build_info[job_name].values()] + header = u"Version:," + u",".join(versions) + u'\n' + csv_tables[job_name].append(header) + + for chart in spec.cpta[u"plots"]: + result = _generate_chart(chart) + if not result: + continue + + csv_tables[result[u"job_name"]].extend(result[u"csv_table"]) + + if anomaly_classifications.get(result[u"job_name"], None) is None: + anomaly_classifications[result[u"job_name"]] = dict() + anomaly_classifications[result[u"job_name"]].update(result[u"results"]) + + # Write the tables: + for job_name, csv_table in csv_tables.items(): + file_name = spec.cpta[u"output-file"] + u"-" + job_name + u"-trending" + with open(f"{file_name}.csv", u"w") as file_handler: + file_handler.writelines(csv_table) + + txt_table = None + with open(f"{file_name}.csv", u"rt") as csv_file: + csv_content = csv.reader(csv_file, delimiter=u',', quotechar=u'"') + line_nr = 0 + for row in csv_content: + if txt_table is None: + txt_table = prettytable.PrettyTable(row) + else: + if line_nr > 1: + for idx, item in enumerate(row): + try: + row[idx] = str(round(float(item) / 1000000, 2)) + except ValueError: + pass + try: + txt_table.add_row(row) + # PrettyTable raises Exception + except Exception as err: + logging.warning( + f"Error occurred while generating TXT table:\n{err}" + ) + line_nr += 1 + txt_table.align[u"Build Number:"] = u"l" + with open(f"{file_name}.txt", u"w") as txt_file: + txt_file.write(str(txt_table)) + + # Evaluate result: + if anomaly_classifications: + result = u"PASS" + for job_name, job_data in anomaly_classifications.items(): + file_name = \ + f"{spec.cpta[u'output-file']}-regressions-{job_name}.txt" + with open(file_name, u'w') as txt_file: + for test_name, classification in job_data.items(): + if classification == u"regression": + txt_file.write(test_name + u'\n') + if classification in (u"regression", u"outlier"): + result = u"FAIL" + file_name = \ + f"{spec.cpta[u'output-file']}-progressions-{job_name}.txt" + with open(file_name, u'w') as txt_file: + for test_name, classification in job_data.items(): + if classification == u"progression": + txt_file.write(test_name + u'\n') + else: + result = u"FAIL" + + logging.info(f"Partial results: {anomaly_classifications}") + logging.info(f"Result: {result}") + + return result -- cgit 1.2.3-korg