Commit | Line | Data |
---|---|---|
66cbbb18 VF |
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_trim.py | |
9 | # | |
10 | # Test fio's io_uring_cmd ioengine with NVMe pass-through dataset management | |
11 | # commands that trim multiple ranges. | |
12 | # | |
13 | # USAGE | |
14 | # see python3 nvmept_trim.py --help | |
15 | # | |
16 | # EXAMPLES | |
17 | # python3 t/nvmept_trim.py --dut /dev/ng0n1 | |
18 | # python3 t/nvmept_trim.py --dut /dev/ng1n1 -f ./fio | |
19 | # | |
20 | # REQUIREMENTS | |
21 | # Python 3.6 | |
22 | # | |
23 | """ | |
24 | import os | |
25 | import sys | |
26 | import time | |
27 | import logging | |
28 | import argparse | |
29 | from pathlib import Path | |
30 | from fiotestlib import FioJobCmdTest, run_fio_tests | |
31 | from fiotestcommon import SUCCESS_NONZERO | |
32 | ||
33 | ||
34 | class TrimTest(FioJobCmdTest): | |
35 | """ | |
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. | |
38 | """ | |
39 | ||
40 | def setup(self, parameters): | |
41 | """Setup a test.""" | |
42 | ||
43 | fio_args = [ | |
44 | "--name=nvmept-trim", | |
45 | "--ioengine=io_uring_cmd", | |
46 | "--cmd_type=nvme", | |
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']}", | |
51 | ] | |
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) | |
61 | ||
62 | super().setup(fio_args) | |
63 | ||
64 | ||
65 | def check_result(self): | |
66 | ||
67 | super().check_result() | |
68 | ||
69 | if 'rw' not in self.fio_opts or \ | |
70 | not self.passed or \ | |
71 | 'json' not in self.fio_opts['output-format']: | |
72 | return | |
73 | ||
74 | job = self.json_data['jobs'][0] | |
75 | ||
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) | |
81 | else: | |
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) | |
89 | else: | |
90 | logging.error("Unhandled rw value %s", self.fio_opts['rw']) | |
91 | self.passed = False | |
92 | ||
93 | if 'iodepth' in self.fio_opts: | |
94 | # We will need to figure something out if any test uses an iodepth | |
95 | # different from 8 | |
96 | if job['iodepth_level']['8'] < 95: | |
97 | logging.error("Did not achieve requested iodepth") | |
98 | self.passed = False | |
99 | else: | |
100 | logging.debug("iodepth 8 target met %s", job['iodepth_level']['8']) | |
101 | ||
102 | ||
103 | class RangeTrimTest(TrimTest): | |
104 | """ | |
105 | Multi-range trim test class. | |
106 | """ | |
107 | ||
108 | def get_bs(self): | |
109 | """Calculate block size and determine whether bs will be an average or exact.""" | |
110 | ||
111 | if 'bs' in self.fio_opts: | |
112 | exact_size = True | |
113 | bs = self.fio_opts['bs'] | |
114 | elif 'bssplit' in self.fio_opts: | |
115 | exact_size = False | |
116 | bs = 0 | |
117 | total = 0 | |
118 | for split in self.fio_opts['bssplit'].split(':'): | |
119 | [blocksize, share] = split.split('/') | |
120 | total += int(share) | |
121 | bs += int(blocksize) * int(share) / 100 | |
122 | if total != 100: | |
123 | logging.error("bssplit '%s' total percentage is not 100", self.fio_opts['bssplit']) | |
124 | self.passed = False | |
125 | else: | |
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: | |
130 | exact_size = False | |
131 | [minbs, maxbs] = self.fio_opts['bsrange'].split('-') | |
132 | minbs = int(minbs) | |
133 | maxbs = int(maxbs) | |
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 | |
138 | else: | |
139 | exact_size = True | |
140 | bs = 4096 | |
141 | ||
142 | return bs, exact_size | |
143 | ||
144 | ||
145 | def check_result(self): | |
146 | """ | |
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. | |
150 | """ | |
151 | # TODO Enable debug output to check the actual offsets | |
152 | ||
153 | super().check_result() | |
154 | ||
155 | if not self.passed or 'json' not in self.fio_opts['output-format']: | |
156 | return | |
157 | ||
158 | job = self.json_data['jobs'][0]['trim'] | |
159 | bs, exact_size = self.get_bs() | |
160 | ||
161 | # make sure bw and IOPS are consistent | |
162 | bw = job['bw_bytes'] | |
163 | iops = job['iops'] | |
164 | runtime = job['runtime'] | |
165 | ||
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) | |
171 | self.passed = False | |
172 | else: | |
173 | logging.debug("Total bytes %d from bw matches reported total bytes %d", calculated, | |
174 | expected) | |
175 | ||
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) | |
180 | self.passed = False | |
181 | else: | |
182 | logging.debug("Total bytes %d from IOPS matches reported total bytes %d", calculated, | |
183 | expected) | |
184 | ||
185 | if 'size' in self.fio_opts: | |
186 | io_count = self.fio_opts['size'] / self.fio_opts['num_range'] / bs | |
187 | if exact_size: | |
188 | delta = 0.1 | |
189 | else: | |
190 | delta = 0.05*job['total_ios'] | |
191 | ||
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']) | |
195 | self.passed = False | |
196 | else: | |
197 | logging.debug("Expected numbers of IOs %d matches actual value %d", io_count, | |
198 | job['total_ios']) | |
199 | ||
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']) | |
204 | self.passed = False | |
205 | else: | |
206 | logging.debug("Actual rate %f matches expeected rate %f", bw, self.fio_opts['rate']) | |
207 | ||
208 | ||
209 | ||
210 | TEST_LIST = [ | |
211 | # The group of tests below checks existing use cases to make sure there are | |
212 | # no regressions. | |
213 | { | |
214 | "test_id": 1, | |
215 | "fio_opts": { | |
216 | "rw": 'trim', | |
217 | "time_based": 1, | |
218 | "runtime": 3, | |
219 | "output-format": "json", | |
220 | }, | |
221 | "test_class": TrimTest, | |
222 | }, | |
223 | { | |
224 | "test_id": 2, | |
225 | "fio_opts": { | |
226 | "rw": 'randtrim', | |
227 | "time_based": 1, | |
228 | "runtime": 3, | |
229 | "output-format": "json", | |
230 | }, | |
231 | "test_class": TrimTest, | |
232 | }, | |
233 | { | |
234 | "test_id": 3, | |
235 | "fio_opts": { | |
236 | "rw": 'trim', | |
237 | "time_based": 1, | |
238 | "runtime": 3, | |
239 | "iodepth": 8, | |
240 | "iodepth_batch": 4, | |
241 | "iodepth_batch_complete": 4, | |
242 | "output-format": "json", | |
243 | }, | |
244 | "test_class": TrimTest, | |
245 | }, | |
246 | { | |
247 | "test_id": 4, | |
248 | "fio_opts": { | |
249 | "rw": 'randtrim', | |
250 | "time_based": 1, | |
251 | "runtime": 3, | |
252 | "iodepth": 8, | |
253 | "iodepth_batch": 4, | |
254 | "iodepth_batch_complete": 4, | |
255 | "output-format": "json", | |
256 | }, | |
257 | "test_class": TrimTest, | |
258 | }, | |
259 | { | |
260 | "test_id": 5, | |
261 | "fio_opts": { | |
262 | "rw": 'trimwrite', | |
263 | "time_based": 1, | |
264 | "runtime": 3, | |
265 | "output-format": "json", | |
266 | }, | |
267 | "test_class": TrimTest, | |
268 | }, | |
269 | { | |
270 | "test_id": 6, | |
271 | "fio_opts": { | |
272 | "rw": 'randtrimwrite', | |
273 | "time_based": 1, | |
274 | "runtime": 3, | |
275 | "output-format": "json", | |
276 | }, | |
277 | "test_class": TrimTest, | |
278 | }, | |
279 | { | |
280 | "test_id": 7, | |
281 | "fio_opts": { | |
282 | "rw": 'randtrim', | |
283 | "time_based": 1, | |
284 | "runtime": 3, | |
285 | "fixedbufs": 0, | |
286 | "nonvectored": 1, | |
287 | "force_async": 1, | |
288 | "registerfiles": 1, | |
289 | "sqthread_poll": 1, | |
290 | "fixedbuffs": 1, | |
291 | "output-format": "json", | |
292 | }, | |
293 | "test_class": TrimTest, | |
294 | }, | |
295 | # The group of tests below try out the new functionality | |
296 | { | |
297 | "test_id": 100, | |
298 | "fio_opts": { | |
299 | "rw": 'trim', | |
300 | "num_range": 2, | |
301 | "size": 16*1024*1024, | |
302 | "output-format": "json", | |
303 | }, | |
304 | "test_class": RangeTrimTest, | |
305 | }, | |
306 | { | |
307 | "test_id": 101, | |
308 | "fio_opts": { | |
309 | "rw": 'randtrim', | |
310 | "num_range": 2, | |
311 | "size": 16*1024*1024, | |
312 | "output-format": "json", | |
313 | }, | |
314 | "test_class": RangeTrimTest, | |
315 | }, | |
316 | { | |
317 | "test_id": 102, | |
318 | "fio_opts": { | |
319 | "rw": 'randtrim', | |
320 | "num_range": 256, | |
321 | "size": 64*1024*1024, | |
322 | "output-format": "json", | |
323 | }, | |
324 | "test_class": RangeTrimTest, | |
325 | }, | |
326 | { | |
327 | "test_id": 103, | |
328 | "fio_opts": { | |
329 | "rw": 'trim', | |
330 | "num_range": 2, | |
331 | "bs": 16*1024, | |
332 | "size": 32*1024*1024, | |
333 | "output-format": "json", | |
334 | }, | |
335 | "test_class": RangeTrimTest, | |
336 | }, | |
337 | { | |
338 | "test_id": 104, | |
339 | "fio_opts": { | |
340 | "rw": 'randtrim', | |
341 | "num_range": 2, | |
342 | "bs": 16*1024, | |
343 | "size": 32*1024*1024, | |
344 | "output-format": "json", | |
345 | }, | |
346 | "test_class": RangeTrimTest, | |
347 | }, | |
348 | { | |
349 | "test_id": 105, | |
350 | "fio_opts": { | |
351 | "rw": 'randtrim', | |
352 | "num_range": 2, | |
353 | "bssplit": "4096/50:16384/50", | |
354 | "size": 80*1024*1024, | |
355 | "output-format": "json", | |
356 | "randrepeat": 0, | |
357 | }, | |
358 | "test_class": RangeTrimTest, | |
359 | }, | |
360 | { | |
361 | "test_id": 106, | |
362 | "fio_opts": { | |
363 | "rw": 'randtrim', | |
364 | "num_range": 4, | |
365 | "bssplit": "4096/25:8192/25:12288/25:16384/25", | |
366 | "size": 80*1024*1024, | |
367 | "output-format": "json", | |
368 | "randrepeat": 0, | |
369 | }, | |
370 | "test_class": RangeTrimTest, | |
371 | }, | |
372 | { | |
373 | "test_id": 107, | |
374 | "fio_opts": { | |
375 | "rw": 'randtrim', | |
376 | "num_range": 4, | |
377 | "bssplit": "4096/20:8192/20:12288/20:16384/20:20480/20", | |
378 | "size": 72*1024*1024, | |
379 | "output-format": "json", | |
380 | "randrepeat": 0, | |
381 | }, | |
382 | "test_class": RangeTrimTest, | |
383 | }, | |
384 | { | |
385 | "test_id": 108, | |
386 | "fio_opts": { | |
387 | "rw": 'randtrim', | |
388 | "num_range": 2, | |
389 | "bsrange": "4096-16384", | |
390 | "size": 80*1024*1024, | |
391 | "output-format": "json", | |
392 | "randrepeat": 0, | |
393 | }, | |
394 | "test_class": RangeTrimTest, | |
395 | }, | |
396 | { | |
397 | "test_id": 109, | |
398 | "fio_opts": { | |
399 | "rw": 'randtrim', | |
400 | "num_range": 4, | |
401 | "bsrange": "4096-20480", | |
402 | "size": 72*1024*1024, | |
403 | "output-format": "json", | |
404 | "randrepeat": 0, | |
405 | }, | |
406 | "test_class": RangeTrimTest, | |
407 | }, | |
408 | { | |
409 | "test_id": 110, | |
410 | "fio_opts": { | |
411 | "rw": 'randtrim', | |
412 | "time_based": 1, | |
413 | "runtime": 10, | |
414 | "rate": 1024*1024, | |
415 | "num_range": 2, | |
416 | "output-format": "json", | |
417 | }, | |
418 | "test_class": RangeTrimTest, | |
419 | }, | |
420 | # All of the tests below should fail | |
421 | # TODO check the error messages resulting from the jobs below | |
422 | { | |
423 | "test_id": 200, | |
424 | "fio_opts": { | |
425 | "rw": 'randtrimwrite', | |
426 | "time_based": 1, | |
427 | "runtime": 10, | |
428 | "rate": 1024*1024, | |
429 | "num_range": 2, | |
430 | "output-format": "normal", | |
431 | }, | |
432 | "test_class": RangeTrimTest, | |
433 | "success": SUCCESS_NONZERO, | |
434 | }, | |
435 | { | |
436 | "test_id": 201, | |
437 | "fio_opts": { | |
438 | "rw": 'trimwrite', | |
439 | "time_based": 1, | |
440 | "runtime": 10, | |
441 | "rate": 1024*1024, | |
442 | "num_range": 2, | |
443 | "output-format": "normal", | |
444 | }, | |
445 | "test_class": RangeTrimTest, | |
446 | "success": SUCCESS_NONZERO, | |
447 | }, | |
448 | { | |
449 | "test_id": 202, | |
450 | "fio_opts": { | |
451 | "rw": 'trim', | |
452 | "time_based": 1, | |
453 | "runtime": 10, | |
454 | "num_range": 257, | |
455 | "output-format": "normal", | |
456 | }, | |
457 | "test_class": RangeTrimTest, | |
458 | "success": SUCCESS_NONZERO, | |
459 | }, | |
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 | |
466 | { | |
467 | "test_id": 300, | |
468 | "fio_opts": { | |
469 | "rw": 'write', | |
470 | "output-format": 'json', | |
471 | "buffer_pattern": 0x0f, | |
472 | "size": 256*1024*1024, | |
6a9a9bd2 | 473 | "bs": 256*1024, |
66cbbb18 VF |
474 | }, |
475 | "test_class": TrimTest, | |
476 | }, | |
477 | { | |
478 | "test_id": 301, | |
479 | "fio_opts": { | |
480 | "rw": 'read', | |
481 | "output-format": 'json', | |
482 | "verify_pattern": 0x0f, | |
483 | "verify": "pattern", | |
484 | "size": 256*1024*1024, | |
6a9a9bd2 | 485 | "bs": 256*1024, |
66cbbb18 VF |
486 | }, |
487 | "test_class": TrimTest, | |
488 | }, | |
489 | { | |
490 | "test_id": 302, | |
491 | "fio_opts": { | |
492 | "rw": 'randtrim', | |
493 | "num_range": 8, | |
494 | "output-format": 'json', | |
495 | "size": 128*1024*1024, | |
6a9a9bd2 | 496 | "bs": 256*1024, |
66cbbb18 VF |
497 | }, |
498 | "test_class": TrimTest, | |
499 | }, | |
500 | # The identify namespace data structure has a DLFEAT field which specifies | |
501 | # what happens when reading data from deallocated blocks. There are three | |
502 | # options: | |
503 | # - read behavior not reported | |
504 | # - deallocated logical block returns all bytes 0x0 | |
505 | # - deallocated logical block returns all bytes 0xff | |
506 | # The test below merely checks that the original data pattern is not returned. | |
507 | # Source: Figure 97 from | |
508 | # https://nvmexpress.org/wp-content/uploads/NVM-Express-NVM-Command-Set-Specification-1.0c-2022.10.03-Ratified.pdf | |
509 | { | |
510 | "test_id": 303, | |
511 | "fio_opts": { | |
512 | "rw": 'read', | |
513 | "output-format": 'json', | |
514 | "verify_pattern": 0x0f, | |
515 | "verify": "pattern", | |
516 | "size": 128*1024*1024, | |
6a9a9bd2 | 517 | "bs": 256*1024, |
66cbbb18 VF |
518 | }, |
519 | "test_class": TrimTest, | |
520 | "success": SUCCESS_NONZERO, | |
521 | }, | |
522 | { | |
523 | "test_id": 304, | |
524 | "fio_opts": { | |
525 | "rw": 'read', | |
526 | "output-format": 'json', | |
527 | "verify_pattern": 0x0f, | |
528 | "verify": "pattern", | |
529 | "offset": 128*1024*1024, | |
530 | "size": 128*1024*1024, | |
6a9a9bd2 | 531 | "bs": 256*1024, |
66cbbb18 VF |
532 | }, |
533 | "test_class": TrimTest, | |
534 | }, | |
535 | ] | |
536 | ||
537 | def parse_args(): | |
538 | """Parse command-line arguments.""" | |
539 | ||
540 | parser = argparse.ArgumentParser() | |
541 | parser.add_argument('-d', '--debug', help='Enable debug messages', action='store_true') | |
542 | parser.add_argument('-f', '--fio', help='path to file executable (e.g., ./fio)') | |
543 | parser.add_argument('-a', '--artifact-root', help='artifact root directory') | |
544 | parser.add_argument('-s', '--skip', nargs='+', type=int, | |
545 | help='list of test(s) to skip') | |
546 | parser.add_argument('-o', '--run-only', nargs='+', type=int, | |
547 | help='list of test(s) to run, skipping all others') | |
548 | parser.add_argument('--dut', help='target NVMe character device to test ' | |
549 | '(e.g., /dev/ng0n1). WARNING: THIS IS A DESTRUCTIVE TEST', required=True) | |
550 | args = parser.parse_args() | |
551 | ||
552 | return args | |
553 | ||
554 | ||
555 | def main(): | |
556 | """Run tests using fio's io_uring_cmd ioengine to send NVMe pass through commands.""" | |
557 | ||
558 | args = parse_args() | |
559 | ||
560 | if args.debug: | |
561 | logging.basicConfig(level=logging.DEBUG) | |
562 | else: | |
563 | logging.basicConfig(level=logging.INFO) | |
564 | ||
565 | artifact_root = args.artifact_root if args.artifact_root else \ | |
566 | f"nvmept-trim-test-{time.strftime('%Y%m%d-%H%M%S')}" | |
567 | os.mkdir(artifact_root) | |
568 | print(f"Artifact directory is {artifact_root}") | |
569 | ||
570 | if args.fio: | |
571 | fio_path = str(Path(args.fio).absolute()) | |
572 | else: | |
573 | fio_path = 'fio' | |
574 | print(f"fio path is {fio_path}") | |
575 | ||
576 | for test in TEST_LIST: | |
577 | test['fio_opts']['filename'] = args.dut | |
578 | ||
579 | test_env = { | |
580 | 'fio_path': fio_path, | |
581 | 'fio_root': str(Path(__file__).absolute().parent.parent), | |
582 | 'artifact_root': artifact_root, | |
583 | 'basename': 'nvmept-trim', | |
584 | } | |
585 | ||
586 | _, failed, _ = run_fio_tests(TEST_LIST, test_env, args) | |
587 | sys.exit(failed) | |
588 | ||
589 | ||
590 | if __name__ == '__main__': | |
591 | main() |