--- /dev/null
+#!/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_trim.py
+#
+# Test fio's io_uring_cmd ioengine with NVMe pass-through dataset management
+# commands that trim multiple ranges.
+#
+# USAGE
+# see python3 nvmept_trim.py --help
+#
+# EXAMPLES
+# python3 t/nvmept_trim.py --dut /dev/ng0n1
+# python3 t/nvmept_trim.py --dut /dev/ng1n1 -f ./fio
+#
+# REQUIREMENTS
+# Python 3.6
+#
+"""
+import os
+import sys
+import time
+import logging
+import argparse
+from pathlib import Path
+from fiotestlib import FioJobCmdTest, run_fio_tests
+from fiotestcommon import SUCCESS_NONZERO
+
+
+class TrimTest(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-trim",
+ "--ioengine=io_uring_cmd",
+ "--cmd_type=nvme",
+ 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', 'verify', 'offset']:
+ 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):
+
+ 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 RangeTrimTest(TrimTest):
+ """
+ Multi-range trim test class.
+ """
+
+ def get_bs(self):
+ """Calculate block size and determine whether bs will be an average or exact."""
+
+ if 'bs' in self.fio_opts:
+ exact_size = True
+ bs = self.fio_opts['bs']
+ elif 'bssplit' in self.fio_opts:
+ exact_size = False
+ bs = 0
+ total = 0
+ for split in self.fio_opts['bssplit'].split(':'):
+ [blocksize, share] = split.split('/')
+ total += int(share)
+ bs += int(blocksize) * int(share) / 100
+ if total != 100:
+ logging.error("bssplit '%s' total percentage is not 100", self.fio_opts['bssplit'])
+ self.passed = False
+ else:
+ logging.debug("bssplit: average block size is %d", int(bs))
+ # The only check we do here for bssplit is to calculate an average
+ # blocksize and see if the IOPS and bw are consistent
+ elif 'bsrange' in self.fio_opts:
+ exact_size = False
+ [minbs, maxbs] = self.fio_opts['bsrange'].split('-')
+ minbs = int(minbs)
+ maxbs = int(maxbs)
+ bs = int((minbs + maxbs) / 2)
+ logging.debug("bsrange: average block size is %d", int(bs))
+ # The only check we do here for bsrange is to calculate an average
+ # blocksize and see if the IOPS and bw are consistent
+ else:
+ exact_size = True
+ bs = 4096
+
+ return bs, exact_size
+
+
+ def check_result(self):
+ """
+ Make sure that the number of IO requests is consistent with the
+ blocksize and num_range values. In other words, if the blocksize is
+ 4KiB and num_range is 2, we should have 128 IO requests to trim 1MiB.
+ """
+ # TODO Enable debug output to check the actual offsets
+
+ super().check_result()
+
+ if not self.passed or 'json' not in self.fio_opts['output-format']:
+ return
+
+ job = self.json_data['jobs'][0]['trim']
+ bs, exact_size = self.get_bs()
+
+ # make sure bw and IOPS are consistent
+ bw = job['bw_bytes']
+ iops = job['iops']
+ runtime = job['runtime']
+
+ calculated = int(bw*runtime/1000)
+ expected = job['io_bytes']
+ if abs(calculated - expected) / expected > 0.05:
+ logging.error("Total bytes %d from bw does not match reported total bytes %d",
+ calculated, expected)
+ self.passed = False
+ else:
+ logging.debug("Total bytes %d from bw matches reported total bytes %d", calculated,
+ expected)
+
+ calculated = int(iops*runtime/1000*bs*self.fio_opts['num_range'])
+ if abs(calculated - expected) / expected > 0.05:
+ logging.error("Total bytes %d from IOPS does not match reported total bytes %d",
+ calculated, expected)
+ self.passed = False
+ else:
+ logging.debug("Total bytes %d from IOPS matches reported total bytes %d", calculated,
+ expected)
+
+ if 'size' in self.fio_opts:
+ io_count = self.fio_opts['size'] / self.fio_opts['num_range'] / bs
+ if exact_size:
+ delta = 0.1
+ else:
+ delta = 0.05*job['total_ios']
+
+ if abs(job['total_ios'] - io_count) > delta:
+ logging.error("Expected numbers of IOs %d does not match actual value %d",
+ io_count, job['total_ios'])
+ self.passed = False
+ else:
+ logging.debug("Expected numbers of IOs %d matches actual value %d", io_count,
+ job['total_ios'])
+
+ if 'rate' in self.fio_opts:
+ if abs(bw - self.fio_opts['rate']) / self.fio_opts['rate'] > 0.05:
+ logging.error("Actual rate %f does not match expected rate %f", bw,
+ self.fio_opts['rate'])
+ self.passed = False
+ else:
+ logging.debug("Actual rate %f matches expeected rate %f", bw, self.fio_opts['rate'])
+
+
+
+TEST_LIST = [
+ # The group of tests below checks existing use cases to make sure there are
+ # no regressions.
+ {
+ "test_id": 1,
+ "fio_opts": {
+ "rw": 'trim',
+ "time_based": 1,
+ "runtime": 3,
+ "output-format": "json",
+ },
+ "test_class": TrimTest,
+ },
+ {
+ "test_id": 2,
+ "fio_opts": {
+ "rw": 'randtrim',
+ "time_based": 1,
+ "runtime": 3,
+ "output-format": "json",
+ },
+ "test_class": TrimTest,
+ },
+ {
+ "test_id": 3,
+ "fio_opts": {
+ "rw": 'trim',
+ "time_based": 1,
+ "runtime": 3,
+ "iodepth": 8,
+ "iodepth_batch": 4,
+ "iodepth_batch_complete": 4,
+ "output-format": "json",
+ },
+ "test_class": TrimTest,
+ },
+ {
+ "test_id": 4,
+ "fio_opts": {
+ "rw": 'randtrim',
+ "time_based": 1,
+ "runtime": 3,
+ "iodepth": 8,
+ "iodepth_batch": 4,
+ "iodepth_batch_complete": 4,
+ "output-format": "json",
+ },
+ "test_class": TrimTest,
+ },
+ {
+ "test_id": 5,
+ "fio_opts": {
+ "rw": 'trimwrite',
+ "time_based": 1,
+ "runtime": 3,
+ "output-format": "json",
+ },
+ "test_class": TrimTest,
+ },
+ {
+ "test_id": 6,
+ "fio_opts": {
+ "rw": 'randtrimwrite',
+ "time_based": 1,
+ "runtime": 3,
+ "output-format": "json",
+ },
+ "test_class": TrimTest,
+ },
+ {
+ "test_id": 7,
+ "fio_opts": {
+ "rw": 'randtrim',
+ "time_based": 1,
+ "runtime": 3,
+ "fixedbufs": 0,
+ "nonvectored": 1,
+ "force_async": 1,
+ "registerfiles": 1,
+ "sqthread_poll": 1,
+ "fixedbuffs": 1,
+ "output-format": "json",
+ },
+ "test_class": TrimTest,
+ },
+ # The group of tests below try out the new functionality
+ {
+ "test_id": 100,
+ "fio_opts": {
+ "rw": 'trim',
+ "num_range": 2,
+ "size": 16*1024*1024,
+ "output-format": "json",
+ },
+ "test_class": RangeTrimTest,
+ },
+ {
+ "test_id": 101,
+ "fio_opts": {
+ "rw": 'randtrim',
+ "num_range": 2,
+ "size": 16*1024*1024,
+ "output-format": "json",
+ },
+ "test_class": RangeTrimTest,
+ },
+ {
+ "test_id": 102,
+ "fio_opts": {
+ "rw": 'randtrim',
+ "num_range": 256,
+ "size": 64*1024*1024,
+ "output-format": "json",
+ },
+ "test_class": RangeTrimTest,
+ },
+ {
+ "test_id": 103,
+ "fio_opts": {
+ "rw": 'trim',
+ "num_range": 2,
+ "bs": 16*1024,
+ "size": 32*1024*1024,
+ "output-format": "json",
+ },
+ "test_class": RangeTrimTest,
+ },
+ {
+ "test_id": 104,
+ "fio_opts": {
+ "rw": 'randtrim',
+ "num_range": 2,
+ "bs": 16*1024,
+ "size": 32*1024*1024,
+ "output-format": "json",
+ },
+ "test_class": RangeTrimTest,
+ },
+ {
+ "test_id": 105,
+ "fio_opts": {
+ "rw": 'randtrim',
+ "num_range": 2,
+ "bssplit": "4096/50:16384/50",
+ "size": 80*1024*1024,
+ "output-format": "json",
+ "randrepeat": 0,
+ },
+ "test_class": RangeTrimTest,
+ },
+ {
+ "test_id": 106,
+ "fio_opts": {
+ "rw": 'randtrim',
+ "num_range": 4,
+ "bssplit": "4096/25:8192/25:12288/25:16384/25",
+ "size": 80*1024*1024,
+ "output-format": "json",
+ "randrepeat": 0,
+ },
+ "test_class": RangeTrimTest,
+ },
+ {
+ "test_id": 107,
+ "fio_opts": {
+ "rw": 'randtrim',
+ "num_range": 4,
+ "bssplit": "4096/20:8192/20:12288/20:16384/20:20480/20",
+ "size": 72*1024*1024,
+ "output-format": "json",
+ "randrepeat": 0,
+ },
+ "test_class": RangeTrimTest,
+ },
+ {
+ "test_id": 108,
+ "fio_opts": {
+ "rw": 'randtrim',
+ "num_range": 2,
+ "bsrange": "4096-16384",
+ "size": 80*1024*1024,
+ "output-format": "json",
+ "randrepeat": 0,
+ },
+ "test_class": RangeTrimTest,
+ },
+ {
+ "test_id": 109,
+ "fio_opts": {
+ "rw": 'randtrim',
+ "num_range": 4,
+ "bsrange": "4096-20480",
+ "size": 72*1024*1024,
+ "output-format": "json",
+ "randrepeat": 0,
+ },
+ "test_class": RangeTrimTest,
+ },
+ {
+ "test_id": 110,
+ "fio_opts": {
+ "rw": 'randtrim',
+ "time_based": 1,
+ "runtime": 10,
+ "rate": 1024*1024,
+ "num_range": 2,
+ "output-format": "json",
+ },
+ "test_class": RangeTrimTest,
+ },
+ # All of the tests below should fail
+ # TODO check the error messages resulting from the jobs below
+ {
+ "test_id": 200,
+ "fio_opts": {
+ "rw": 'randtrimwrite',
+ "time_based": 1,
+ "runtime": 10,
+ "rate": 1024*1024,
+ "num_range": 2,
+ "output-format": "normal",
+ },
+ "test_class": RangeTrimTest,
+ "success": SUCCESS_NONZERO,
+ },
+ {
+ "test_id": 201,
+ "fio_opts": {
+ "rw": 'trimwrite',
+ "time_based": 1,
+ "runtime": 10,
+ "rate": 1024*1024,
+ "num_range": 2,
+ "output-format": "normal",
+ },
+ "test_class": RangeTrimTest,
+ "success": SUCCESS_NONZERO,
+ },
+ {
+ "test_id": 202,
+ "fio_opts": {
+ "rw": 'trim',
+ "time_based": 1,
+ "runtime": 10,
+ "num_range": 257,
+ "output-format": "normal",
+ },
+ "test_class": RangeTrimTest,
+ "success": SUCCESS_NONZERO,
+ },
+ # The sequence of jobs below constitute a single test with multiple steps
+ # - write a data pattern
+ # - verify the data pattern
+ # - trim the first half of the LBA space
+ # - verify that the trim'd LBA space no longer returns the original data pattern
+ # - verify that the remaining LBA space has the expected pattern
+ {
+ "test_id": 300,
+ "fio_opts": {
+ "rw": 'write',
+ "output-format": 'json',
+ "buffer_pattern": 0x0f,
+ "size": 256*1024*1024,
+ },
+ "test_class": TrimTest,
+ },
+ {
+ "test_id": 301,
+ "fio_opts": {
+ "rw": 'read',
+ "output-format": 'json',
+ "verify_pattern": 0x0f,
+ "verify": "pattern",
+ "size": 256*1024*1024,
+ },
+ "test_class": TrimTest,
+ },
+ {
+ "test_id": 302,
+ "fio_opts": {
+ "rw": 'randtrim',
+ "num_range": 8,
+ "output-format": 'json',
+ "size": 128*1024*1024,
+ },
+ "test_class": TrimTest,
+ },
+ # The identify namespace data structure has a DLFEAT field which specifies
+ # what happens when reading data from deallocated blocks. There are three
+ # options:
+ # - read behavior not reported
+ # - deallocated logical block returns all bytes 0x0
+ # - deallocated logical block returns all bytes 0xff
+ # The test below merely checks that the original data pattern is not returned.
+ # Source: Figure 97 from
+ # https://nvmexpress.org/wp-content/uploads/NVM-Express-NVM-Command-Set-Specification-1.0c-2022.10.03-Ratified.pdf
+ {
+ "test_id": 303,
+ "fio_opts": {
+ "rw": 'read',
+ "output-format": 'json',
+ "verify_pattern": 0x0f,
+ "verify": "pattern",
+ "size": 128*1024*1024,
+ },
+ "test_class": TrimTest,
+ "success": SUCCESS_NONZERO,
+ },
+ {
+ "test_id": 304,
+ "fio_opts": {
+ "rw": 'read',
+ "output-format": 'json',
+ "verify_pattern": 0x0f,
+ "verify": "pattern",
+ "offset": 128*1024*1024,
+ "size": 128*1024*1024,
+ },
+ "test_class": TrimTest,
+ },
+]
+
+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
+
+
+def main():
+ """Run tests using fio's io_uring_cmd ioengine to send NVMe pass through commands."""
+
+ 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-trim-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
+
+ test_env = {
+ 'fio_path': fio_path,
+ 'fio_root': str(Path(__file__).absolute().parent.parent),
+ 'artifact_root': artifact_root,
+ 'basename': 'nvmept-trim',
+ }
+
+ _, failed, _ = run_fio_tests(TEST_LIST, test_env, args)
+ sys.exit(failed)
+
+
+if __name__ == '__main__':
+ main()