xref: /dpdk/app/test-crypto-perf/dpdk-graph-crypto-perf.py (revision 5ba31a4e2e27cf0f78e4db138c42b79d8e0ccc95)
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