blob: e5425506c6b0420e1895370f8cc190b696de1d49 [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_streams.py
#
# Test fio's NVMe streams support using the io_uring_cmd ioengine with NVMe
# pass-through commands.
#
# USAGE
# see python3 nvmept_streams.py --help
#
# EXAMPLES
# python3 t/nvmept_streams.py --dut /dev/ng0n1
# python3 t/nvmept_streams.py --dut /dev/ng1n1 -f ./fio
#
# REQUIREMENTS
# Python 3.6
#
# WARNING
# This is a destructive test
#
# Enable streams with
# nvme dir-send -D 0 -O 1 -e 1 -T 1 /dev/nvme0n1
#
# See streams directive status with
# nvme dir-receive -D 0 -O 1 -H /dev/nvme0n1
"""
import os
import sys
import time
import locale
import logging
import argparse
import subprocess
from pathlib import Path
from fiotestlib import FioJobCmdTest, run_fio_tests
from fiotestcommon import SUCCESS_NONZERO
class StreamsTest(FioJobCmdTest):
"""
NVMe pass-through test class for streams. 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-streams",
"--ioengine=io_uring_cmd",
"--cmd_type=nvme",
"--randrepeat=0",
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', 'offset', 'dataplacement',
'plids', 'plid_select' ]:
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):
try:
self._check_result()
finally:
release_all_streams(self.fio_opts['filename'])
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'])
stream_ids = [int(stream) for stream in self.fio_opts['plids'].split(',')]
if not self.check_streams(self.fio_opts['filename'], stream_ids):
self.passed = False
logging.error("Streams not as expected")
else:
logging.debug("Streams created as expected")
def check_streams(self, dut, stream_ids):
"""
Confirm that the specified stream IDs exist on the specified device.
"""
id_list = get_device_stream_ids(dut)
if not id_list:
return False
for stream in stream_ids:
if stream in id_list:
logging.debug("Stream ID %d found active on device", stream)
id_list.remove(stream)
else:
if self.__class__.__name__ != "StreamsTestRand":
logging.error("Stream ID %d not found on device", stream)
else:
logging.debug("Stream ID %d not found on device", stream)
return False
if len(id_list) != 0:
logging.error("Extra stream IDs %s found on device", str(id_list))
return False
return True
class StreamsTestRR(StreamsTest):
"""
NVMe pass-through test class for streams. Check to make sure output for
selected data direction(s) is non-zero and that zero data appears for other
directions. Check that Stream IDs are accessed in round robin order.
"""
def check_streams(self, dut, stream_ids):
"""
The number of IOs is less than the number of stream IDs provided. Let N
be the number of IOs. Make sure that the device only has the first N of
the stream IDs provided.
This will miss some cases where some other selection algorithm happens
to select the first N stream IDs. The solution would be to repeat this
test multiple times. Multiple trials passing would be evidence that
round robin is working correctly.
"""
id_list = get_device_stream_ids(dut)
if not id_list:
return False
num_streams = int(self.fio_opts['io_size'] / self.fio_opts['bs'])
stream_ids = sorted(stream_ids)[0:num_streams]
return super().check_streams(dut, stream_ids)
class StreamsTestRand(StreamsTest):
"""
NVMe pass-through test class for streams. Check to make sure output for
selected data direction(s) is non-zero and that zero data appears for other
directions. Check that Stream IDs are accessed in random order.
"""
def check_streams(self, dut, stream_ids):
"""
The number of IOs is less than the number of stream IDs provided. Let N
be the number of IOs. Confirm that the stream IDs on the device are not
the first N stream IDs.
This will produce false positives because it is possible for the first
N stream IDs to be randomly selected. We can reduce the probability of
false positives by increasing N and increasing the number of streams
IDs to choose from, although fio has a max of 16 placement IDs.
"""
id_list = get_device_stream_ids(dut)
if not id_list:
return False
num_streams = int(self.fio_opts['io_size'] / self.fio_opts['bs'])
stream_ids = sorted(stream_ids)[0:num_streams]
return not super().check_streams(dut, stream_ids)
def get_device_stream_ids(dut):
cmd = f"sudo nvme dir-receive -D 1 -O 2 -H {dut}"
logging.debug("check streams command: %s", cmd)
cmd = cmd.split(' ')
cmd_result = subprocess.run(cmd, capture_output=True, check=False,
encoding=locale.getpreferredencoding())
logging.debug(cmd_result.stdout)
if cmd_result.returncode != 0:
logging.error("Error obtaining device %s stream IDs: %s", dut, cmd_result.stderr)
return False
id_list = []
for line in cmd_result.stdout.split('\n'):
if not 'Stream Identifier' in line:
continue
tokens = line.split(':')
id_list.append(int(tokens[1]))
return id_list
def release_stream(dut, stream_id):
"""
Release stream on given device with selected ID.
"""
cmd = f"nvme dir-send -D 1 -O 1 -S {stream_id} {dut}"
logging.debug("release stream command: %s", cmd)
cmd = cmd.split(' ')
cmd_result = subprocess.run(cmd, capture_output=True, check=False,
encoding=locale.getpreferredencoding())
if cmd_result.returncode != 0:
logging.error("Error releasing %s stream %d", dut, stream_id)
return False
return True
def release_all_streams(dut):
"""
Release all streams on specified device.
"""
id_list = get_device_stream_ids(dut)
if not id_list:
return False
for stream in id_list:
if not release_stream(dut, stream):
return False
return True
TEST_LIST = [
# 4k block size
# {seq write, rand write} x {single stream, four streams}
{
"test_id": 1,
"fio_opts": {
"rw": 'write',
"bs": 4096,
"io_size": 256*1024*1024,
"verify": "crc32c",
"plids": "8",
"dataplacement": "streams",
"output-format": "json",
},
"test_class": StreamsTest,
},
{
"test_id": 2,
"fio_opts": {
"rw": 'randwrite',
"bs": 4096,
"io_size": 256*1024*1024,
"verify": "crc32c",
"plids": "3",
"dataplacement": "streams",
"output-format": "json",
},
"test_class": StreamsTest,
},
{
"test_id": 3,
"fio_opts": {
"rw": 'write',
"bs": 4096,
"io_size": 256*1024*1024,
"verify": "crc32c",
"plids": "1,2,3,4",
"dataplacement": "streams",
"output-format": "json",
},
"test_class": StreamsTest,
},
{
"test_id": 4,
"fio_opts": {
"rw": 'randwrite',
"bs": 4096,
"io_size": 256*1024*1024,
"verify": "crc32c",
"plids": "5,6,7,8",
"dataplacement": "streams",
"output-format": "json",
},
"test_class": StreamsTest,
},
# 256KiB block size
# {seq write, rand write} x {single stream, four streams}
{
"test_id": 10,
"fio_opts": {
"rw": 'write',
"bs": 256*1024,
"io_size": 256*1024*1024,
"verify": "crc32c",
"plids": "88",
"dataplacement": "streams",
"output-format": "json",
},
"test_class": StreamsTest,
},
{
"test_id": 11,
"fio_opts": {
"rw": 'randwrite',
"bs": 256*1024,
"io_size": 256*1024*1024,
"verify": "crc32c",
"plids": "20",
"dataplacement": "streams",
"output-format": "json",
},
"test_class": StreamsTest,
},
{
"test_id": 12,
"fio_opts": {
"rw": 'write',
"bs": 256*1024,
"io_size": 256*1024*1024,
"verify": "crc32c",
"plids": "16,32,64,128",
"dataplacement": "streams",
"output-format": "json",
},
"test_class": StreamsTest,
},
{
"test_id": 13,
"fio_opts": {
"rw": 'randwrite',
"bs": 256*1024,
"io_size": 256*1024*1024,
"verify": "crc32c",
"plids": "10,20,40,82",
"dataplacement": "streams",
"output-format": "json",
},
"test_class": StreamsTest,
},
# Test placement ID selection patterns
# default is round robin
{
"test_id": 20,
"fio_opts": {
"rw": 'write',
"bs": 4096,
"io_size": 8192,
"plids": '88,99,100,123,124,125,126,127,128,129,130,131,132,133,134,135',
"dataplacement": "streams",
"output-format": "json",
},
"test_class": StreamsTestRR,
},
{
"test_id": 21,
"fio_opts": {
"rw": 'write',
"bs": 4096,
"io_size": 8192,
"plids": '12,88,99,100,123,124,125,126,127,128,129,130,131,132,133,11',
"dataplacement": "streams",
"output-format": "json",
},
"test_class": StreamsTestRR,
},
# explicitly select round robin
{
"test_id": 22,
"fio_opts": {
"rw": 'write',
"bs": 4096,
"io_size": 8192,
"plids": '22,88,99,100,123,124,125,126,127,128,129,130,131,132,133,134',
"dataplacement": "streams",
"output-format": "json",
"plid_select": "roundrobin",
},
"test_class": StreamsTestRR,
},
# explicitly select random
{
"test_id": 23,
"fio_opts": {
"rw": 'write',
"bs": 4096,
"io_size": 8192,
"plids": '1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16',
"dataplacement": "streams",
"output-format": "json",
"plid_select": "random",
},
"test_class": StreamsTestRand,
},
# Error case with placement ID > 0xFFFF
{
"test_id": 30,
"fio_opts": {
"rw": 'write',
"bs": 4096,
"io_size": 8192,
"plids": "1,2,3,0x10000",
"dataplacement": "streams",
"output-format": "normal",
"plid_select": "random",
},
"test_class": StreamsTestRand,
"success": SUCCESS_NONZERO,
},
# Error case with no stream IDs provided
{
"test_id": 31,
"fio_opts": {
"rw": 'write',
"bs": 4096,
"io_size": 8192,
"dataplacement": "streams",
"output-format": "normal",
},
"test_class": StreamsTestRand,
"success": SUCCESS_NONZERO,
},
]
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-streams-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
release_all_streams(args.dut)
test_env = {
'fio_path': fio_path,
'fio_root': str(Path(__file__).absolute().parent.parent),
'artifact_root': artifact_root,
'basename': 'nvmept-streams',
}
_, failed, _ = run_fio_tests(TEST_LIST, test_env, args)
sys.exit(failed)
if __name__ == '__main__':
main()