xref: /spdk/scripts/perf/nvmf/common.py (revision 588dfe314bb83d86effdf67ec42837b11c2620bf)
1#  SPDX-License-Identifier: BSD-3-Clause
2#  Copyright (C) 2018 Intel Corporation.
3#  All rights reserved.
4
5import os
6import re
7import json
8import logging
9from subprocess import check_output
10from collections import OrderedDict
11from json.decoder import JSONDecodeError
12
13
14def read_json_stats(file):
15    with open(file, "r") as json_data:
16        data = json.load(json_data)
17        job_pos = 0  # job_post = 0 because using aggregated results
18
19        # Check if latency is in nano or microseconds to choose correct dict key
20        def get_lat_unit(key_prefix, dict_section):
21            # key prefix - lat, clat or slat.
22            # dict section - portion of json containing latency bucket in question
23            # Return dict key to access the bucket and unit as string
24            for k, _ in dict_section.items():
25                if k.startswith(key_prefix):
26                    return k, k.split("_")[1]
27
28        def get_clat_percentiles(clat_dict_leaf):
29            if "percentile" in clat_dict_leaf:
30                p99_lat = float(clat_dict_leaf["percentile"]["99.000000"])
31                p99_9_lat = float(clat_dict_leaf["percentile"]["99.900000"])
32                p99_99_lat = float(clat_dict_leaf["percentile"]["99.990000"])
33                p99_999_lat = float(clat_dict_leaf["percentile"]["99.999000"])
34
35                return [p99_lat, p99_9_lat, p99_99_lat, p99_999_lat]
36            else:
37                # Latest fio versions do not provide "percentile" results if no
38                # measurements were done, so just return zeroes
39                return [0, 0, 0, 0]
40
41        read_iops = float(data["jobs"][job_pos]["read"]["iops"])
42        read_bw = float(data["jobs"][job_pos]["read"]["bw"])
43        lat_key, lat_unit = get_lat_unit("lat", data["jobs"][job_pos]["read"])
44        read_avg_lat = float(data["jobs"][job_pos]["read"][lat_key]["mean"])
45        read_min_lat = float(data["jobs"][job_pos]["read"][lat_key]["min"])
46        read_max_lat = float(data["jobs"][job_pos]["read"][lat_key]["max"])
47        clat_key, clat_unit = get_lat_unit("clat", data["jobs"][job_pos]["read"])
48        read_p99_lat, read_p99_9_lat, read_p99_99_lat, read_p99_999_lat = get_clat_percentiles(
49            data["jobs"][job_pos]["read"][clat_key])
50
51        if "ns" in lat_unit:
52            read_avg_lat, read_min_lat, read_max_lat = [x / 1000 for x in [read_avg_lat, read_min_lat, read_max_lat]]
53        if "ns" in clat_unit:
54            read_p99_lat = read_p99_lat / 1000
55            read_p99_9_lat = read_p99_9_lat / 1000
56            read_p99_99_lat = read_p99_99_lat / 1000
57            read_p99_999_lat = read_p99_999_lat / 1000
58
59        write_iops = float(data["jobs"][job_pos]["write"]["iops"])
60        write_bw = float(data["jobs"][job_pos]["write"]["bw"])
61        lat_key, lat_unit = get_lat_unit("lat", data["jobs"][job_pos]["write"])
62        write_avg_lat = float(data["jobs"][job_pos]["write"][lat_key]["mean"])
63        write_min_lat = float(data["jobs"][job_pos]["write"][lat_key]["min"])
64        write_max_lat = float(data["jobs"][job_pos]["write"][lat_key]["max"])
65        clat_key, clat_unit = get_lat_unit("clat", data["jobs"][job_pos]["write"])
66        write_p99_lat, write_p99_9_lat, write_p99_99_lat, write_p99_999_lat = get_clat_percentiles(
67            data["jobs"][job_pos]["write"][clat_key])
68
69        if "ns" in lat_unit:
70            write_avg_lat, write_min_lat, write_max_lat = [x / 1000 for x in [write_avg_lat, write_min_lat, write_max_lat]]
71        if "ns" in clat_unit:
72            write_p99_lat = write_p99_lat / 1000
73            write_p99_9_lat = write_p99_9_lat / 1000
74            write_p99_99_lat = write_p99_99_lat / 1000
75            write_p99_999_lat = write_p99_999_lat / 1000
76
77    return [read_iops, read_bw, read_avg_lat, read_min_lat, read_max_lat,
78            read_p99_lat, read_p99_9_lat, read_p99_99_lat, read_p99_999_lat,
79            write_iops, write_bw, write_avg_lat, write_min_lat, write_max_lat,
80            write_p99_lat, write_p99_9_lat, write_p99_99_lat, write_p99_999_lat]
81
82
83def parse_results(results_dir, csv_file):
84    files = os.listdir(results_dir)
85    fio_files = filter(lambda x: ".fio" in x, files)
86    json_files = [x for x in files if ".json" in x]
87
88    headers = ["read_iops", "read_bw", "read_avg_lat_us", "read_min_lat_us", "read_max_lat_us",
89               "read_p99_lat_us", "read_p99.9_lat_us", "read_p99.99_lat_us", "read_p99.999_lat_us",
90               "write_iops", "write_bw", "write_avg_lat_us", "write_min_lat_us", "write_max_lat_us",
91               "write_p99_lat_us", "write_p99.9_lat_us", "write_p99.99_lat_us", "write_p99.999_lat_us"]
92
93    aggr_headers = ["iops", "bw", "avg_lat_us", "min_lat_us", "max_lat_us",
94                    "p99_lat_us", "p99.9_lat_us", "p99.99_lat_us", "p99.999_lat_us"]
95
96    header_line = ",".join(["Name", *headers])
97    aggr_header_line = ",".join(["Name", *aggr_headers])
98
99    # Create empty results file
100    with open(os.path.join(results_dir, csv_file), "w") as fh:
101        fh.write(aggr_header_line + "\n")
102    rows = set()
103
104    for fio_config in fio_files:
105        logging.info("Getting FIO stats for %s" % fio_config)
106        job_name, _ = os.path.splitext(fio_config)
107
108        # Look in the filename for rwmixread value. Function arguments do
109        # not have that information.
110        # TODO: Improve this function by directly using workload params instead
111        # of regexing through filenames.
112        if "read" in job_name:
113            rw_mixread = 1
114        elif "write" in job_name:
115            rw_mixread = 0
116        else:
117            rw_mixread = float(re.search(r"m_(\d+)", job_name).group(1)) / 100
118
119        # If "_CPU" exists in name - ignore it
120        # Initiators for the same job could have different num_cores parameter
121        job_name = re.sub(r"_\d+CPU", "", job_name)
122        job_result_files = [x for x in json_files if x.startswith(job_name)]
123        logging.info("Matching result files for current fio config:")
124        for j in job_result_files:
125            logging.info("\t %s" % j)
126
127        # There may have been more than 1 initiator used in test, need to check that
128        # Result files are created so that string after last "_" separator is server name
129        inits_names = set([os.path.splitext(x)[0].split("_")[-1] for x in job_result_files])
130        inits_avg_results = []
131        for i in inits_names:
132            logging.info("\tGetting stats for initiator %s" % i)
133            # There may have been more than 1 test run for this job, calculate average results for initiator
134            i_results = [x for x in job_result_files if i in x]
135            i_results_filename = re.sub(r"run_\d+_", "", i_results[0].replace("json", "csv"))
136
137            separate_stats = []
138            for r in i_results:
139                try:
140                    stats = read_json_stats(os.path.join(results_dir, r))
141                    separate_stats.append(stats)
142                    logging.info(stats)
143                except JSONDecodeError:
144                    logging.error("ERROR: Failed to parse %s results! Results might be incomplete!" % r)
145
146            init_results = [sum(x) for x in zip(*separate_stats)]
147            init_results = [x / len(separate_stats) for x in init_results]
148            inits_avg_results.append(init_results)
149
150            logging.info("\tAverage results for initiator %s" % i)
151            logging.info(init_results)
152            with open(os.path.join(results_dir, i_results_filename), "w") as fh:
153                fh.write(header_line + "\n")
154                fh.write(",".join([job_name, *["{0:.3f}".format(x) for x in init_results]]) + "\n")
155
156        # Sum results of all initiators running this FIO job.
157        # Latency results are an average of latencies from accros all initiators.
158        inits_avg_results = [sum(x) for x in zip(*inits_avg_results)]
159        inits_avg_results = OrderedDict(zip(headers, inits_avg_results))
160        for key in inits_avg_results:
161            if "lat" in key:
162                inits_avg_results[key] /= len(inits_names)
163
164        # Aggregate separate read/write values into common labels
165        # Take rw_mixread into consideration for mixed read/write workloads.
166        aggregate_results = OrderedDict()
167        for h in aggr_headers:
168            read_stat, write_stat = [float(value) for key, value in inits_avg_results.items() if h in key]
169            if "lat" in h:
170                _ = rw_mixread * read_stat + (1 - rw_mixread) * write_stat
171            else:
172                _ = read_stat + write_stat
173            aggregate_results[h] = "{0:.3f}".format(_)
174
175        rows.add(",".join([job_name, *aggregate_results.values()]))
176
177    # Save results to file
178    for row in rows:
179        with open(os.path.join(results_dir, csv_file), "a") as fh:
180            fh.write(row + "\n")
181    logging.info("You can find the test results in the file %s" % os.path.join(results_dir, csv_file))
182