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 """Generation of Continuous Performance Trending and Analysis.
17 import multiprocessing
22 import plotly.offline as ploff
23 import plotly.graph_objs as plgo
24 import plotly.exceptions as plerr
28 from collections import OrderedDict
29 from datetime import datetime
31 from utils import split_outliers, archive_input_data, execute_command, Worker
34 # Command to build the html format of the report
35 HTML_BUILDER = 'sphinx-build -v -c conf_cpta -a ' \
38 '-D version="{date}" ' \
42 # .css file for the html format of the report
43 THEME_OVERRIDES = """/* override table width restrictions */
45 max-width: 1200px !important;
49 COLORS = ["SkyBlue", "Olive", "Purple", "Coral", "Indigo", "Pink",
50 "Chocolate", "Brown", "Magenta", "Cyan", "Orange", "Black",
51 "Violet", "Blue", "Yellow"]
54 def generate_cpta(spec, data):
55 """Generate all formats and versions of the Continuous Performance Trending
58 :param spec: Specification read from the specification file.
59 :param data: Full data set.
60 :type spec: Specification
64 logging.info("Generating the Continuous Performance Trending and Analysis "
67 ret_code = _generate_all_charts(spec, data)
69 cmd = HTML_BUILDER.format(
70 date=datetime.utcnow().strftime('%m/%d/%Y %H:%M UTC'),
71 working_dir=spec.environment["paths"]["DIR[WORKING,SRC]"],
72 build_dir=spec.environment["paths"]["DIR[BUILD,HTML]"])
75 with open(spec.environment["paths"]["DIR[CSS_PATCH_FILE]"], "w") as \
77 css_file.write(THEME_OVERRIDES)
79 with open(spec.environment["paths"]["DIR[CSS_PATCH_FILE2]"], "w") as \
81 css_file.write(THEME_OVERRIDES)
83 archive_input_data(spec)
90 def _evaluate_results(trimmed_data, window=10):
91 """Evaluates if the sample value is regress, normal or progress compared to
92 previous data within the window.
93 We use the intervals defined as:
94 - regress: less than trimmed moving median - 3 * stdev
95 - normal: between trimmed moving median - 3 * stdev and median + 3 * stdev
96 - progress: more than trimmed moving median + 3 * stdev
97 where stdev is trimmed moving standard deviation.
99 :param trimmed_data: Full data set with the outliers replaced by nan.
100 :param window: Window size used to calculate moving average and moving stdev.
101 :type trimmed_data: pandas.Series
103 :returns: Evaluated results.
107 if len(trimmed_data) > 2:
108 win_size = trimmed_data.size if trimmed_data.size < window else window
110 tmm = trimmed_data.rolling(window=win_size, min_periods=2).median()
111 tmstd = trimmed_data.rolling(window=win_size, min_periods=2).std()
114 for build_nr, value in trimmed_data.iteritems():
119 or np.isnan(tmm[build_nr])
120 or np.isnan(tmstd[build_nr])):
122 elif value < (tmm[build_nr] - 3 * tmstd[build_nr]):
124 elif value > (tmm[build_nr] + 3 * tmstd[build_nr]):
131 tmm = np.median(trimmed_data)
132 tmstd = np.std(trimmed_data)
133 if trimmed_data.values[-1] < (tmm - 3 * tmstd):
135 elif (tmm - 3 * tmstd) <= trimmed_data.values[-1] <= (
145 def _generate_trending_traces(in_data, build_info, moving_win_size=10,
146 show_trend_line=True, name="", color=""):
147 """Generate the trending traces:
149 - trimmed moving median (trending line)
150 - outliers, regress, progress
152 :param in_data: Full data set.
153 :param build_info: Information about the builds.
154 :param moving_win_size: Window size.
155 :param show_trend_line: Show moving median (trending plot).
156 :param name: Name of the plot
157 :param color: Name of the color for the plot.
158 :type in_data: OrderedDict
159 :type build_info: dict
160 :type moving_win_size: int
161 :type show_trend_line: bool
164 :returns: Generated traces (list) and the evaluated result.
165 :rtype: tuple(traces, result)
168 data_x = list(in_data.keys())
169 data_y = list(in_data.values())
174 hover_text.append("vpp-ref: {0}<br>csit-ref: mrr-daily-build-{1}".
175 format(build_info[str(idx)][1].rsplit('~', 1)[0],
177 date = build_info[str(idx)][0]
178 xaxis.append(datetime(int(date[0:4]), int(date[4:6]), int(date[6:8]),
179 int(date[9:11]), int(date[12:])))
181 data_pd = pd.Series(data_y, index=xaxis)
183 t_data, outliers = split_outliers(data_pd, outlier_const=1.5,
184 window=moving_win_size)
185 results = _evaluate_results(t_data, window=moving_win_size)
187 anomalies = pd.Series()
188 anomalies_res = list()
189 for idx, item in enumerate(data_pd.items()):
190 item_pd = pd.Series([item[1], ], index=[item[0], ])
191 if item[0] in outliers.keys():
192 anomalies = anomalies.append(item_pd)
193 anomalies_res.append(0.0)
194 elif results[idx] in (0.33, 1.0):
195 anomalies = anomalies.append(item_pd)
196 anomalies_res.append(results[idx])
197 anomalies_res.extend([0.0, 0.33, 0.66, 1.0])
200 color_scale = [[0.00, "grey"],
209 trace_samples = plgo.Scatter(
217 name="{name}-thput".format(name=name),
224 hoverinfo="x+y+text+name"
226 traces = [trace_samples, ]
228 trace_anomalies = plgo.Scatter(
235 name="{name}-anomalies".format(name=name),
238 "symbol": "circle-open",
239 "color": anomalies_res,
240 "colorscale": color_scale,
248 "title": "Circles Marking Data Classification",
249 "titleside": 'right',
254 "tickvals": [0.125, 0.375, 0.625, 0.875],
255 "ticktext": ["Outlier", "Regression", "Normal", "Progression"],
263 traces.append(trace_anomalies)
266 data_trend = t_data.rolling(window=moving_win_size,
267 min_periods=2).median()
268 trace_trend = plgo.Scatter(
270 y=data_trend.tolist(),
278 name='{name}-trend'.format(name=name)
280 traces.append(trace_trend)
282 return traces, results[-1]
285 def _generate_all_charts(spec, input_data):
286 """Generate all charts specified in the specification file.
288 :param spec: Specification.
289 :param input_data: Full data set.
290 :type spec: Specification
291 :type input_data: InputData
294 def _generate_chart(_, data_q, graph):
295 """Generates the chart.
300 logging.info(" Generating the chart '{0}' ...".
301 format(graph.get("title", "")))
302 logs.append(("INFO", " Generating the chart '{0}' ...".
303 format(graph.get("title", ""))))
305 job_name = spec.cpta["data"].keys()[0]
311 logs.append(("INFO", " Creating the data set for the {0} '{1}'.".
312 format(graph.get("type", ""), graph.get("title", ""))))
313 data = input_data.filter_data(graph, continue_on_error=True)
315 logging.error("No data.")
320 for index, bld in job.items():
321 for test_name, test in bld.items():
322 if chart_data.get(test_name, None) is None:
323 chart_data[test_name] = OrderedDict()
325 chart_data[test_name][int(index)] = \
326 test["result"]["throughput"]
327 except (KeyError, TypeError):
330 # Add items to the csv table:
331 for tst_name, tst_data in chart_data.items():
333 for bld in builds_lst:
334 itm = tst_data.get(int(bld), '')
335 tst_lst.append(str(itm))
336 csv_tbl.append("{0},".format(tst_name) + ",".join(tst_lst) + '\n')
341 for test_name, test_data in chart_data.items():
343 logs.append(("WARNING", "No data for the test '{0}'".
346 test_name = test_name.split('.')[-1]
347 trace, rslt = _generate_trending_traces(
349 build_info=build_info,
350 moving_win_size=win_size,
351 name='-'.join(test_name.split('-')[3:-1]),
358 # Generate the chart:
359 graph["layout"]["xaxis"]["title"] = \
360 graph["layout"]["xaxis"]["title"].format(job=job_name)
361 name_file = "{0}-{1}{2}".format(spec.cpta["output-file"],
362 graph["output-file-name"],
363 spec.cpta["output-file-type"])
365 logs.append(("INFO", " Writing the file '{0}' ...".
367 plpl = plgo.Figure(data=traces, layout=graph["layout"])
369 ploff.plot(plpl, show_link=False, auto_open=False,
371 except plerr.PlotlyEmptyDataError:
372 logs.append(("WARNING", "No data for the plot. Skipped."))
374 logging.info(" Done.")
377 "csv_table": csv_tbl,
383 job_name = spec.cpta["data"].keys()[0]
386 for build in spec.input["builds"][job_name]:
387 status = build["status"]
388 if status != "failed" and status != "not found":
389 builds_lst.append(str(build["build"]))
391 # Get "build ID": "date" dict:
392 build_info = OrderedDict()
393 for build in builds_lst:
395 build_info[build] = (
396 input_data.metadata(job_name, build)["generated"][:14],
397 input_data.metadata(job_name, build)["version"]
400 build_info[build] = ("", "")
402 work_queue = multiprocessing.JoinableQueue()
403 manager = multiprocessing.Manager()
404 data_queue = manager.Queue()
405 cpus = multiprocessing.cpu_count()
408 for cpu in range(cpus):
409 worker = Worker(work_queue,
414 workers.append(worker)
415 os.system("taskset -p -c {0} {1} > /dev/null 2>&1".
416 format(cpu, worker.pid))
418 for chart in spec.cpta["plots"]:
419 work_queue.put((chart, ))
426 header = "Build Number:," + ",".join(builds_lst) + '\n'
427 csv_table.append(header)
428 build_dates = [x[0] for x in build_info.values()]
429 header = "Build Date:," + ",".join(build_dates) + '\n'
430 csv_table.append(header)
431 vpp_versions = [x[1] for x in build_info.values()]
432 header = "VPP Version:," + ",".join(vpp_versions) + '\n'
433 csv_table.append(header)
435 while not data_queue.empty():
436 result = data_queue.get()
438 results.extend(result["results"])
439 csv_table.extend(result["csv_table"])
441 for item in result["logs"]:
442 if item[0] == "INFO":
443 logging.info(item[1])
444 elif item[0] == "ERROR":
445 logging.error(item[1])
446 elif item[0] == "DEBUG":
447 logging.debug(item[1])
448 elif item[0] == "CRITICAL":
449 logging.critical(item[1])
450 elif item[0] == "WARNING":
451 logging.warning(item[1])
455 # Terminate all workers
456 for worker in workers:
461 file_name = spec.cpta["output-file"] + "-trending"
462 with open("{0}.csv".format(file_name), 'w') as file_handler:
463 file_handler.writelines(csv_table)
466 with open("{0}.csv".format(file_name), 'rb') as csv_file:
467 csv_content = csv.reader(csv_file, delimiter=',', quotechar='"')
469 for row in csv_content:
470 if txt_table is None:
471 txt_table = prettytable.PrettyTable(row)
474 for idx, item in enumerate(row):
476 row[idx] = str(round(float(item) / 1000000, 2))
480 txt_table.add_row(row)
481 except Exception as err:
482 logging.warning("Error occurred while generating TXT table:"
485 txt_table.align["Build Number:"] = "l"
486 with open("{0}.txt".format(file_name), "w") as txt_file:
487 txt_file.write(str(txt_table))
495 if item == 0.66 and result == "PASS":
497 elif item == 0.33 or item == 0.0:
500 logging.info("Partial results: {0}".format(results))
501 logging.info("Result: {0}".format(result))