blob: c990747dbcd9d5ec1cb501e5dd25fd40ab56b9e0 [file] [log] [blame]
#!/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,
"bs": 256*1024,
},
"test_class": TrimTest,
},
{
"test_id": 301,
"fio_opts": {
"rw": 'read',
"output-format": 'json',
"verify_pattern": 0x0f,
"verify": "pattern",
"size": 256*1024*1024,
"bs": 256*1024,
},
"test_class": TrimTest,
},
{
"test_id": 302,
"fio_opts": {
"rw": 'randtrim',
"num_range": 8,
"output-format": 'json',
"size": 128*1024*1024,
"bs": 256*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,
"bs": 256*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,
"bs": 256*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()