xref: /spdk/scripts/perf/nvmf/common.py (revision 34edd9f1bf5fda4c987f4500ddc3c9f50be32e7d)
1588dfe31SMichal Berger#  SPDX-License-Identifier: BSD-3-Clause
2588dfe31SMichal Berger#  Copyright (C) 2018 Intel Corporation.
3588dfe31SMichal Berger#  All rights reserved.
4588dfe31SMichal Berger
53a359b79SKarol Lateckiimport os
63a359b79SKarol Lateckiimport re
73a359b79SKarol Lateckiimport json
83a359b79SKarol Lateckiimport logging
993e056baSKarol Lateckifrom subprocess import check_output
103a359b79SKarol Lateckifrom collections import OrderedDict
113a359b79SKarol Lateckifrom json.decoder import JSONDecodeError
12a42dfab1SKarol Latecki
13a42dfab1SKarol Latecki
143a359b79SKarol Lateckidef read_json_stats(file):
153a359b79SKarol Latecki    with open(file, "r") as json_data:
163a359b79SKarol Latecki        data = json.load(json_data)
17bd1b0714SKarol Latecki        job_data = data["jobs"][0]  # 0 because using aggregated results, fio group reporting
183a359b79SKarol Latecki
193a359b79SKarol Latecki        # Check if latency is in nano or microseconds to choose correct dict key
203a359b79SKarol Latecki        def get_lat_unit(key_prefix, dict_section):
213a359b79SKarol Latecki            # key prefix - lat, clat or slat.
223a359b79SKarol Latecki            # dict section - portion of json containing latency bucket in question
233a359b79SKarol Latecki            # Return dict key to access the bucket and unit as string
243a359b79SKarol Latecki            for k, _ in dict_section.items():
253a359b79SKarol Latecki                if k.startswith(key_prefix):
263a359b79SKarol Latecki                    return k, k.split("_")[1]
273a359b79SKarol Latecki
283a359b79SKarol Latecki        def get_clat_percentiles(clat_dict_leaf):
293a359b79SKarol Latecki            if "percentile" in clat_dict_leaf:
303a359b79SKarol Latecki                p99_lat = float(clat_dict_leaf["percentile"]["99.000000"])
313a359b79SKarol Latecki                p99_9_lat = float(clat_dict_leaf["percentile"]["99.900000"])
323a359b79SKarol Latecki                p99_99_lat = float(clat_dict_leaf["percentile"]["99.990000"])
333a359b79SKarol Latecki                p99_999_lat = float(clat_dict_leaf["percentile"]["99.999000"])
343a359b79SKarol Latecki
353a359b79SKarol Latecki                return [p99_lat, p99_9_lat, p99_99_lat, p99_999_lat]
363a359b79SKarol Latecki            else:
373a359b79SKarol Latecki                # Latest fio versions do not provide "percentile" results if no
383a359b79SKarol Latecki                # measurements were done, so just return zeroes
393a359b79SKarol Latecki                return [0, 0, 0, 0]
403a359b79SKarol Latecki
41bd1b0714SKarol Latecki        read_iops = float(job_data["read"]["iops"])
42bd1b0714SKarol Latecki        read_bw = float(job_data["read"]["bw"])
43bd1b0714SKarol Latecki        lat_key, lat_unit = get_lat_unit("lat", job_data["read"])
44bd1b0714SKarol Latecki        read_avg_lat = float(job_data["read"][lat_key]["mean"])
45bd1b0714SKarol Latecki        read_min_lat = float(job_data["read"][lat_key]["min"])
46bd1b0714SKarol Latecki        read_max_lat = float(job_data["read"][lat_key]["max"])
47bd1b0714SKarol Latecki        clat_key, clat_unit = get_lat_unit("clat", job_data["read"])
483a359b79SKarol Latecki        read_p99_lat, read_p99_9_lat, read_p99_99_lat, read_p99_999_lat = get_clat_percentiles(
49bd1b0714SKarol Latecki            job_data["read"][clat_key])
503a359b79SKarol Latecki
513a359b79SKarol Latecki        if "ns" in lat_unit:
523a359b79SKarol Latecki            read_avg_lat, read_min_lat, read_max_lat = [x / 1000 for x in [read_avg_lat, read_min_lat, read_max_lat]]
533a359b79SKarol Latecki        if "ns" in clat_unit:
543a359b79SKarol Latecki            read_p99_lat = read_p99_lat / 1000
553a359b79SKarol Latecki            read_p99_9_lat = read_p99_9_lat / 1000
563a359b79SKarol Latecki            read_p99_99_lat = read_p99_99_lat / 1000
573a359b79SKarol Latecki            read_p99_999_lat = read_p99_999_lat / 1000
583a359b79SKarol Latecki
59bd1b0714SKarol Latecki        write_iops = float(job_data["write"]["iops"])
60bd1b0714SKarol Latecki        write_bw = float(job_data["write"]["bw"])
61bd1b0714SKarol Latecki        lat_key, lat_unit = get_lat_unit("lat", job_data["write"])
62bd1b0714SKarol Latecki        write_avg_lat = float(job_data["write"][lat_key]["mean"])
63bd1b0714SKarol Latecki        write_min_lat = float(job_data["write"][lat_key]["min"])
64bd1b0714SKarol Latecki        write_max_lat = float(job_data["write"][lat_key]["max"])
65bd1b0714SKarol Latecki        clat_key, clat_unit = get_lat_unit("clat", job_data["write"])
663a359b79SKarol Latecki        write_p99_lat, write_p99_9_lat, write_p99_99_lat, write_p99_999_lat = get_clat_percentiles(
67bd1b0714SKarol Latecki            job_data["write"][clat_key])
683a359b79SKarol Latecki
693a359b79SKarol Latecki        if "ns" in lat_unit:
703a359b79SKarol Latecki            write_avg_lat, write_min_lat, write_max_lat = [x / 1000 for x in [write_avg_lat, write_min_lat, write_max_lat]]
713a359b79SKarol Latecki        if "ns" in clat_unit:
723a359b79SKarol Latecki            write_p99_lat = write_p99_lat / 1000
733a359b79SKarol Latecki            write_p99_9_lat = write_p99_9_lat / 1000
743a359b79SKarol Latecki            write_p99_99_lat = write_p99_99_lat / 1000
753a359b79SKarol Latecki            write_p99_999_lat = write_p99_999_lat / 1000
763a359b79SKarol Latecki
773a359b79SKarol Latecki    return [read_iops, read_bw, read_avg_lat, read_min_lat, read_max_lat,
783a359b79SKarol Latecki            read_p99_lat, read_p99_9_lat, read_p99_99_lat, read_p99_999_lat,
793a359b79SKarol Latecki            write_iops, write_bw, write_avg_lat, write_min_lat, write_max_lat,
803a359b79SKarol Latecki            write_p99_lat, write_p99_9_lat, write_p99_99_lat, write_p99_999_lat]
813a359b79SKarol Latecki
823a359b79SKarol Latecki
83da55cb87SKarol Lateckidef read_target_stats(measurement_name, results_file_list, results_dir):
84da55cb87SKarol Latecki    # Read additional metrics measurements done on target side and
85da55cb87SKarol Latecki    # calculate the average from across all workload iterations.
86da55cb87SKarol Latecki    # Currently only works for SAR CPU utilization and power draw measurements.
87da55cb87SKarol Latecki    # Other (bwm-ng, pcm, dpdk memory) need to be refactored and provide more
88da55cb87SKarol Latecki    # structured result files instead of a output dump.
89da55cb87SKarol Latecki    total_util = 0
90da55cb87SKarol Latecki    for result_file in results_file_list:
91da55cb87SKarol Latecki        with open(os.path.join(results_dir, result_file), "r") as result_file_fh:
92da55cb87SKarol Latecki            total_util += float(result_file_fh.read())
93da55cb87SKarol Latecki    avg_util = total_util / len(results_file_list)
94da55cb87SKarol Latecki
95da55cb87SKarol Latecki    return {measurement_name: "{0:.3f}".format(avg_util)}
96da55cb87SKarol Latecki
97da55cb87SKarol Latecki
983a359b79SKarol Lateckidef parse_results(results_dir, csv_file):
993a359b79SKarol Latecki    files = os.listdir(results_dir)
1003a359b79SKarol Latecki    fio_files = filter(lambda x: ".fio" in x, files)
1013a359b79SKarol Latecki    json_files = [x for x in files if ".json" in x]
102da55cb87SKarol Latecki    sar_files = [x for x in files if "sar" in x and "util" in x]
103da55cb87SKarol Latecki    pm_files = [x for x in files if "pm" in x and "avg" in x]
1043a359b79SKarol Latecki
1053a359b79SKarol Latecki    headers = ["read_iops", "read_bw", "read_avg_lat_us", "read_min_lat_us", "read_max_lat_us",
1063a359b79SKarol Latecki               "read_p99_lat_us", "read_p99.9_lat_us", "read_p99.99_lat_us", "read_p99.999_lat_us",
1073a359b79SKarol Latecki               "write_iops", "write_bw", "write_avg_lat_us", "write_min_lat_us", "write_max_lat_us",
1083a359b79SKarol Latecki               "write_p99_lat_us", "write_p99.9_lat_us", "write_p99.99_lat_us", "write_p99.999_lat_us"]
1093a359b79SKarol Latecki
1103a359b79SKarol Latecki    header_line = ",".join(["Name", *headers])
1113a359b79SKarol Latecki    rows = set()
1123a359b79SKarol Latecki
1133a359b79SKarol Latecki    for fio_config in fio_files:
1143a359b79SKarol Latecki        logging.info("Getting FIO stats for %s" % fio_config)
1153a359b79SKarol Latecki        job_name, _ = os.path.splitext(fio_config)
116da55cb87SKarol Latecki        aggr_headers = ["iops", "bw", "avg_lat_us", "min_lat_us", "max_lat_us",
117da55cb87SKarol Latecki                        "p99_lat_us", "p99.9_lat_us", "p99.99_lat_us", "p99.999_lat_us"]
1183a359b79SKarol Latecki
1193a359b79SKarol Latecki        # Look in the filename for rwmixread value. Function arguments do
1203a359b79SKarol Latecki        # not have that information.
1213a359b79SKarol Latecki        # TODO: Improve this function by directly using workload params instead
1223a359b79SKarol Latecki        # of regexing through filenames.
1233a359b79SKarol Latecki        if "read" in job_name:
1243a359b79SKarol Latecki            rw_mixread = 1
1253a359b79SKarol Latecki        elif "write" in job_name:
1263a359b79SKarol Latecki            rw_mixread = 0
1273a359b79SKarol Latecki        else:
1283a359b79SKarol Latecki            rw_mixread = float(re.search(r"m_(\d+)", job_name).group(1)) / 100
1293a359b79SKarol Latecki
1303a359b79SKarol Latecki        # If "_CPU" exists in name - ignore it
1313a359b79SKarol Latecki        # Initiators for the same job could have different num_cores parameter
1323a359b79SKarol Latecki        job_name = re.sub(r"_\d+CPU", "", job_name)
1333a359b79SKarol Latecki        job_result_files = [x for x in json_files if x.startswith(job_name)]
134da55cb87SKarol Latecki        sar_result_files = [x for x in sar_files if x.startswith(job_name)]
135bd1e8c2aSKamil Godzwon
136bd1e8c2aSKamil Godzwon        # Collect all pm files for the current job
137bd1e8c2aSKamil Godzwon        job_pm_files = [x for x in pm_files if x.startswith(job_name)]
138bd1e8c2aSKamil Godzwon
139bd1e8c2aSKamil Godzwon        # Filter out data from DCMI sensors and socket/dram sensors
140bd1e8c2aSKamil Godzwon        dcmi_sensors = [x for x in job_pm_files if "DCMI" in x]
141bd1e8c2aSKamil Godzwon        socket_dram_sensors = [x for x in job_pm_files if "DCMI" not in x and ("socket" in x or "dram" in x)]
142bd1e8c2aSKamil Godzwon        sdr_sensors = list(set(job_pm_files) - set(dcmi_sensors) - set(socket_dram_sensors))
143bd1e8c2aSKamil Godzwon
144bd1e8c2aSKamil Godzwon        # Determine the final list of pm_result_files, if DCMI file is present, use it as a primary source
145bd1e8c2aSKamil Godzwon        # of power consumption data. If not, use SDR sensors data if available. If SDR sensors are not available,
146bd1e8c2aSKamil Godzwon        # use socket and dram sensors as a fallback.
147bd1e8c2aSKamil Godzwon        pm_result_files = dcmi_sensors or sdr_sensors
148bd1e8c2aSKamil Godzwon        if not pm_result_files and socket_dram_sensors:
149bd1e8c2aSKamil Godzwon            logging.warning("No DCMI or SDR data found for %s, using socket and dram data sensors as a fallback" % job_name)
150bd1e8c2aSKamil Godzwon            pm_result_files = socket_dram_sensors
151da55cb87SKarol Latecki
152da55cb87SKarol Latecki        logging.info("Matching result files for current fio config %s:" % job_name)
1533a359b79SKarol Latecki        for j in job_result_files:
1543a359b79SKarol Latecki            logging.info("\t %s" % j)
1553a359b79SKarol Latecki
1563a359b79SKarol Latecki        # There may have been more than 1 initiator used in test, need to check that
1573a359b79SKarol Latecki        # Result files are created so that string after last "_" separator is server name
1583a359b79SKarol Latecki        inits_names = set([os.path.splitext(x)[0].split("_")[-1] for x in job_result_files])
1593a359b79SKarol Latecki        inits_avg_results = []
1603a359b79SKarol Latecki        for i in inits_names:
1613a359b79SKarol Latecki            logging.info("\tGetting stats for initiator %s" % i)
1623a359b79SKarol Latecki            # There may have been more than 1 test run for this job, calculate average results for initiator
1633a359b79SKarol Latecki            i_results = [x for x in job_result_files if i in x]
1643a359b79SKarol Latecki            i_results_filename = re.sub(r"run_\d+_", "", i_results[0].replace("json", "csv"))
1653a359b79SKarol Latecki
1663a359b79SKarol Latecki            separate_stats = []
1673a359b79SKarol Latecki            for r in i_results:
1683a359b79SKarol Latecki                try:
1693a359b79SKarol Latecki                    stats = read_json_stats(os.path.join(results_dir, r))
1703a359b79SKarol Latecki                    separate_stats.append(stats)
171aea1abb9SKarol Latecki                    logging.info([float("{0:.3f}".format(x)) for x in stats])
1723a359b79SKarol Latecki                except JSONDecodeError:
1733a359b79SKarol Latecki                    logging.error("ERROR: Failed to parse %s results! Results might be incomplete!" % r)
1743a359b79SKarol Latecki
1753a359b79SKarol Latecki            init_results = [sum(x) for x in zip(*separate_stats)]
1763a359b79SKarol Latecki            init_results = [x / len(separate_stats) for x in init_results]
177aea1abb9SKarol Latecki            init_results = [round(x, 3) for x in init_results]
1783a359b79SKarol Latecki            inits_avg_results.append(init_results)
1793a359b79SKarol Latecki
1803a359b79SKarol Latecki            logging.info("\tAverage results for initiator %s" % i)
1813a359b79SKarol Latecki            logging.info(init_results)
1823a359b79SKarol Latecki            with open(os.path.join(results_dir, i_results_filename), "w") as fh:
1833a359b79SKarol Latecki                fh.write(header_line + "\n")
1843a359b79SKarol Latecki                fh.write(",".join([job_name, *["{0:.3f}".format(x) for x in init_results]]) + "\n")
1853a359b79SKarol Latecki
1863a359b79SKarol Latecki        # Sum results of all initiators running this FIO job.
187*34edd9f1SKamil Godzwon        # Latency results are an average of latencies from across all initiators.
1883a359b79SKarol Latecki        inits_avg_results = [sum(x) for x in zip(*inits_avg_results)]
1893a359b79SKarol Latecki        inits_avg_results = OrderedDict(zip(headers, inits_avg_results))
1903a359b79SKarol Latecki        for key in inits_avg_results:
1913a359b79SKarol Latecki            if "lat" in key:
1923a359b79SKarol Latecki                inits_avg_results[key] /= len(inits_names)
1933a359b79SKarol Latecki
1943a359b79SKarol Latecki        # Aggregate separate read/write values into common labels
1953a359b79SKarol Latecki        # Take rw_mixread into consideration for mixed read/write workloads.
1963a359b79SKarol Latecki        aggregate_results = OrderedDict()
1973a359b79SKarol Latecki        for h in aggr_headers:
1983a359b79SKarol Latecki            read_stat, write_stat = [float(value) for key, value in inits_avg_results.items() if h in key]
1993a359b79SKarol Latecki            if "lat" in h:
2003a359b79SKarol Latecki                _ = rw_mixread * read_stat + (1 - rw_mixread) * write_stat
2013a359b79SKarol Latecki            else:
2023a359b79SKarol Latecki                _ = read_stat + write_stat
2033a359b79SKarol Latecki            aggregate_results[h] = "{0:.3f}".format(_)
2043a359b79SKarol Latecki
205da55cb87SKarol Latecki        if sar_result_files:
206da55cb87SKarol Latecki            aggr_headers.append("target_avg_cpu_util")
207da55cb87SKarol Latecki            aggregate_results.update(read_target_stats("target_avg_cpu_util", sar_result_files, results_dir))
208da55cb87SKarol Latecki
209da55cb87SKarol Latecki        if pm_result_files:
210da55cb87SKarol Latecki            aggr_headers.append("target_avg_power")
211da55cb87SKarol Latecki            aggregate_results.update(read_target_stats("target_avg_power", pm_result_files, results_dir))
212da55cb87SKarol Latecki
2133a359b79SKarol Latecki        rows.add(",".join([job_name, *aggregate_results.values()]))
2143a359b79SKarol Latecki
215da55cb87SKarol Latecki    # Create empty results file with just the header line
216da55cb87SKarol Latecki    aggr_header_line = ",".join(["Name", *aggr_headers])
217da55cb87SKarol Latecki    with open(os.path.join(results_dir, csv_file), "w") as fh:
218da55cb87SKarol Latecki        fh.write(aggr_header_line + "\n")
219da55cb87SKarol Latecki
2203a359b79SKarol Latecki    # Save results to file
2213a359b79SKarol Latecki    for row in rows:
2223a359b79SKarol Latecki        with open(os.path.join(results_dir, csv_file), "a") as fh:
2233a359b79SKarol Latecki            fh.write(row + "\n")
2243a359b79SKarol Latecki    logging.info("You can find the test results in the file %s" % os.path.join(results_dir, csv_file))
225