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 FDP write commands.
13 # see python3 nvmept_fdp.py --help
16 # python3 t/nvmept_fdp.py --dut /dev/ng0n1
17 # python3 t/nvmept_fdp.py --dut /dev/ng1n1 -f ./fio
21 # Device formatted with LBA data size 4096 bytes
22 # Device with at least five placement IDs
25 # This is a destructive test
35 from pathlib import Path
36 from fiotestlib import FioJobCmdTest, run_fio_tests
37 from fiotestcommon import SUCCESS_NONZERO
40 class FDPTest(FioJobCmdTest):
42 NVMe pass-through test class. Check to make sure output for selected data
43 direction(s) is non-zero and that zero data appears for other directions.
46 def setup(self, parameters):
51 "--ioengine=io_uring_cmd",
54 f"--filename={self.fio_opts['filename']}",
55 f"--rw={self.fio_opts['rw']}",
56 f"--output={self.filenames['output']}",
57 f"--output-format={self.fio_opts['output-format']}",
59 for opt in ['fixedbufs', 'nonvectored', 'force_async', 'registerfiles',
60 'sqthread_poll', 'sqthread_poll_cpu', 'hipri', 'nowait',
61 'time_based', 'runtime', 'verify', 'io_size', 'num_range',
62 'iodepth', 'iodepth_batch', 'iodepth_batch_complete',
63 'size', 'rate', 'bs', 'bssplit', 'bsrange', 'randrepeat',
64 'buffer_pattern', 'verify_pattern', 'offset', 'fdp',
65 'fdp_pli', 'fdp_pli_select', 'dataplacement', 'plid_select',
66 'plids', 'number_ios']:
67 if opt in self.fio_opts:
68 option = f"--{opt}={self.fio_opts[opt]}"
69 fio_args.append(option)
71 super().setup(fio_args)
74 def check_result(self):
78 if not update_all_ruhs(self.fio_opts['filename']):
79 logging.error("Could not reset device")
80 if not check_all_ruhs(self.fio_opts['filename']):
81 logging.error("Reclaim units have inconsistent RUAMW values")
84 def _check_result(self):
86 super().check_result()
88 if 'rw' not in self.fio_opts or \
90 'json' not in self.fio_opts['output-format']:
93 job = self.json_data['jobs'][0]
95 if self.fio_opts['rw'] in ['read', 'randread']:
96 self.passed = self.check_all_ddirs(['read'], job)
97 elif self.fio_opts['rw'] in ['write', 'randwrite']:
98 if 'verify' not in self.fio_opts:
99 self.passed = self.check_all_ddirs(['write'], job)
101 self.passed = self.check_all_ddirs(['read', 'write'], job)
102 elif self.fio_opts['rw'] in ['trim', 'randtrim']:
103 self.passed = self.check_all_ddirs(['trim'], job)
104 elif self.fio_opts['rw'] in ['readwrite', 'randrw']:
105 self.passed = self.check_all_ddirs(['read', 'write'], job)
106 elif self.fio_opts['rw'] in ['trimwrite', 'randtrimwrite']:
107 self.passed = self.check_all_ddirs(['trim', 'write'], job)
109 logging.error("Unhandled rw value %s", self.fio_opts['rw'])
112 if 'iodepth' in self.fio_opts:
113 # We will need to figure something out if any test uses an iodepth
115 if job['iodepth_level']['8'] < 95:
116 logging.error("Did not achieve requested iodepth")
119 logging.debug("iodepth 8 target met %s", job['iodepth_level']['8'])
122 class FDPMultiplePLIDTest(FDPTest):
124 Write to multiple placement IDs.
127 def setup(self, parameters):
129 'nruhsd': FIO_FDP_NUMBER_PLIDS,
130 'max_ruamw': FIO_FDP_MAX_RUAMW,
132 if 'number_ios' in self.fio_opts and isinstance(self.fio_opts['number_ios'], str):
133 self.fio_opts['number_ios'] = eval(self.fio_opts['number_ios'].format(**mapping))
135 super().setup(parameters)
137 def _check_result(self):
138 if 'fdp_pli' in self.fio_opts:
139 plid_list = self.fio_opts['fdp_pli'].split(',')
140 elif 'plids' in self.fio_opts:
141 plid_list = self.fio_opts['plids'].split(',')
143 plid_list = list(range(FIO_FDP_NUMBER_PLIDS))
145 plid_list = sorted([int(i) for i in plid_list])
146 logging.debug("plid_list: %s", str(plid_list))
148 fdp_status = get_fdp_status(self.fio_opts['filename'])
150 select = "roundrobin"
151 if 'fdp_pli_select' in self.fio_opts:
152 select = self.fio_opts['fdp_pli_select']
153 elif 'plid_select' in self.fio_opts:
154 select = self.fio_opts['plid_select']
156 if select == "roundrobin":
157 self._check_robin(plid_list, fdp_status)
158 elif select == "random":
159 self._check_random(plid_list, fdp_status)
161 logging.error("Unknown plid selection strategy %s", select)
164 super()._check_result()
166 def _check_robin(self, plid_list, fdp_status):
168 With round robin we can know exactly how many writes each PLID will
171 ruamw = [FIO_FDP_MAX_RUAMW] * FIO_FDP_NUMBER_PLIDS
173 remainder = int(self.fio_opts['number_ios'] % len(plid_list))
174 whole = int((self.fio_opts['number_ios'] - remainder) / len(plid_list))
175 logging.debug("PLIDs in the list should receive %d writes; %d PLIDs will receive one extra",
178 for plid in plid_list:
183 logging.debug("Expected ruamw values: %s", str(ruamw))
185 for idx, ruhs in enumerate(fdp_status['ruhss']):
186 if ruhs['ruamw'] != ruamw[idx]:
187 logging.error("RUAMW mismatch with idx %d, pid %d, expected %d, observed %d", idx,
188 ruhs['pid'], ruamw[idx], ruhs['ruamw'])
192 logging.debug("RUAMW match with idx %d, pid %d: ruamw=%d", idx, ruhs['pid'], ruamw[idx])
194 def _check_random(self, plid_list, fdp_status):
196 With random selection, a set of PLIDs will receive all the write
197 operations and the remainder will be untouched.
201 for plid in plid_list:
202 total_ruamw += fdp_status['ruhss'][plid]['ruamw']
204 expected = len(plid_list) * FIO_FDP_MAX_RUAMW - self.fio_opts['number_ios']
205 if total_ruamw != expected:
206 logging.error("Expected total ruamw %d for plids %s, observed %d", expected,
207 str(plid_list), total_ruamw)
210 logging.debug("Observed expected total ruamw %d for plids %s", expected, str(plid_list))
212 for idx, ruhs in enumerate(fdp_status['ruhss']):
215 if ruhs['ruamw'] != FIO_FDP_MAX_RUAMW:
216 logging.error("Unexpected ruamw %d for idx %d, pid %d, expected %d", ruhs['ruamw'],
217 idx, ruhs['pid'], FIO_FDP_MAX_RUAMW)
220 logging.debug("Observed expected ruamw %d for idx %d, pid %d", ruhs['ruamw'], idx,
224 class FDPSinglePLIDTest(FDPTest):
226 Write to a single placement ID only.
229 def _check_result(self):
230 if 'plids' in self.fio_opts:
231 plid = self.fio_opts['plids']
232 elif 'fdp_pli' in self.fio_opts:
233 plid = self.fio_opts['fdp_pli']
237 fdp_status = get_fdp_status(self.fio_opts['filename'])
238 ruamw = fdp_status['ruhss'][plid]['ruamw']
239 lba_count = self.fio_opts['number_ios']
241 if FIO_FDP_MAX_RUAMW - lba_count != ruamw:
242 logging.error("FDP accounting mismatch for plid %d; expected ruamw %d, observed %d",
243 plid, FIO_FDP_MAX_RUAMW - lba_count, ruamw)
246 logging.debug("FDP accounting as expected for plid %d; ruamw = %d", plid, ruamw)
248 super()._check_result()
251 class FDPReadTest(FDPTest):
256 def _check_result(self):
257 ruamw = check_all_ruhs(self.fio_opts['filename'])
259 if ruamw != FIO_FDP_MAX_RUAMW:
260 logging.error("Read workload affected FDP ruamw")
263 logging.debug("Read workload did not disturb FDP ruamw")
264 super()._check_result()
267 def get_fdp_status(dut):
269 Run the nvme-cli command to obtain FDP status and return result as a JSON
273 cmd = f"sudo nvme fdp status --output-format=json {dut}"
275 cmd_result = subprocess.run(cmd, capture_output=True, check=False,
276 encoding=locale.getpreferredencoding())
278 if cmd_result.returncode != 0:
279 logging.error("Error obtaining device %s FDP status: %s", dut, cmd_result.stderr)
282 return json.loads(cmd_result.stdout)
285 def update_ruh(dut, plid):
287 Update reclaim unit handles with specified ID(s). This tells the device to
288 point the RUH to a new (empty) reclaim unit.
291 ids = ','.join(plid) if isinstance(plid, list) else plid
292 cmd = f"nvme fdp update --pids={ids} {dut}"
294 cmd_result = subprocess.run(cmd, capture_output=True, check=False,
295 encoding=locale.getpreferredencoding())
297 if cmd_result.returncode != 0:
298 logging.error("Error updating RUH %s ID(s) %s", dut, ids)
304 def update_all_ruhs(dut):
306 Update all reclaim unit handles on the device.
309 fdp_status = get_fdp_status(dut)
310 for ruhs in fdp_status['ruhss']:
311 if not update_ruh(dut, ruhs['pid']):
317 def check_all_ruhs(dut):
319 Check that all RUHs have the same value for reclaim unit available media
320 writes (RUAMW). Return the RUAMW value.
323 fdp_status = get_fdp_status(dut)
324 ruh_status = fdp_status['ruhss']
326 ruamw = ruh_status[0]['ruamw']
327 for ruhs in ruh_status:
328 if ruhs['ruamw'] != ruamw:
329 logging.error("RUAMW mismatch: found %d, expected %d", ruhs['ruamw'], ruamw)
336 # Write one LBA to one PLID using both the old and new sets of options
337 ## omit fdp_pli_select/plid_select
347 "output-format": "json",
349 "test_class": FDPSinglePLIDTest,
358 "dataplacement": "fdp",
360 "output-format": "json",
362 "test_class": FDPSinglePLIDTest,
364 ## fdp_pli_select/plid_select=roundrobin
374 "fdp_pli_select": "roundrobin",
375 "output-format": "json",
377 "test_class": FDPSinglePLIDTest,
386 "dataplacement": "fdp",
388 "plid_select": "roundrobin",
389 "output-format": "json",
391 "test_class": FDPSinglePLIDTest,
393 ## fdp_pli_select/plid_select=random
403 "fdp_pli_select": "random",
404 "output-format": "json",
406 "test_class": FDPSinglePLIDTest,
415 "dataplacement": "fdp",
417 "plid_select": "random",
418 "output-format": "json",
420 "test_class": FDPSinglePLIDTest,
422 # Write four LBAs to one PLID using both the old and new sets of options
423 ## omit fdp_pli_select/plid_select
433 "output-format": "json",
435 "test_class": FDPSinglePLIDTest,
444 "dataplacement": "fdp",
446 "output-format": "json",
448 "test_class": FDPSinglePLIDTest,
450 ## fdp_pli_select/plid_select=roundrobin
460 "fdp_pli_select": "roundrobin",
461 "output-format": "json",
463 "test_class": FDPSinglePLIDTest,
472 "dataplacement": "fdp",
474 "plid_select": "roundrobin",
475 "output-format": "json",
477 "test_class": FDPSinglePLIDTest,
479 ## fdp_pli_select/plid_select=random
489 "fdp_pli_select": "random",
490 "output-format": "json",
492 "test_class": FDPSinglePLIDTest,
501 "dataplacement": "fdp",
503 "plid_select": "random",
504 "output-format": "json",
506 "test_class": FDPSinglePLIDTest,
508 # Just a regular write without FDP directive--should land on plid 0
516 "output-format": "json",
518 "test_class": FDPSinglePLIDTest,
527 "output-format": "json",
529 "test_class": FDPReadTest,
531 # write to multiple PLIDs using round robin to select PLIDs
532 ## write to all PLIDs using old and new sets of options
538 "number_ios": "2*{nruhsd}+3",
541 "fdp_pli_select": "roundrobin",
542 "output-format": "json",
544 "test_class": FDPMultiplePLIDTest,
551 "number_ios": "2*{nruhsd}+3",
553 "dataplacement": "fdp",
554 "plid_select": "roundrobin",
555 "output-format": "json",
557 "test_class": FDPMultiplePLIDTest,
559 ## write to a subset of PLIDs using old and new sets of options
565 "number_ios": "{nruhsd}+1",
569 "fdp_pli_select": "roundrobin",
570 "output-format": "json",
572 "test_class": FDPMultiplePLIDTest,
579 "number_ios": "{nruhsd}+1",
581 "dataplacement": "fdp",
583 "plid_select": "roundrobin",
584 "output-format": "json",
586 "test_class": FDPMultiplePLIDTest,
588 # write to multiple PLIDs using random selection of PLIDs
589 ## write to all PLIDs using old and new sets of options
595 "number_ios": "{max_ruamw}-1",
598 "fdp_pli_select": "random",
599 "output-format": "json",
601 "test_class": FDPMultiplePLIDTest,
608 "number_ios": "{max_ruamw}-1",
610 "dataplacement": "fdp",
611 "plid_select": "random",
612 "output-format": "json",
614 "test_class": FDPMultiplePLIDTest,
616 ## write to a subset of PLIDs using old and new sets of options
622 "number_ios": "{max_ruamw}-1",
626 "fdp_pli_select": "random",
627 "output-format": "json",
629 "test_class": FDPMultiplePLIDTest,
636 "number_ios": "{max_ruamw}-1",
638 "dataplacement": "fdp",
640 "plid_select": "random",
641 "output-format": "json",
643 "test_class": FDPMultiplePLIDTest,
645 # Specify invalid options fdp=1 and dataplacement=none
655 "output-format": "normal",
656 "dataplacement": "none",
658 "test_class": FDPTest,
659 "success": SUCCESS_NONZERO,
661 # Specify invalid options fdp=1 and dataplacement=streams
671 "output-format": "normal",
672 "dataplacement": "streams",
674 "test_class": FDPTest,
675 "success": SUCCESS_NONZERO,
680 """Parse command-line arguments."""
682 parser = argparse.ArgumentParser()
683 parser.add_argument('-d', '--debug', help='Enable debug messages', action='store_true')
684 parser.add_argument('-f', '--fio', help='path to file executable (e.g., ./fio)')
685 parser.add_argument('-a', '--artifact-root', help='artifact root directory')
686 parser.add_argument('-s', '--skip', nargs='+', type=int,
687 help='list of test(s) to skip')
688 parser.add_argument('-o', '--run-only', nargs='+', type=int,
689 help='list of test(s) to run, skipping all others')
690 parser.add_argument('--dut', help='target NVMe character device to test '
691 '(e.g., /dev/ng0n1). WARNING: THIS IS A DESTRUCTIVE TEST', required=True)
692 args = parser.parse_args()
697 FIO_FDP_MAX_RUAMW = 0
698 FIO_FDP_NUMBER_PLIDS = 0
701 """Run tests using fio's io_uring_cmd ioengine to send NVMe pass through commands."""
702 global FIO_FDP_MAX_RUAMW
703 global FIO_FDP_NUMBER_PLIDS
708 logging.basicConfig(level=logging.DEBUG)
710 logging.basicConfig(level=logging.INFO)
712 artifact_root = args.artifact_root if args.artifact_root else \
713 f"nvmept-fdp-test-{time.strftime('%Y%m%d-%H%M%S')}"
714 os.mkdir(artifact_root)
715 print(f"Artifact directory is {artifact_root}")
718 fio_path = str(Path(args.fio).absolute())
721 print(f"fio path is {fio_path}")
723 for test in TEST_LIST:
724 test['fio_opts']['filename'] = args.dut
726 fdp_status = get_fdp_status(args.dut)
727 FIO_FDP_NUMBER_PLIDS = fdp_status['nruhsd']
728 update_all_ruhs(args.dut)
729 FIO_FDP_MAX_RUAMW = check_all_ruhs(args.dut)
730 if not FIO_FDP_MAX_RUAMW:
734 'fio_path': fio_path,
735 'fio_root': str(Path(__file__).absolute().parent.parent),
736 'artifact_root': artifact_root,
737 'basename': 'nvmept-fdp',
740 _, failed, _ = run_fio_tests(TEST_LIST, test_env, args)
744 if __name__ == '__main__':