3 # Copyright 2024 Samsung Electronics Co., Ltd All Rights Reserved
5 # For conditions of distribution and use, see the accompanying COPYING file.
10 # Test fio's io_uring_cmd ioengine with NVMe pass-through dataset management
11 # commands that trim multiple ranges.
14 # see python3 nvmept_trim.py --help
17 # python3 t/nvmept_trim.py --dut /dev/ng0n1
18 # python3 t/nvmept_trim.py --dut /dev/ng1n1 -f ./fio
29 from pathlib import Path
30 from fiotestlib import FioJobCmdTest, run_fio_tests
31 from fiotestcommon import SUCCESS_NONZERO
34 class TrimTest(FioJobCmdTest):
36 NVMe pass-through test class. Check to make sure output for selected data
37 direction(s) is non-zero and that zero data appears for other directions.
40 def setup(self, parameters):
45 "--ioengine=io_uring_cmd",
47 f"--filename={self.fio_opts['filename']}",
48 f"--rw={self.fio_opts['rw']}",
49 f"--output={self.filenames['output']}",
50 f"--output-format={self.fio_opts['output-format']}",
52 for opt in ['fixedbufs', 'nonvectored', 'force_async', 'registerfiles',
53 'sqthread_poll', 'sqthread_poll_cpu', 'hipri', 'nowait',
54 'time_based', 'runtime', 'verify', 'io_size', 'num_range',
55 'iodepth', 'iodepth_batch', 'iodepth_batch_complete',
56 'size', 'rate', 'bs', 'bssplit', 'bsrange', 'randrepeat',
57 'buffer_pattern', 'verify_pattern', 'verify', 'offset']:
58 if opt in self.fio_opts:
59 option = f"--{opt}={self.fio_opts[opt]}"
60 fio_args.append(option)
62 super().setup(fio_args)
65 def check_result(self):
67 super().check_result()
69 if 'rw' not in self.fio_opts or \
71 'json' not in self.fio_opts['output-format']:
74 job = self.json_data['jobs'][0]
76 if self.fio_opts['rw'] in ['read', 'randread']:
77 self.passed = self.check_all_ddirs(['read'], job)
78 elif self.fio_opts['rw'] in ['write', 'randwrite']:
79 if 'verify' not in self.fio_opts:
80 self.passed = self.check_all_ddirs(['write'], job)
82 self.passed = self.check_all_ddirs(['read', 'write'], job)
83 elif self.fio_opts['rw'] in ['trim', 'randtrim']:
84 self.passed = self.check_all_ddirs(['trim'], job)
85 elif self.fio_opts['rw'] in ['readwrite', 'randrw']:
86 self.passed = self.check_all_ddirs(['read', 'write'], job)
87 elif self.fio_opts['rw'] in ['trimwrite', 'randtrimwrite']:
88 self.passed = self.check_all_ddirs(['trim', 'write'], job)
90 logging.error("Unhandled rw value %s", self.fio_opts['rw'])
93 if 'iodepth' in self.fio_opts:
94 # We will need to figure something out if any test uses an iodepth
96 if job['iodepth_level']['8'] < 95:
97 logging.error("Did not achieve requested iodepth")
100 logging.debug("iodepth 8 target met %s", job['iodepth_level']['8'])
103 class RangeTrimTest(TrimTest):
105 Multi-range trim test class.
109 """Calculate block size and determine whether bs will be an average or exact."""
111 if 'bs' in self.fio_opts:
113 bs = self.fio_opts['bs']
114 elif 'bssplit' in self.fio_opts:
118 for split in self.fio_opts['bssplit'].split(':'):
119 [blocksize, share] = split.split('/')
121 bs += int(blocksize) * int(share) / 100
123 logging.error("bssplit '%s' total percentage is not 100", self.fio_opts['bssplit'])
126 logging.debug("bssplit: average block size is %d", int(bs))
127 # The only check we do here for bssplit is to calculate an average
128 # blocksize and see if the IOPS and bw are consistent
129 elif 'bsrange' in self.fio_opts:
131 [minbs, maxbs] = self.fio_opts['bsrange'].split('-')
134 bs = int((minbs + maxbs) / 2)
135 logging.debug("bsrange: average block size is %d", int(bs))
136 # The only check we do here for bsrange is to calculate an average
137 # blocksize and see if the IOPS and bw are consistent
142 return bs, exact_size
145 def check_result(self):
147 Make sure that the number of IO requests is consistent with the
148 blocksize and num_range values. In other words, if the blocksize is
149 4KiB and num_range is 2, we should have 128 IO requests to trim 1MiB.
151 # TODO Enable debug output to check the actual offsets
153 super().check_result()
155 if not self.passed or 'json' not in self.fio_opts['output-format']:
158 job = self.json_data['jobs'][0]['trim']
159 bs, exact_size = self.get_bs()
161 # make sure bw and IOPS are consistent
164 runtime = job['runtime']
166 calculated = int(bw*runtime/1000)
167 expected = job['io_bytes']
168 if abs(calculated - expected) / expected > 0.05:
169 logging.error("Total bytes %d from bw does not match reported total bytes %d",
170 calculated, expected)
173 logging.debug("Total bytes %d from bw matches reported total bytes %d", calculated,
176 calculated = int(iops*runtime/1000*bs*self.fio_opts['num_range'])
177 if abs(calculated - expected) / expected > 0.05:
178 logging.error("Total bytes %d from IOPS does not match reported total bytes %d",
179 calculated, expected)
182 logging.debug("Total bytes %d from IOPS matches reported total bytes %d", calculated,
185 if 'size' in self.fio_opts:
186 io_count = self.fio_opts['size'] / self.fio_opts['num_range'] / bs
190 delta = 0.05*job['total_ios']
192 if abs(job['total_ios'] - io_count) > delta:
193 logging.error("Expected numbers of IOs %d does not match actual value %d",
194 io_count, job['total_ios'])
197 logging.debug("Expected numbers of IOs %d matches actual value %d", io_count,
200 if 'rate' in self.fio_opts:
201 if abs(bw - self.fio_opts['rate']) / self.fio_opts['rate'] > 0.05:
202 logging.error("Actual rate %f does not match expected rate %f", bw,
203 self.fio_opts['rate'])
206 logging.debug("Actual rate %f matches expeected rate %f", bw, self.fio_opts['rate'])
211 # The group of tests below checks existing use cases to make sure there are
219 "output-format": "json",
221 "test_class": TrimTest,
229 "output-format": "json",
231 "test_class": TrimTest,
241 "iodepth_batch_complete": 4,
242 "output-format": "json",
244 "test_class": TrimTest,
254 "iodepth_batch_complete": 4,
255 "output-format": "json",
257 "test_class": TrimTest,
265 "output-format": "json",
267 "test_class": TrimTest,
272 "rw": 'randtrimwrite',
275 "output-format": "json",
277 "test_class": TrimTest,
291 "output-format": "json",
293 "test_class": TrimTest,
295 # The group of tests below try out the new functionality
301 "size": 16*1024*1024,
302 "output-format": "json",
304 "test_class": RangeTrimTest,
311 "size": 16*1024*1024,
312 "output-format": "json",
314 "test_class": RangeTrimTest,
321 "size": 64*1024*1024,
322 "output-format": "json",
324 "test_class": RangeTrimTest,
332 "size": 32*1024*1024,
333 "output-format": "json",
335 "test_class": RangeTrimTest,
343 "size": 32*1024*1024,
344 "output-format": "json",
346 "test_class": RangeTrimTest,
353 "bssplit": "4096/50:16384/50",
354 "size": 80*1024*1024,
355 "output-format": "json",
358 "test_class": RangeTrimTest,
365 "bssplit": "4096/25:8192/25:12288/25:16384/25",
366 "size": 80*1024*1024,
367 "output-format": "json",
370 "test_class": RangeTrimTest,
377 "bssplit": "4096/20:8192/20:12288/20:16384/20:20480/20",
378 "size": 72*1024*1024,
379 "output-format": "json",
382 "test_class": RangeTrimTest,
389 "bsrange": "4096-16384",
390 "size": 80*1024*1024,
391 "output-format": "json",
394 "test_class": RangeTrimTest,
401 "bsrange": "4096-20480",
402 "size": 72*1024*1024,
403 "output-format": "json",
406 "test_class": RangeTrimTest,
416 "output-format": "json",
418 "test_class": RangeTrimTest,
420 # All of the tests below should fail
421 # TODO check the error messages resulting from the jobs below
425 "rw": 'randtrimwrite',
430 "output-format": "normal",
432 "test_class": RangeTrimTest,
433 "success": SUCCESS_NONZERO,
443 "output-format": "normal",
445 "test_class": RangeTrimTest,
446 "success": SUCCESS_NONZERO,
455 "output-format": "normal",
457 "test_class": RangeTrimTest,
458 "success": SUCCESS_NONZERO,
460 # The sequence of jobs below constitute a single test with multiple steps
461 # - write a data pattern
462 # - verify the data pattern
463 # - trim the first half of the LBA space
464 # - verify that the trim'd LBA space no longer returns the original data pattern
465 # - verify that the remaining LBA space has the expected pattern
470 "output-format": 'json',
471 "buffer_pattern": 0x0f,
472 "size": 256*1024*1024,
474 "test_class": TrimTest,
480 "output-format": 'json',
481 "verify_pattern": 0x0f,
483 "size": 256*1024*1024,
485 "test_class": TrimTest,
492 "output-format": 'json',
493 "size": 128*1024*1024,
495 "test_class": TrimTest,
497 # The identify namespace data structure has a DLFEAT field which specifies
498 # what happens when reading data from deallocated blocks. There are three
500 # - read behavior not reported
501 # - deallocated logical block returns all bytes 0x0
502 # - deallocated logical block returns all bytes 0xff
503 # The test below merely checks that the original data pattern is not returned.
504 # Source: Figure 97 from
505 # https://nvmexpress.org/wp-content/uploads/NVM-Express-NVM-Command-Set-Specification-1.0c-2022.10.03-Ratified.pdf
510 "output-format": 'json',
511 "verify_pattern": 0x0f,
513 "size": 128*1024*1024,
515 "test_class": TrimTest,
516 "success": SUCCESS_NONZERO,
522 "output-format": 'json',
523 "verify_pattern": 0x0f,
525 "offset": 128*1024*1024,
526 "size": 128*1024*1024,
528 "test_class": TrimTest,
533 """Parse command-line arguments."""
535 parser = argparse.ArgumentParser()
536 parser.add_argument('-d', '--debug', help='Enable debug messages', action='store_true')
537 parser.add_argument('-f', '--fio', help='path to file executable (e.g., ./fio)')
538 parser.add_argument('-a', '--artifact-root', help='artifact root directory')
539 parser.add_argument('-s', '--skip', nargs='+', type=int,
540 help='list of test(s) to skip')
541 parser.add_argument('-o', '--run-only', nargs='+', type=int,
542 help='list of test(s) to run, skipping all others')
543 parser.add_argument('--dut', help='target NVMe character device to test '
544 '(e.g., /dev/ng0n1). WARNING: THIS IS A DESTRUCTIVE TEST', required=True)
545 args = parser.parse_args()
551 """Run tests using fio's io_uring_cmd ioengine to send NVMe pass through commands."""
556 logging.basicConfig(level=logging.DEBUG)
558 logging.basicConfig(level=logging.INFO)
560 artifact_root = args.artifact_root if args.artifact_root else \
561 f"nvmept-trim-test-{time.strftime('%Y%m%d-%H%M%S')}"
562 os.mkdir(artifact_root)
563 print(f"Artifact directory is {artifact_root}")
566 fio_path = str(Path(args.fio).absolute())
569 print(f"fio path is {fio_path}")
571 for test in TEST_LIST:
572 test['fio_opts']['filename'] = args.dut
575 'fio_path': fio_path,
576 'fio_root': str(Path(__file__).absolute().parent.parent),
577 'artifact_root': artifact_root,
578 'basename': 'nvmept-trim',
581 _, failed, _ = run_fio_tests(TEST_LIST, test_env, args)
585 if __name__ == '__main__':