From ece3a998831dff9f111eb8432dd85ee476fc5a56 Mon Sep 17 00:00:00 2001 From: Vincent Fu Date: Tue, 9 Jan 2024 04:14:19 +0000 Subject: [PATCH] t/nvmept_fdp.py: test script for FDP This test script uses the io_uring pass-through ioengine to test fio's FDP support. This uses both the orignal and the new fdp-related options. Signed-off-by: Vincent Fu --- t/nvmept_fdp.py | 745 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 745 insertions(+) create mode 100755 t/nvmept_fdp.py diff --git a/t/nvmept_fdp.py b/t/nvmept_fdp.py new file mode 100755 index 00000000..031b439c --- /dev/null +++ b/t/nvmept_fdp.py @@ -0,0 +1,745 @@ +#!/usr/bin/env python3 +# +# Copyright 2024 Samsung Electronics Co., Ltd All Rights Reserved +# +# For conditions of distribution and use, see the accompanying COPYING file. +# +""" +# nvmept_fdp.py +# +# Test fio's io_uring_cmd ioengine with NVMe pass-through FDP write commands. +# +# USAGE +# see python3 nvmept_fdp.py --help +# +# EXAMPLES +# python3 t/nvmept_fdp.py --dut /dev/ng0n1 +# python3 t/nvmept_fdp.py --dut /dev/ng1n1 -f ./fio +# +# REQUIREMENTS +# Python 3.6 +# Device formatted with LBA data size 4096 bytes +# Device with at least five placement IDs +# +# WARNING +# This is a destructive test +""" +import os +import sys +import json +import time +import locale +import logging +import argparse +import subprocess +from pathlib import Path +from fiotestlib import FioJobCmdTest, run_fio_tests +from fiotestcommon import SUCCESS_NONZERO + + +class FDPTest(FioJobCmdTest): + """ + NVMe pass-through test class. Check to make sure output for selected data + direction(s) is non-zero and that zero data appears for other directions. + """ + + def setup(self, parameters): + """Setup a test.""" + + fio_args = [ + "--name=nvmept-fdp", + "--ioengine=io_uring_cmd", + "--cmd_type=nvme", + "--randrepeat=0", + f"--filename={self.fio_opts['filename']}", + f"--rw={self.fio_opts['rw']}", + f"--output={self.filenames['output']}", + f"--output-format={self.fio_opts['output-format']}", + ] + for opt in ['fixedbufs', 'nonvectored', 'force_async', 'registerfiles', + 'sqthread_poll', 'sqthread_poll_cpu', 'hipri', 'nowait', + 'time_based', 'runtime', 'verify', 'io_size', 'num_range', + 'iodepth', 'iodepth_batch', 'iodepth_batch_complete', + 'size', 'rate', 'bs', 'bssplit', 'bsrange', 'randrepeat', + 'buffer_pattern', 'verify_pattern', 'offset', 'fdp', + 'fdp_pli', 'fdp_pli_select', 'dataplacement', 'plid_select', + 'plids', 'number_ios']: + if opt in self.fio_opts: + option = f"--{opt}={self.fio_opts[opt]}" + fio_args.append(option) + + super().setup(fio_args) + + + def check_result(self): + try: + self._check_result() + finally: + if not update_all_ruhs(self.fio_opts['filename']): + logging.error("Could not reset device") + if not check_all_ruhs(self.fio_opts['filename']): + logging.error("Reclaim units have inconsistent RUAMW values") + + + def _check_result(self): + + super().check_result() + + if 'rw' not in self.fio_opts or \ + not self.passed or \ + 'json' not in self.fio_opts['output-format']: + return + + job = self.json_data['jobs'][0] + + if self.fio_opts['rw'] in ['read', 'randread']: + self.passed = self.check_all_ddirs(['read'], job) + elif self.fio_opts['rw'] in ['write', 'randwrite']: + if 'verify' not in self.fio_opts: + self.passed = self.check_all_ddirs(['write'], job) + else: + self.passed = self.check_all_ddirs(['read', 'write'], job) + elif self.fio_opts['rw'] in ['trim', 'randtrim']: + self.passed = self.check_all_ddirs(['trim'], job) + elif self.fio_opts['rw'] in ['readwrite', 'randrw']: + self.passed = self.check_all_ddirs(['read', 'write'], job) + elif self.fio_opts['rw'] in ['trimwrite', 'randtrimwrite']: + self.passed = self.check_all_ddirs(['trim', 'write'], job) + else: + logging.error("Unhandled rw value %s", self.fio_opts['rw']) + self.passed = False + + if 'iodepth' in self.fio_opts: + # We will need to figure something out if any test uses an iodepth + # different from 8 + if job['iodepth_level']['8'] < 95: + logging.error("Did not achieve requested iodepth") + self.passed = False + else: + logging.debug("iodepth 8 target met %s", job['iodepth_level']['8']) + + +class FDPMultiplePLIDTest(FDPTest): + """ + Write to multiple placement IDs. + """ + + def setup(self, parameters): + mapping = { + 'nruhsd': FIO_FDP_NUMBER_PLIDS, + 'max_ruamw': FIO_FDP_MAX_RUAMW, + } + if 'number_ios' in self.fio_opts and isinstance(self.fio_opts['number_ios'], str): + self.fio_opts['number_ios'] = eval(self.fio_opts['number_ios'].format(**mapping)) + + super().setup(parameters) + + def _check_result(self): + if 'fdp_pli' in self.fio_opts: + plid_list = self.fio_opts['fdp_pli'].split(',') + elif 'plids' in self.fio_opts: + plid_list = self.fio_opts['plids'].split(',') + else: + plid_list = list(range(FIO_FDP_NUMBER_PLIDS)) + + plid_list = sorted([int(i) for i in plid_list]) + logging.debug("plid_list: %s", str(plid_list)) + + fdp_status = get_fdp_status(self.fio_opts['filename']) + + select = "roundrobin" + if 'fdp_pli_select' in self.fio_opts: + select = self.fio_opts['fdp_pli_select'] + elif 'plid_select' in self.fio_opts: + select = self.fio_opts['plid_select'] + + if select == "roundrobin": + self._check_robin(plid_list, fdp_status) + elif select == "random": + self._check_random(plid_list, fdp_status) + else: + logging.error("Unknown plid selection strategy %s", select) + self.passed = False + + super()._check_result() + + def _check_robin(self, plid_list, fdp_status): + """ + With round robin we can know exactly how many writes each PLID will + receive. + """ + ruamw = [FIO_FDP_MAX_RUAMW] * FIO_FDP_NUMBER_PLIDS + + remainder = int(self.fio_opts['number_ios'] % len(plid_list)) + whole = int((self.fio_opts['number_ios'] - remainder) / len(plid_list)) + logging.debug("PLIDs in the list should receive %d writes; %d PLIDs will receive one extra", + whole, remainder) + + for plid in plid_list: + ruamw[plid] -= whole + if remainder: + ruamw[plid] -= 1 + remainder -= 1 + logging.debug("Expected ruamw values: %s", str(ruamw)) + + for idx, ruhs in enumerate(fdp_status['ruhss']): + if ruhs['ruamw'] != ruamw[idx]: + logging.error("RUAMW mismatch with idx %d, pid %d, expected %d, observed %d", idx, + ruhs['pid'], ruamw[idx], ruhs['ruamw']) + self.passed = False + break + + logging.debug("RUAMW match with idx %d, pid %d: ruamw=%d", idx, ruhs['pid'], ruamw[idx]) + + def _check_random(self, plid_list, fdp_status): + """ + With random selection, a set of PLIDs will receive all the write + operations and the remainder will be untouched. + """ + + total_ruamw = 0 + for plid in plid_list: + total_ruamw += fdp_status['ruhss'][plid]['ruamw'] + + expected = len(plid_list) * FIO_FDP_MAX_RUAMW - self.fio_opts['number_ios'] + if total_ruamw != expected: + logging.error("Expected total ruamw %d for plids %s, observed %d", expected, + str(plid_list), total_ruamw) + self.passed = False + else: + logging.debug("Observed expected total ruamw %d for plids %s", expected, str(plid_list)) + + for idx, ruhs in enumerate(fdp_status['ruhss']): + if idx in plid_list: + continue + if ruhs['ruamw'] != FIO_FDP_MAX_RUAMW: + logging.error("Unexpected ruamw %d for idx %d, pid %d, expected %d", ruhs['ruamw'], + idx, ruhs['pid'], FIO_FDP_MAX_RUAMW) + self.passed = False + else: + logging.debug("Observed expected ruamw %d for idx %d, pid %d", ruhs['ruamw'], idx, + ruhs['pid']) + + +class FDPSinglePLIDTest(FDPTest): + """ + Write to a single placement ID only. + """ + + def _check_result(self): + if 'plids' in self.fio_opts: + plid = self.fio_opts['plids'] + elif 'fdp_pli' in self.fio_opts: + plid = self.fio_opts['fdp_pli'] + else: + plid = 0 + + fdp_status = get_fdp_status(self.fio_opts['filename']) + ruamw = fdp_status['ruhss'][plid]['ruamw'] + lba_count = self.fio_opts['number_ios'] + + if FIO_FDP_MAX_RUAMW - lba_count != ruamw: + logging.error("FDP accounting mismatch for plid %d; expected ruamw %d, observed %d", + plid, FIO_FDP_MAX_RUAMW - lba_count, ruamw) + self.passed = False + else: + logging.debug("FDP accounting as expected for plid %d; ruamw = %d", plid, ruamw) + + super()._check_result() + + +class FDPReadTest(FDPTest): + """ + Read workload test. + """ + + def _check_result(self): + ruamw = check_all_ruhs(self.fio_opts['filename']) + + if ruamw != FIO_FDP_MAX_RUAMW: + logging.error("Read workload affected FDP ruamw") + self.passed = False + else: + logging.debug("Read workload did not disturb FDP ruamw") + super()._check_result() + + +def get_fdp_status(dut): + """ + Run the nvme-cli command to obtain FDP status and return result as a JSON + object. + """ + + cmd = f"sudo nvme fdp status --output-format=json {dut}" + cmd = cmd.split(' ') + cmd_result = subprocess.run(cmd, capture_output=True, check=False, + encoding=locale.getpreferredencoding()) + + if cmd_result.returncode != 0: + logging.error("Error obtaining device %s FDP status: %s", dut, cmd_result.stderr) + return False + + return json.loads(cmd_result.stdout) + + +def update_ruh(dut, plid): + """ + Update reclaim unit handles with specified ID(s). This tells the device to + point the RUH to a new (empty) reclaim unit. + """ + + ids = ','.join(plid) if isinstance(plid, list) else plid + cmd = f"nvme fdp update --pids={ids} {dut}" + cmd = cmd.split(' ') + cmd_result = subprocess.run(cmd, capture_output=True, check=False, + encoding=locale.getpreferredencoding()) + + if cmd_result.returncode != 0: + logging.error("Error updating RUH %s ID(s) %s", dut, ids) + return False + + return True + + +def update_all_ruhs(dut): + """ + Update all reclaim unit handles on the device. + """ + + fdp_status = get_fdp_status(dut) + for ruhs in fdp_status['ruhss']: + if not update_ruh(dut, ruhs['pid']): + return False + + return True + + +def check_all_ruhs(dut): + """ + Check that all RUHs have the same value for reclaim unit available media + writes (RUAMW). Return the RUAMW value. + """ + + fdp_status = get_fdp_status(dut) + ruh_status = fdp_status['ruhss'] + + ruamw = ruh_status[0]['ruamw'] + for ruhs in ruh_status: + if ruhs['ruamw'] != ruamw: + logging.error("RUAMW mismatch: found %d, expected %d", ruhs['ruamw'], ruamw) + return False + + return ruamw + + +TEST_LIST = [ + # Write one LBA to one PLID using both the old and new sets of options + ## omit fdp_pli_select/plid_select + { + "test_id": 1, + "fio_opts": { + "rw": 'write', + "bs": 4096, + "number_ios": 1, + "verify": "crc32c", + "fdp": 1, + "fdp_pli": 3, + "output-format": "json", + }, + "test_class": FDPSinglePLIDTest, + }, + { + "test_id": 2, + "fio_opts": { + "rw": 'randwrite', + "bs": 4096, + "number_ios": 1, + "verify": "crc32c", + "dataplacement": "fdp", + "plids": 3, + "output-format": "json", + }, + "test_class": FDPSinglePLIDTest, + }, + ## fdp_pli_select/plid_select=roundrobin + { + "test_id": 3, + "fio_opts": { + "rw": 'write', + "bs": 4096, + "number_ios": 1, + "verify": "crc32c", + "fdp": 1, + "fdp_pli": 3, + "fdp_pli_select": "roundrobin", + "output-format": "json", + }, + "test_class": FDPSinglePLIDTest, + }, + { + "test_id": 4, + "fio_opts": { + "rw": 'randwrite', + "bs": 4096, + "number_ios": 1, + "verify": "crc32c", + "dataplacement": "fdp", + "plids": 3, + "plid_select": "roundrobin", + "output-format": "json", + }, + "test_class": FDPSinglePLIDTest, + }, + ## fdp_pli_select/plid_select=random + { + "test_id": 5, + "fio_opts": { + "rw": 'write', + "bs": 4096, + "number_ios": 1, + "verify": "crc32c", + "fdp": 1, + "fdp_pli": 3, + "fdp_pli_select": "random", + "output-format": "json", + }, + "test_class": FDPSinglePLIDTest, + }, + { + "test_id": 6, + "fio_opts": { + "rw": 'randwrite', + "bs": 4096, + "number_ios": 1, + "verify": "crc32c", + "dataplacement": "fdp", + "plids": 3, + "plid_select": "random", + "output-format": "json", + }, + "test_class": FDPSinglePLIDTest, + }, + # Write four LBAs to one PLID using both the old and new sets of options + ## omit fdp_pli_select/plid_select + { + "test_id": 7, + "fio_opts": { + "rw": 'write', + "bs": 4096, + "number_ios": 4, + "verify": "crc32c", + "fdp": 1, + "fdp_pli": 1, + "output-format": "json", + }, + "test_class": FDPSinglePLIDTest, + }, + { + "test_id": 8, + "fio_opts": { + "rw": 'randwrite', + "bs": 4096, + "number_ios": 4, + "verify": "crc32c", + "dataplacement": "fdp", + "plids": 1, + "output-format": "json", + }, + "test_class": FDPSinglePLIDTest, + }, + ## fdp_pli_select/plid_select=roundrobin + { + "test_id": 9, + "fio_opts": { + "rw": 'write', + "bs": 4096, + "number_ios": 4, + "verify": "crc32c", + "fdp": 1, + "fdp_pli": 1, + "fdp_pli_select": "roundrobin", + "output-format": "json", + }, + "test_class": FDPSinglePLIDTest, + }, + { + "test_id": 10, + "fio_opts": { + "rw": 'randwrite', + "bs": 4096, + "number_ios": 4, + "verify": "crc32c", + "dataplacement": "fdp", + "plids": 1, + "plid_select": "roundrobin", + "output-format": "json", + }, + "test_class": FDPSinglePLIDTest, + }, + ## fdp_pli_select/plid_select=random + { + "test_id": 11, + "fio_opts": { + "rw": 'write', + "bs": 4096, + "number_ios": 4, + "verify": "crc32c", + "fdp": 1, + "fdp_pli": 1, + "fdp_pli_select": "random", + "output-format": "json", + }, + "test_class": FDPSinglePLIDTest, + }, + { + "test_id": 12, + "fio_opts": { + "rw": 'randwrite', + "bs": 4096, + "number_ios": 4, + "verify": "crc32c", + "dataplacement": "fdp", + "plids": 1, + "plid_select": "random", + "output-format": "json", + }, + "test_class": FDPSinglePLIDTest, + }, + # Just a regular write without FDP directive--should land on plid 0 + { + "test_id": 13, + "fio_opts": { + "rw": 'randwrite', + "bs": 4096, + "number_ios": 19, + "verify": "crc32c", + "output-format": "json", + }, + "test_class": FDPSinglePLIDTest, + }, + # Read workload + { + "test_id": 14, + "fio_opts": { + "rw": 'randread', + "bs": 4096, + "number_ios": 19, + "output-format": "json", + }, + "test_class": FDPReadTest, + }, + # write to multiple PLIDs using round robin to select PLIDs + ## write to all PLIDs using old and new sets of options + { + "test_id": 100, + "fio_opts": { + "rw": 'randwrite', + "bs": 4096, + "number_ios": "2*{nruhsd}+3", + "verify": "crc32c", + "fdp": 1, + "fdp_pli_select": "roundrobin", + "output-format": "json", + }, + "test_class": FDPMultiplePLIDTest, + }, + { + "test_id": 101, + "fio_opts": { + "rw": 'randwrite', + "bs": 4096, + "number_ios": "2*{nruhsd}+3", + "verify": "crc32c", + "dataplacement": "fdp", + "plid_select": "roundrobin", + "output-format": "json", + }, + "test_class": FDPMultiplePLIDTest, + }, + ## write to a subset of PLIDs using old and new sets of options + { + "test_id": 102, + "fio_opts": { + "rw": 'randwrite', + "bs": 4096, + "number_ios": "{nruhsd}+1", + "verify": "crc32c", + "fdp": 1, + "fdp_pli": "1,3", + "fdp_pli_select": "roundrobin", + "output-format": "json", + }, + "test_class": FDPMultiplePLIDTest, + }, + { + "test_id": 103, + "fio_opts": { + "rw": 'randwrite', + "bs": 4096, + "number_ios": "{nruhsd}+1", + "verify": "crc32c", + "dataplacement": "fdp", + "plids": "1,3", + "plid_select": "roundrobin", + "output-format": "json", + }, + "test_class": FDPMultiplePLIDTest, + }, + # write to multiple PLIDs using random selection of PLIDs + ## write to all PLIDs using old and new sets of options + { + "test_id": 200, + "fio_opts": { + "rw": 'randwrite', + "bs": 4096, + "number_ios": "{max_ruamw}-1", + "verify": "crc32c", + "fdp": 1, + "fdp_pli_select": "random", + "output-format": "json", + }, + "test_class": FDPMultiplePLIDTest, + }, + { + "test_id": 201, + "fio_opts": { + "rw": 'randwrite', + "bs": 4096, + "number_ios": "{max_ruamw}-1", + "verify": "crc32c", + "dataplacement": "fdp", + "plid_select": "random", + "output-format": "json", + }, + "test_class": FDPMultiplePLIDTest, + }, + ## write to a subset of PLIDs using old and new sets of options + { + "test_id": 202, + "fio_opts": { + "rw": 'randwrite', + "bs": 4096, + "number_ios": "{max_ruamw}-1", + "verify": "crc32c", + "fdp": 1, + "fdp_pli": "1,3,4", + "fdp_pli_select": "random", + "output-format": "json", + }, + "test_class": FDPMultiplePLIDTest, + }, + { + "test_id": 203, + "fio_opts": { + "rw": 'randwrite', + "bs": 4096, + "number_ios": "{max_ruamw}-1", + "verify": "crc32c", + "dataplacement": "fdp", + "plids": "1,3,4", + "plid_select": "random", + "output-format": "json", + }, + "test_class": FDPMultiplePLIDTest, + }, + # Specify invalid options fdp=1 and dataplacement=none + { + "test_id": 300, + "fio_opts": { + "rw": 'write', + "bs": 4096, + "io_size": 4096, + "verify": "crc32c", + "fdp": 1, + "fdp_pli": 3, + "output-format": "normal", + "dataplacement": "none", + }, + "test_class": FDPTest, + "success": SUCCESS_NONZERO, + }, + # Specify invalid options fdp=1 and dataplacement=streams + { + "test_id": 301, + "fio_opts": { + "rw": 'write', + "bs": 4096, + "io_size": 4096, + "verify": "crc32c", + "fdp": 1, + "fdp_pli": 3, + "output-format": "normal", + "dataplacement": "streams", + }, + "test_class": FDPTest, + "success": SUCCESS_NONZERO, + }, +] + +def parse_args(): + """Parse command-line arguments.""" + + parser = argparse.ArgumentParser() + parser.add_argument('-d', '--debug', help='Enable debug messages', action='store_true') + parser.add_argument('-f', '--fio', help='path to file executable (e.g., ./fio)') + parser.add_argument('-a', '--artifact-root', help='artifact root directory') + parser.add_argument('-s', '--skip', nargs='+', type=int, + help='list of test(s) to skip') + parser.add_argument('-o', '--run-only', nargs='+', type=int, + help='list of test(s) to run, skipping all others') + parser.add_argument('--dut', help='target NVMe character device to test ' + '(e.g., /dev/ng0n1). WARNING: THIS IS A DESTRUCTIVE TEST', required=True) + args = parser.parse_args() + + return args + + +FIO_FDP_MAX_RUAMW = 0 +FIO_FDP_NUMBER_PLIDS = 0 + +def main(): + """Run tests using fio's io_uring_cmd ioengine to send NVMe pass through commands.""" + global FIO_FDP_MAX_RUAMW + global FIO_FDP_NUMBER_PLIDS + + args = parse_args() + + if args.debug: + logging.basicConfig(level=logging.DEBUG) + else: + logging.basicConfig(level=logging.INFO) + + artifact_root = args.artifact_root if args.artifact_root else \ + f"nvmept-fdp-test-{time.strftime('%Y%m%d-%H%M%S')}" + os.mkdir(artifact_root) + print(f"Artifact directory is {artifact_root}") + + if args.fio: + fio_path = str(Path(args.fio).absolute()) + else: + fio_path = 'fio' + print(f"fio path is {fio_path}") + + for test in TEST_LIST: + test['fio_opts']['filename'] = args.dut + + fdp_status = get_fdp_status(args.dut) + FIO_FDP_NUMBER_PLIDS = fdp_status['nruhsd'] + update_all_ruhs(args.dut) + FIO_FDP_MAX_RUAMW = check_all_ruhs(args.dut) + if not FIO_FDP_MAX_RUAMW: + sys.exit(-1) + + test_env = { + 'fio_path': fio_path, + 'fio_root': str(Path(__file__).absolute().parent.parent), + 'artifact_root': artifact_root, + 'basename': 'nvmept-fdp', + } + + _, failed, _ = run_fio_tests(TEST_LIST, test_env, args) + sys.exit(failed) + + +if __name__ == '__main__': + main() -- 2.25.1