Merge branch 'atomic-writes'
[fio.git] / t / nvmept_fdp.py
1 #!/usr/bin/env python3
2 #
3 # Copyright 2024 Samsung Electronics Co., Ltd All Rights Reserved
4 #
5 # For conditions of distribution and use, see the accompanying COPYING file.
6 #
7 """
8 # nvmept_fdp.py
9 #
10 # Test fio's io_uring_cmd ioengine with NVMe pass-through FDP write commands.
11 #
12 # USAGE
13 # see python3 nvmept_fdp.py --help
14 #
15 # EXAMPLES
16 # python3 t/nvmept_fdp.py --dut /dev/ng0n1
17 # python3 t/nvmept_fdp.py --dut /dev/ng1n1 -f ./fio
18 #
19 # REQUIREMENTS
20 # Python 3.6
21 # Device formatted with LBA data size 4096 bytes
22 # Device with at least five placement IDs
23 #
24 # WARNING
25 # This is a destructive test
26 """
27 import os
28 import sys
29 import json
30 import time
31 import locale
32 import logging
33 import argparse
34 import subprocess
35 from pathlib import Path
36 from fiotestlib import FioJobCmdTest, run_fio_tests
37 from fiotestcommon import SUCCESS_NONZERO
38
39
40 class FDPTest(FioJobCmdTest):
41     """
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.
44     """
45
46     def setup(self, parameters):
47         """Setup a test."""
48
49         fio_args = [
50             "--name=nvmept-fdp",
51             "--ioengine=io_uring_cmd",
52             "--cmd_type=nvme",
53             "--randrepeat=0",
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']}",
58         ]
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)
70
71         super().setup(fio_args)
72
73
74     def check_result(self):
75         try:
76             self._check_result()
77         finally:
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")
82
83
84     def _check_result(self):
85
86         super().check_result()
87
88         if 'rw' not in self.fio_opts or \
89                 not self.passed or \
90                 'json' not in self.fio_opts['output-format']:
91             return
92
93         job = self.json_data['jobs'][0]
94
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)
100             else:
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)
108         else:
109             logging.error("Unhandled rw value %s", self.fio_opts['rw'])
110             self.passed = False
111
112         if 'iodepth' in self.fio_opts:
113             # We will need to figure something out if any test uses an iodepth
114             # different from 8
115             if job['iodepth_level']['8'] < 95:
116                 logging.error("Did not achieve requested iodepth")
117                 self.passed = False
118             else:
119                 logging.debug("iodepth 8 target met %s", job['iodepth_level']['8'])
120
121
122 class FDPMultiplePLIDTest(FDPTest):
123     """
124     Write to multiple placement IDs.
125     """
126
127     def setup(self, parameters):
128         mapping = {
129                     'nruhsd': FIO_FDP_NUMBER_PLIDS,
130                     'max_ruamw': FIO_FDP_MAX_RUAMW,
131                 }
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))
134
135         super().setup(parameters)
136
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(',')
142         else:
143             plid_list = list(range(FIO_FDP_NUMBER_PLIDS))
144
145         plid_list = sorted([int(i) for i in plid_list])
146         logging.debug("plid_list: %s", str(plid_list))
147
148         fdp_status = get_fdp_status(self.fio_opts['filename'])
149
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']
155
156         if select == "roundrobin":
157             self._check_robin(plid_list, fdp_status)
158         elif select == "random":
159             self._check_random(plid_list, fdp_status)
160         else:
161             logging.error("Unknown plid selection strategy %s", select)
162             self.passed = False
163
164         super()._check_result()
165
166     def _check_robin(self, plid_list, fdp_status):
167         """
168         With round robin we can know exactly how many writes each PLID will
169         receive.
170         """
171         ruamw = [FIO_FDP_MAX_RUAMW] * FIO_FDP_NUMBER_PLIDS
172
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",
176                       whole, remainder)
177
178         for plid in plid_list:
179             ruamw[plid] -= whole
180             if remainder:
181                 ruamw[plid] -= 1
182                 remainder -= 1
183         logging.debug("Expected ruamw values: %s", str(ruamw))
184
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'])
189                 self.passed = False
190                 break
191
192             logging.debug("RUAMW match with idx %d, pid %d: ruamw=%d", idx, ruhs['pid'], ruamw[idx])
193
194     def _check_random(self, plid_list, fdp_status):
195         """
196         With random selection, a set of PLIDs will receive all the write
197         operations and the remainder will be untouched.
198         """
199
200         total_ruamw = 0
201         for plid in plid_list:
202             total_ruamw += fdp_status['ruhss'][plid]['ruamw']
203
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)
208             self.passed = False
209         else:
210             logging.debug("Observed expected total ruamw %d for plids %s", expected, str(plid_list))
211
212         for idx, ruhs in enumerate(fdp_status['ruhss']):
213             if idx in plid_list:
214                 continue
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)
218                 self.passed = False
219             else:
220                 logging.debug("Observed expected ruamw %d for idx %d, pid %d", ruhs['ruamw'], idx,
221                               ruhs['pid'])
222
223
224 class FDPSinglePLIDTest(FDPTest):
225     """
226     Write to a single placement ID only.
227     """
228
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']
234         else:
235             plid = 0
236
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']
240
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)
244             self.passed = False
245         else:
246             logging.debug("FDP accounting as expected for plid %d; ruamw = %d", plid, ruamw)
247
248         super()._check_result()
249
250
251 class FDPReadTest(FDPTest):
252     """
253     Read workload test.
254     """
255
256     def _check_result(self):
257         ruamw = check_all_ruhs(self.fio_opts['filename'])
258
259         if ruamw != FIO_FDP_MAX_RUAMW:
260             logging.error("Read workload affected FDP ruamw")
261             self.passed = False
262         else:
263             logging.debug("Read workload did not disturb FDP ruamw")
264             super()._check_result()
265
266
267 def get_fdp_status(dut):
268     """
269     Run the nvme-cli command to obtain FDP status and return result as a JSON
270     object.
271     """
272
273     cmd = f"sudo nvme fdp status --output-format=json {dut}"
274     cmd = cmd.split(' ')
275     cmd_result = subprocess.run(cmd, capture_output=True, check=False,
276                                 encoding=locale.getpreferredencoding())
277
278     if cmd_result.returncode != 0:
279         logging.error("Error obtaining device %s FDP status: %s", dut, cmd_result.stderr)
280         return False
281
282     return json.loads(cmd_result.stdout)
283
284
285 def update_ruh(dut, plid):
286     """
287     Update reclaim unit handles with specified ID(s). This tells the device to
288     point the RUH to a new (empty) reclaim unit.
289     """
290
291     ids = ','.join(plid) if isinstance(plid, list) else plid
292     cmd = f"nvme fdp update --pids={ids} {dut}"
293     cmd = cmd.split(' ')
294     cmd_result = subprocess.run(cmd, capture_output=True, check=False,
295                                 encoding=locale.getpreferredencoding())
296
297     if cmd_result.returncode != 0:
298         logging.error("Error updating RUH %s ID(s) %s", dut, ids)
299         return False
300
301     return True
302
303
304 def update_all_ruhs(dut):
305     """
306     Update all reclaim unit handles on the device.
307     """
308
309     fdp_status = get_fdp_status(dut)
310     for ruhs in fdp_status['ruhss']:
311         if not update_ruh(dut, ruhs['pid']):
312             return False
313
314     return True
315
316
317 def check_all_ruhs(dut):
318     """
319     Check that all RUHs have the same value for reclaim unit available media
320     writes (RUAMW).  Return the RUAMW value.
321     """
322
323     fdp_status = get_fdp_status(dut)
324     ruh_status = fdp_status['ruhss']
325
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)
330             return False
331
332     return ruamw
333
334
335 TEST_LIST = [
336     # Write one LBA to one PLID using both the old and new sets of options
337     ## omit fdp_pli_select/plid_select
338     {
339         "test_id": 1,
340         "fio_opts": {
341             "rw": 'write',
342             "bs": 4096,
343             "number_ios": 1,
344             "verify": "crc32c",
345             "fdp": 1,
346             "fdp_pli": 3,
347             "output-format": "json",
348             },
349         "test_class": FDPSinglePLIDTest,
350     },
351     {
352         "test_id": 2,
353         "fio_opts": {
354             "rw": 'randwrite',
355             "bs": 4096,
356             "number_ios": 1,
357             "verify": "crc32c",
358             "dataplacement": "fdp",
359             "plids": 3,
360             "output-format": "json",
361             },
362         "test_class": FDPSinglePLIDTest,
363     },
364     ## fdp_pli_select/plid_select=roundrobin
365     {
366         "test_id": 3,
367         "fio_opts": {
368             "rw": 'write',
369             "bs": 4096,
370             "number_ios": 1,
371             "verify": "crc32c",
372             "fdp": 1,
373             "fdp_pli": 3,
374             "fdp_pli_select": "roundrobin",
375             "output-format": "json",
376             },
377         "test_class": FDPSinglePLIDTest,
378     },
379     {
380         "test_id": 4,
381         "fio_opts": {
382             "rw": 'randwrite',
383             "bs": 4096,
384             "number_ios": 1,
385             "verify": "crc32c",
386             "dataplacement": "fdp",
387             "plids": 3,
388             "plid_select": "roundrobin",
389             "output-format": "json",
390             },
391         "test_class": FDPSinglePLIDTest,
392     },
393     ## fdp_pli_select/plid_select=random
394     {
395         "test_id": 5,
396         "fio_opts": {
397             "rw": 'write',
398             "bs": 4096,
399             "number_ios": 1,
400             "verify": "crc32c",
401             "fdp": 1,
402             "fdp_pli": 3,
403             "fdp_pli_select": "random",
404             "output-format": "json",
405             },
406         "test_class": FDPSinglePLIDTest,
407     },
408     {
409         "test_id": 6,
410         "fio_opts": {
411             "rw": 'randwrite',
412             "bs": 4096,
413             "number_ios": 1,
414             "verify": "crc32c",
415             "dataplacement": "fdp",
416             "plids": 3,
417             "plid_select": "random",
418             "output-format": "json",
419             },
420         "test_class": FDPSinglePLIDTest,
421     },
422     # Write four LBAs to one PLID using both the old and new sets of options
423     ## omit fdp_pli_select/plid_select
424     {
425         "test_id": 7,
426         "fio_opts": {
427             "rw": 'write',
428             "bs": 4096,
429             "number_ios": 4,
430             "verify": "crc32c",
431             "fdp": 1,
432             "fdp_pli": 1,
433             "output-format": "json",
434             },
435         "test_class": FDPSinglePLIDTest,
436     },
437     {
438         "test_id": 8,
439         "fio_opts": {
440             "rw": 'randwrite',
441             "bs": 4096,
442             "number_ios": 4,
443             "verify": "crc32c",
444             "dataplacement": "fdp",
445             "plids": 1,
446             "output-format": "json",
447             },
448         "test_class": FDPSinglePLIDTest,
449     },
450     ## fdp_pli_select/plid_select=roundrobin
451     {
452         "test_id": 9,
453         "fio_opts": {
454             "rw": 'write',
455             "bs": 4096,
456             "number_ios": 4,
457             "verify": "crc32c",
458             "fdp": 1,
459             "fdp_pli": 1,
460             "fdp_pli_select": "roundrobin",
461             "output-format": "json",
462             },
463         "test_class": FDPSinglePLIDTest,
464     },
465     {
466         "test_id": 10,
467         "fio_opts": {
468             "rw": 'randwrite',
469             "bs": 4096,
470             "number_ios": 4,
471             "verify": "crc32c",
472             "dataplacement": "fdp",
473             "plids": 1,
474             "plid_select": "roundrobin",
475             "output-format": "json",
476             },
477         "test_class": FDPSinglePLIDTest,
478     },
479     ## fdp_pli_select/plid_select=random
480     {
481         "test_id": 11,
482         "fio_opts": {
483             "rw": 'write',
484             "bs": 4096,
485             "number_ios": 4,
486             "verify": "crc32c",
487             "fdp": 1,
488             "fdp_pli": 1,
489             "fdp_pli_select": "random",
490             "output-format": "json",
491             },
492         "test_class": FDPSinglePLIDTest,
493     },
494     {
495         "test_id": 12,
496         "fio_opts": {
497             "rw": 'randwrite',
498             "bs": 4096,
499             "number_ios": 4,
500             "verify": "crc32c",
501             "dataplacement": "fdp",
502             "plids": 1,
503             "plid_select": "random",
504             "output-format": "json",
505             },
506         "test_class": FDPSinglePLIDTest,
507     },
508     # Just a regular write without FDP directive--should land on plid 0
509     {
510         "test_id": 13,
511         "fio_opts": {
512             "rw": 'randwrite',
513             "bs": 4096,
514             "number_ios": 19,
515             "verify": "crc32c",
516             "output-format": "json",
517             },
518         "test_class": FDPSinglePLIDTest,
519     },
520     # Read workload
521     {
522         "test_id": 14,
523         "fio_opts": {
524             "rw": 'randread',
525             "bs": 4096,
526             "number_ios": 19,
527             "output-format": "json",
528             },
529         "test_class": FDPReadTest,
530     },
531     # write to multiple PLIDs using round robin to select PLIDs
532     ## write to all PLIDs using old and new sets of options
533     {
534         "test_id": 100,
535         "fio_opts": {
536             "rw": 'randwrite',
537             "bs": 4096,
538             "number_ios": "2*{nruhsd}+3",
539             "verify": "crc32c",
540             "fdp": 1,
541             "fdp_pli_select": "roundrobin",
542             "output-format": "json",
543             },
544         "test_class": FDPMultiplePLIDTest,
545     },
546     {
547         "test_id": 101,
548         "fio_opts": {
549             "rw": 'randwrite',
550             "bs": 4096,
551             "number_ios": "2*{nruhsd}+3",
552             "verify": "crc32c",
553             "dataplacement": "fdp",
554             "plid_select": "roundrobin",
555             "output-format": "json",
556             },
557         "test_class": FDPMultiplePLIDTest,
558     },
559     ## write to a subset of PLIDs using old and new sets of options
560     {
561         "test_id": 102,
562         "fio_opts": {
563             "rw": 'randwrite',
564             "bs": 4096,
565             "number_ios": "{nruhsd}+1",
566             "verify": "crc32c",
567             "fdp": 1,
568             "fdp_pli": "1,3",
569             "fdp_pli_select": "roundrobin",
570             "output-format": "json",
571             },
572         "test_class": FDPMultiplePLIDTest,
573     },
574     {
575         "test_id": 103,
576         "fio_opts": {
577             "rw": 'randwrite',
578             "bs": 4096,
579             "number_ios": "{nruhsd}+1",
580             "verify": "crc32c",
581             "dataplacement": "fdp",
582             "plids": "1,3",
583             "plid_select": "roundrobin",
584             "output-format": "json",
585             },
586         "test_class": FDPMultiplePLIDTest,
587     },
588     # write to multiple PLIDs using random selection of PLIDs
589     ## write to all PLIDs using old and new sets of options
590     {
591         "test_id": 200,
592         "fio_opts": {
593             "rw": 'randwrite',
594             "bs": 4096,
595             "number_ios": "{max_ruamw}-1",
596             "verify": "crc32c",
597             "fdp": 1,
598             "fdp_pli_select": "random",
599             "output-format": "json",
600             },
601         "test_class": FDPMultiplePLIDTest,
602     },
603     {
604         "test_id": 201,
605         "fio_opts": {
606             "rw": 'randwrite',
607             "bs": 4096,
608             "number_ios": "{max_ruamw}-1",
609             "verify": "crc32c",
610             "dataplacement": "fdp",
611             "plid_select": "random",
612             "output-format": "json",
613             },
614         "test_class": FDPMultiplePLIDTest,
615     },
616     ## write to a subset of PLIDs using old and new sets of options
617     {
618         "test_id": 202,
619         "fio_opts": {
620             "rw": 'randwrite',
621             "bs": 4096,
622             "number_ios": "{max_ruamw}-1",
623             "verify": "crc32c",
624             "fdp": 1,
625             "fdp_pli": "1,3,4",
626             "fdp_pli_select": "random",
627             "output-format": "json",
628             },
629         "test_class": FDPMultiplePLIDTest,
630     },
631     {
632         "test_id": 203,
633         "fio_opts": {
634             "rw": 'randwrite',
635             "bs": 4096,
636             "number_ios": "{max_ruamw}-1",
637             "verify": "crc32c",
638             "dataplacement": "fdp",
639             "plids": "1,3,4",
640             "plid_select": "random",
641             "output-format": "json",
642             },
643         "test_class": FDPMultiplePLIDTest,
644     },
645     # Specify invalid options fdp=1 and dataplacement=none
646     {
647         "test_id": 300,
648         "fio_opts": {
649             "rw": 'write',
650             "bs": 4096,
651             "io_size": 4096,
652             "verify": "crc32c",
653             "fdp": 1,
654             "fdp_pli": 3,
655             "output-format": "normal",
656             "dataplacement": "none",
657             },
658         "test_class": FDPTest,
659         "success": SUCCESS_NONZERO,
660     },
661     # Specify invalid options fdp=1 and dataplacement=streams
662     {
663         "test_id": 301,
664         "fio_opts": {
665             "rw": 'write',
666             "bs": 4096,
667             "io_size": 4096,
668             "verify": "crc32c",
669             "fdp": 1,
670             "fdp_pli": 3,
671             "output-format": "normal",
672             "dataplacement": "streams",
673             },
674         "test_class": FDPTest,
675         "success": SUCCESS_NONZERO,
676     },
677 ]
678
679 def parse_args():
680     """Parse command-line arguments."""
681
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()
693
694     return args
695
696
697 FIO_FDP_MAX_RUAMW = 0
698 FIO_FDP_NUMBER_PLIDS = 0
699
700 def main():
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
704
705     args = parse_args()
706
707     if args.debug:
708         logging.basicConfig(level=logging.DEBUG)
709     else:
710         logging.basicConfig(level=logging.INFO)
711
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}")
716
717     if args.fio:
718         fio_path = str(Path(args.fio).absolute())
719     else:
720         fio_path = 'fio'
721     print(f"fio path is {fio_path}")
722
723     for test in TEST_LIST:
724         test['fio_opts']['filename'] = args.dut
725
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:
731         sys.exit(-1)
732
733     test_env = {
734               'fio_path': fio_path,
735               'fio_root': str(Path(__file__).absolute().parent.parent),
736               'artifact_root': artifact_root,
737               'basename': 'nvmept-fdp',
738               }
739
740     _, failed, _ = run_fio_tests(TEST_LIST, test_env, args)
741     sys.exit(failed)
742
743
744 if __name__ == '__main__':
745     main()