1f400e0b8SCiara Power#! /usr/bin/env python3 2f400e0b8SCiara Power# SPDX-License-Identifier: BSD-3-Clause 3f400e0b8SCiara Power# Copyright(c) 2021 Intel Corporation 4f400e0b8SCiara Power 5f400e0b8SCiara Power""" 6f400e0b8SCiara PowerScript to automate running crypto performance tests for a range of test 7f400e0b8SCiara Powercases as configured in the JSON file specified by the user. 8f400e0b8SCiara PowerThe results are processed and output into various graphs in PDF files. 9f400e0b8SCiara PowerCurrently, throughput and latency tests are supported. 10f400e0b8SCiara Power""" 11f400e0b8SCiara Power 12f400e0b8SCiara Powerimport glob 13f400e0b8SCiara Powerimport json 14f400e0b8SCiara Powerimport os 15f400e0b8SCiara Powerimport shutil 16f400e0b8SCiara Powerimport subprocess 17f400e0b8SCiara Powerfrom argparse import ArgumentParser 18f400e0b8SCiara Powerfrom argparse import ArgumentDefaultsHelpFormatter 19f400e0b8SCiara Powerimport img2pdf 20f400e0b8SCiara Powerimport pandas as pd 21f400e0b8SCiara Powerimport plotly.express as px 22f400e0b8SCiara Power 23f400e0b8SCiara PowerSCRIPT_PATH = os.path.dirname(__file__) + "/" 24f400e0b8SCiara PowerGRAPH_DIR = "temp_graphs" 25f400e0b8SCiara Power 26f400e0b8SCiara Power 27f400e0b8SCiara Powerclass Grapher: 28f400e0b8SCiara Power """Grapher object containing all graphing functions. """ 29f400e0b8SCiara Power def __init__(self, config, suite, graph_path): 30f400e0b8SCiara Power self.graph_num = 0 31f400e0b8SCiara Power self.graph_path = graph_path 32f400e0b8SCiara Power self.suite = suite 33f400e0b8SCiara Power self.config = config 34f400e0b8SCiara Power self.test = "" 35f400e0b8SCiara Power self.ptest = "" 36f400e0b8SCiara Power self.data = pd.DataFrame() 37f400e0b8SCiara Power 38f400e0b8SCiara Power def save_graph(self, fig, subdir): 39f400e0b8SCiara Power """ 40f400e0b8SCiara Power Update figure layout to increase readability, output to JPG file. 41f400e0b8SCiara Power """ 42f400e0b8SCiara Power path = os.path.join(self.graph_path, subdir, "") 43f400e0b8SCiara Power if not os.path.exists(path): 44f400e0b8SCiara Power os.makedirs(path) 45f400e0b8SCiara Power fig.update_layout(font_size=30, title_x=0.5, title_font={"size": 25}, 46f400e0b8SCiara Power margin={'t': 300, 'l': 150, 'r': 150, 'b': 150}) 47f400e0b8SCiara Power fig.write_image(path + "%d.jpg" % self.graph_num) 48f400e0b8SCiara Power 49f400e0b8SCiara Power def boxplot_graph(self, x_axis_label, burst, buffer): 50f400e0b8SCiara Power """Plot a boxplot graph for the given parameters.""" 51f400e0b8SCiara Power fig = px.box(self.data, x=x_axis_label, 52f400e0b8SCiara Power title="Config: " + self.config + "<br>Test Suite: " + 53f400e0b8SCiara Power self.suite + "<br>" + self.test + 54f400e0b8SCiara Power "<br>(Outliers Included)<br>Burst Size: " + burst + 55f400e0b8SCiara Power ", Buffer Size: " + buffer, 56f400e0b8SCiara Power height=1400, width=2400) 57f400e0b8SCiara Power self.save_graph(fig, x_axis_label.replace(' ', '_')) 58f400e0b8SCiara Power self.graph_num += 1 59f400e0b8SCiara Power 60f400e0b8SCiara Power def grouped_graph(self, y_axis_label, x_axis_label, color_label): 61f400e0b8SCiara Power """Plot a grouped barchart using the given parameters.""" 62f400e0b8SCiara Power if (self.data[y_axis_label] == 0).all(): 63f400e0b8SCiara Power return 64f400e0b8SCiara Power fig = px.bar(self.data, x=x_axis_label, color=color_label, 65f400e0b8SCiara Power y=y_axis_label, 66f400e0b8SCiara Power title="Config: " + self.config + "<br>Test Suite: " + 67f400e0b8SCiara Power self.suite + "<br>" + self.test + "<br>" 68f400e0b8SCiara Power + y_axis_label + " for each " + x_axis_label + 69f400e0b8SCiara Power "/" + color_label, barmode="group", height=1400, 70f400e0b8SCiara Power width=2400) 71f400e0b8SCiara Power fig.update_xaxes(type='category') 72f400e0b8SCiara Power self.save_graph(fig, y_axis_label.replace(' ', '_')) 73f400e0b8SCiara Power self.graph_num += 1 74f400e0b8SCiara Power 75f400e0b8SCiara Power def histogram_graph(self, x_axis_label, burst, buffer): 76f400e0b8SCiara Power """Plot a histogram graph using the given parameters.""" 77f400e0b8SCiara Power quart1 = self.data[x_axis_label].quantile(0.25) 78f400e0b8SCiara Power quart3 = self.data[x_axis_label].quantile(0.75) 79f400e0b8SCiara Power inter_quart_range = quart3 - quart1 80f400e0b8SCiara Power data_out = self.data[~((self.data[x_axis_label] < 81f400e0b8SCiara Power (quart1 - 1.5 * inter_quart_range)) | 82f400e0b8SCiara Power (self.data[x_axis_label] > 83f400e0b8SCiara Power (quart3 + 1.5 * inter_quart_range)))] 84f400e0b8SCiara Power fig = px.histogram(data_out, x=x_axis_label, 85f400e0b8SCiara Power title="Config: " + self.config + "<br>Test Suite: " 86f400e0b8SCiara Power + self.suite + "<br>" + self.test 87f400e0b8SCiara Power + "<br>(Outliers removed using Interquartile Range)" 88f400e0b8SCiara Power + "<br>Burst Size: " + burst + ", Buffer Size: " + 89f400e0b8SCiara Power buffer, height=1400, width=2400) 90f400e0b8SCiara Power max_val = data_out[x_axis_label].max() 91f400e0b8SCiara Power min_val = data_out[x_axis_label].min() 92f400e0b8SCiara Power fig.update_traces(xbins=dict( 93f400e0b8SCiara Power start=min_val, 94f400e0b8SCiara Power end=max_val, 95f400e0b8SCiara Power size=(max_val - min_val) / 200 96f400e0b8SCiara Power )) 97f400e0b8SCiara Power self.save_graph(fig, x_axis_label.replace(' ', '_')) 98f400e0b8SCiara Power self.graph_num += 1 99f400e0b8SCiara Power 100f400e0b8SCiara Power 101f400e0b8SCiara Powerdef cleanup_throughput_datatypes(data): 102f400e0b8SCiara Power """Cleanup data types of throughput test results dataframe. """ 103f400e0b8SCiara Power data.columns = data.columns.str.replace('/', ' ') 104f400e0b8SCiara Power data.columns = data.columns.str.strip() 105f400e0b8SCiara Power data['Burst Size'] = data['Burst Size'].astype('category') 106f400e0b8SCiara Power data['Buffer Size(B)'] = data['Buffer Size(B)'].astype('category') 107f400e0b8SCiara Power data['Failed Enq'] = data['Failed Enq'].astype('int') 108f400e0b8SCiara Power data['Throughput(Gbps)'] = data['Throughput(Gbps)'].astype('float') 109f400e0b8SCiara Power data['Ops(Millions)'] = data['Ops(Millions)'].astype('float') 110f400e0b8SCiara Power data['Cycles Buf'] = data['Cycles Buf'].astype('float') 111f400e0b8SCiara Power return data 112f400e0b8SCiara Power 113f400e0b8SCiara Power 114f400e0b8SCiara Powerdef cleanup_latency_datatypes(data): 115f400e0b8SCiara Power """Cleanup data types of latency test results dataframe. """ 116f400e0b8SCiara Power data.columns = data.columns.str.strip() 117f400e0b8SCiara Power data = data[['Burst Size', 'Buffer Size', 'time (us)']].copy() 118f400e0b8SCiara Power data['Burst Size'] = data['Burst Size'].astype('category') 119f400e0b8SCiara Power data['Buffer Size'] = data['Buffer Size'].astype('category') 120f400e0b8SCiara Power data['time (us)'] = data['time (us)'].astype('float') 121f400e0b8SCiara Power return data 122f400e0b8SCiara Power 123f400e0b8SCiara Power 124f400e0b8SCiara Powerdef process_test_results(grapher, data): 125f400e0b8SCiara Power """ 126f400e0b8SCiara Power Process results from the test case, 127f400e0b8SCiara Power calling graph functions to output graph images. 128f400e0b8SCiara Power """ 129f400e0b8SCiara Power if grapher.ptest == "throughput": 130f400e0b8SCiara Power grapher.data = cleanup_throughput_datatypes(data) 131f400e0b8SCiara Power for y_label in ["Throughput(Gbps)", "Ops(Millions)", 132f400e0b8SCiara Power "Cycles Buf", "Failed Enq"]: 133f400e0b8SCiara Power grapher.grouped_graph(y_label, "Buffer Size(B)", 134f400e0b8SCiara Power "Burst Size") 135f400e0b8SCiara Power elif grapher.ptest == "latency": 136f400e0b8SCiara Power clean_data = cleanup_latency_datatypes(data) 137f400e0b8SCiara Power for (burst, buffer), group in clean_data.groupby(['Burst Size', 138f400e0b8SCiara Power 'Buffer Size']): 139f400e0b8SCiara Power grapher.data = group 140f400e0b8SCiara Power grapher.histogram_graph("time (us)", burst, buffer) 141f400e0b8SCiara Power grapher.boxplot_graph("time (us)", burst, buffer) 142f400e0b8SCiara Power else: 143f400e0b8SCiara Power print("Invalid ptest") 144f400e0b8SCiara Power return 145f400e0b8SCiara Power 146f400e0b8SCiara Power 147f400e0b8SCiara Powerdef create_results_pdf(graph_path, pdf_path): 148f400e0b8SCiara Power """Output results graphs to PDFs.""" 149f400e0b8SCiara Power if not os.path.exists(pdf_path): 150f400e0b8SCiara Power os.makedirs(pdf_path) 151f400e0b8SCiara Power for _, dirs, _ in os.walk(graph_path): 152f400e0b8SCiara Power for sub in dirs: 153f400e0b8SCiara Power graphs = sorted(glob.glob(os.path.join(graph_path, sub, "*.jpg")), 154f400e0b8SCiara Power key=(lambda x: int((x.rsplit('/', 1)[1]) 155f400e0b8SCiara Power .split('.')[0]))) 156f400e0b8SCiara Power if graphs: 157f400e0b8SCiara Power with open(pdf_path + "%s_results.pdf" % sub, "wb") as pdf_file: 158f400e0b8SCiara Power pdf_file.write(img2pdf.convert(graphs)) 159f400e0b8SCiara Power 160f400e0b8SCiara Power 161f400e0b8SCiara Powerdef run_test(test_cmd, test, grapher, params, verbose): 162f400e0b8SCiara Power """Run performance test app for the given test case parameters.""" 163f400e0b8SCiara Power process = subprocess.Popen(["stdbuf", "-oL", test_cmd] + params, 164f400e0b8SCiara Power universal_newlines=True, 165f400e0b8SCiara Power stdout=subprocess.PIPE, 166f400e0b8SCiara Power stderr=subprocess.STDOUT) 167f400e0b8SCiara Power rows = [] 168f400e0b8SCiara Power if verbose: 169f400e0b8SCiara Power print("\n\tOutput for " + test + ":") 170f400e0b8SCiara Power while process.poll() is None: 171f400e0b8SCiara Power line = process.stdout.readline().strip() 172f400e0b8SCiara Power if not line: 173f400e0b8SCiara Power continue 174f400e0b8SCiara Power if verbose: 175f400e0b8SCiara Power print("\t\t>>" + line) 176f400e0b8SCiara Power 177f400e0b8SCiara Power if line.replace(' ', '').startswith('#lcore'): 178f400e0b8SCiara Power columns = line[1:].split(',') 179f400e0b8SCiara Power elif line[0].isdigit(): 180f400e0b8SCiara Power line = line.replace(';', ',') 181f400e0b8SCiara Power rows.append(line.split(',')) 182f400e0b8SCiara Power else: 183f400e0b8SCiara Power continue 184f400e0b8SCiara Power 185f400e0b8SCiara Power if process.poll() != 0 or not columns or not rows: 186f400e0b8SCiara Power print("\n\t" + test + ": FAIL") 187f400e0b8SCiara Power return 188f400e0b8SCiara Power data = pd.DataFrame(rows, columns=columns) 189f400e0b8SCiara Power grapher.test = test 190f400e0b8SCiara Power process_test_results(grapher, data) 191f400e0b8SCiara Power print("\n\t" + test + ": OK") 192f400e0b8SCiara Power return 193f400e0b8SCiara Power 194f400e0b8SCiara Power 195*5ba31a4eSCiara Powerdef parse_parameters(config_parameters): 196*5ba31a4eSCiara Power """Convert the JSON config to list of strings.""" 197*5ba31a4eSCiara Power params = [] 198*5ba31a4eSCiara Power for (key, val) in config_parameters: 199*5ba31a4eSCiara Power if isinstance(val, bool): 200*5ba31a4eSCiara Power params.append("--" + key if val is True else "") 201*5ba31a4eSCiara Power elif len(key) == 1: 202*5ba31a4eSCiara Power params.append("-" + key) 203*5ba31a4eSCiara Power params.append(val) 204*5ba31a4eSCiara Power else: 205*5ba31a4eSCiara Power params.append("--" + key + "=" + val) 206*5ba31a4eSCiara Power return params 207*5ba31a4eSCiara Power 208*5ba31a4eSCiara Power 209f400e0b8SCiara Powerdef run_test_suite(test_cmd, suite_config, verbose): 210f400e0b8SCiara Power """Parse test cases for the test suite and run each test.""" 211f400e0b8SCiara Power print("\nRunning Test Suite: " + suite_config['suite']) 212f400e0b8SCiara Power graph_path = os.path.join(suite_config['output_path'], GRAPH_DIR, 213f400e0b8SCiara Power suite_config['suite'], "") 214f400e0b8SCiara Power grapher = Grapher(suite_config['config_name'], suite_config['suite'], 215f400e0b8SCiara Power graph_path) 216f400e0b8SCiara Power test_cases = suite_config['test_cases'] 217f400e0b8SCiara Power if 'default' not in test_cases: 218f400e0b8SCiara Power print("Test Suite must contain default case, skipping") 219f400e0b8SCiara Power return 220f400e0b8SCiara Power 221*5ba31a4eSCiara Power default_params = parse_parameters(test_cases['default']['eal'].items()) 222f400e0b8SCiara Power default_params.append("--") 223*5ba31a4eSCiara Power default_params += parse_parameters(test_cases['default']['app'].items()) 224f400e0b8SCiara Power 225f400e0b8SCiara Power if 'ptest' not in test_cases['default']['app']: 226f400e0b8SCiara Power print("Test Suite must contain default ptest value, skipping") 227f400e0b8SCiara Power return 228f400e0b8SCiara Power grapher.ptest = test_cases['default']['app']['ptest'] 229f400e0b8SCiara Power 230f400e0b8SCiara Power for (test, params) in {k: v for (k, v) in test_cases.items() if 231f400e0b8SCiara Power k != "default"}.items(): 232*5ba31a4eSCiara Power extra_params = parse_parameters(params.items()) 233f400e0b8SCiara Power run_test(test_cmd, test, grapher, default_params + extra_params, 234f400e0b8SCiara Power verbose) 235f400e0b8SCiara Power 236f400e0b8SCiara Power create_results_pdf(graph_path, os.path.join(suite_config['output_path'], 237f400e0b8SCiara Power suite_config['suite'], "")) 238f400e0b8SCiara Power 239f400e0b8SCiara Power 240f400e0b8SCiara Powerdef parse_args(): 241f400e0b8SCiara Power """Parse command-line arguments passed to script.""" 242f400e0b8SCiara Power parser = ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter) 243f400e0b8SCiara Power parser.add_argument('config_path', type=str, 244f400e0b8SCiara Power help="Path to JSON configuration file") 245f400e0b8SCiara Power parser.add_argument('-t', '--test-suites', nargs='+', default=["all"], 246f400e0b8SCiara Power help="List of test suites to run") 247f400e0b8SCiara Power parser.add_argument('-v', '--verbose', action='store_true', 248f400e0b8SCiara Power help="""Display perf test app output. 249f400e0b8SCiara Power Not recommended for latency tests.""") 250f400e0b8SCiara Power parser.add_argument('-f', '--file-path', 251f400e0b8SCiara Power default=shutil.which('dpdk-test-crypto-perf'), 252f400e0b8SCiara Power help="Path for perf test app") 253f400e0b8SCiara Power parser.add_argument('-o', '--output-path', default=SCRIPT_PATH, 254f400e0b8SCiara Power help="Path to store output directories") 255f400e0b8SCiara Power args = parser.parse_args() 256f400e0b8SCiara Power return (args.file_path, args.test_suites, args.config_path, 257f400e0b8SCiara Power args.output_path, args.verbose) 258f400e0b8SCiara Power 259f400e0b8SCiara Power 260f400e0b8SCiara Powerdef main(): 261f400e0b8SCiara Power """ 262f400e0b8SCiara Power Load JSON config and call relevant functions to run chosen test suites. 263f400e0b8SCiara Power """ 264f400e0b8SCiara Power test_cmd, test_suites, config_file, output_path, verbose = parse_args() 265f400e0b8SCiara Power if test_cmd is None or not os.path.isfile(test_cmd): 266f400e0b8SCiara Power print("Invalid filepath for perf test app!") 267f400e0b8SCiara Power return 268f400e0b8SCiara Power try: 269f400e0b8SCiara Power with open(config_file) as conf: 270f400e0b8SCiara Power test_suite_ops = json.load(conf) 271f400e0b8SCiara Power config_name = os.path.splitext(config_file)[0] 272f400e0b8SCiara Power if '/' in config_name: 273f400e0b8SCiara Power config_name = config_name.rsplit('/', 1)[1] 274f400e0b8SCiara Power output_path = os.path.join(output_path, config_name, "") 275f400e0b8SCiara Power print("Using config: " + config_file) 276f400e0b8SCiara Power except OSError as err: 277f400e0b8SCiara Power print("Error with JSON file path: " + err.strerror) 278f400e0b8SCiara Power return 279f400e0b8SCiara Power except json.decoder.JSONDecodeError as err: 280f400e0b8SCiara Power print("Error loading JSON config: " + err.msg) 281f400e0b8SCiara Power return 282f400e0b8SCiara Power 283f400e0b8SCiara Power if test_suites != ["all"]: 284f400e0b8SCiara Power suite_list = [] 285f400e0b8SCiara Power for (suite, test_cases) in {k: v for (k, v) in test_suite_ops.items() 286f400e0b8SCiara Power if k in test_suites}.items(): 287f400e0b8SCiara Power suite_list.append(suite) 288f400e0b8SCiara Power suite_config = {'config_name': config_name, 'suite': suite, 289f400e0b8SCiara Power 'test_cases': test_cases, 290f400e0b8SCiara Power 'output_path': output_path} 291f400e0b8SCiara Power run_test_suite(test_cmd, suite_config, verbose) 292f400e0b8SCiara Power if not suite_list: 293f400e0b8SCiara Power print("No valid test suites chosen!") 294f400e0b8SCiara Power return 295f400e0b8SCiara Power else: 296f400e0b8SCiara Power for (suite, test_cases) in test_suite_ops.items(): 297f400e0b8SCiara Power suite_config = {'config_name': config_name, 'suite': suite, 298f400e0b8SCiara Power 'test_cases': test_cases, 299f400e0b8SCiara Power 'output_path': output_path} 300f400e0b8SCiara Power run_test_suite(test_cmd, suite_config, verbose) 301f400e0b8SCiara Power 302f400e0b8SCiara Power graph_path = os.path.join(output_path, GRAPH_DIR, "") 303f400e0b8SCiara Power if os.path.exists(graph_path): 304f400e0b8SCiara Power shutil.rmtree(graph_path) 305f400e0b8SCiara Power 306f400e0b8SCiara Power 307f400e0b8SCiara Powerif __name__ == "__main__": 308f400e0b8SCiara Power main() 309