0001
0002
0003
0004
0005
0006
0007 desc = """
0008 Generate linear IO cost model coefficients used by the blk-iocost
0009 controller. If the target raw testdev is specified, destructive tests
0010 are performed against the whole device; otherwise, on
0011 ./iocost-coef-fio.testfile. The result can be written directly to
0012 /sys/fs/cgroup/io.cost.model.
0013
0014 On high performance devices, --numjobs > 1 is needed to achieve
0015 saturation.
0016
0017 See Documentation/admin-guide/cgroup-v2.rst and block/blk-iocost.c
0018 for more details.
0019 """
0020
0021 import argparse
0022 import re
0023 import json
0024 import glob
0025 import os
0026 import sys
0027 import atexit
0028 import shutil
0029 import tempfile
0030 import subprocess
0031
0032 parser = argparse.ArgumentParser(description=desc,
0033 formatter_class=argparse.RawTextHelpFormatter)
0034 parser.add_argument('--testdev', metavar='DEV',
0035 help='Raw block device to use for testing, ignores --testfile-size')
0036 parser.add_argument('--testfile-size-gb', type=float, metavar='GIGABYTES', default=16,
0037 help='Testfile size in gigabytes (default: %(default)s)')
0038 parser.add_argument('--duration', type=int, metavar='SECONDS', default=120,
0039 help='Individual test run duration in seconds (default: %(default)s)')
0040 parser.add_argument('--seqio-block-mb', metavar='MEGABYTES', type=int, default=128,
0041 help='Sequential test block size in megabytes (default: %(default)s)')
0042 parser.add_argument('--seq-depth', type=int, metavar='DEPTH', default=64,
0043 help='Sequential test queue depth (default: %(default)s)')
0044 parser.add_argument('--rand-depth', type=int, metavar='DEPTH', default=64,
0045 help='Random test queue depth (default: %(default)s)')
0046 parser.add_argument('--numjobs', type=int, metavar='JOBS', default=1,
0047 help='Number of parallel fio jobs to run (default: %(default)s)')
0048 parser.add_argument('--quiet', action='store_true')
0049 parser.add_argument('--verbose', action='store_true')
0050
0051 def info(msg):
0052 if not args.quiet:
0053 print(msg)
0054
0055 def dbg(msg):
0056 if args.verbose and not args.quiet:
0057 print(msg)
0058
0059
0060 def dir_to_dev(path):
0061
0062 devname = subprocess.run(f'findmnt -nvo SOURCE -T{path}',
0063 stdout=subprocess.PIPE, shell=True).stdout
0064 devname = os.path.basename(devname).decode('utf-8').strip()
0065
0066
0067 parents = glob.glob('/sys/block/*/' + devname)
0068 if len(parents):
0069 devname = os.path.basename(os.path.dirname(parents[0]))
0070 rdev = os.stat(f'/dev/{devname}').st_rdev
0071 return (devname, f'{os.major(rdev)}:{os.minor(rdev)}')
0072
0073 def create_testfile(path, size):
0074 global args
0075
0076 if os.path.isfile(path) and os.stat(path).st_size == size:
0077 return
0078
0079 info(f'Creating testfile {path}')
0080 subprocess.check_call(f'rm -f {path}', shell=True)
0081 subprocess.check_call(f'touch {path}', shell=True)
0082 subprocess.call(f'chattr +C {path}', shell=True)
0083 subprocess.check_call(
0084 f'pv -s {size} -pr /dev/urandom {"-q" if args.quiet else ""} | '
0085 f'dd of={path} count={size} '
0086 f'iflag=count_bytes,fullblock oflag=direct bs=16M status=none',
0087 shell=True)
0088
0089 def run_fio(testfile, duration, iotype, iodepth, blocksize, jobs):
0090 global args
0091
0092 eta = 'never' if args.quiet else 'always'
0093 outfile = tempfile.NamedTemporaryFile()
0094 cmd = (f'fio --direct=1 --ioengine=libaio --name=coef '
0095 f'--filename={testfile} --runtime={round(duration)} '
0096 f'--readwrite={iotype} --iodepth={iodepth} --blocksize={blocksize} '
0097 f'--eta={eta} --output-format json --output={outfile.name} '
0098 f'--time_based --numjobs={jobs}')
0099 if args.verbose:
0100 dbg(f'Running {cmd}')
0101 subprocess.check_call(cmd, shell=True)
0102 with open(outfile.name, 'r') as f:
0103 d = json.loads(f.read())
0104 return sum(j['read']['bw_bytes'] + j['write']['bw_bytes'] for j in d['jobs'])
0105
0106 def restore_elevator_nomerges():
0107 global elevator_path, nomerges_path, elevator, nomerges
0108
0109 info(f'Restoring elevator to {elevator} and nomerges to {nomerges}')
0110 with open(elevator_path, 'w') as f:
0111 f.write(elevator)
0112 with open(nomerges_path, 'w') as f:
0113 f.write(nomerges)
0114
0115
0116 args = parser.parse_args()
0117
0118 missing = False
0119 for cmd in [ 'findmnt', 'pv', 'dd', 'fio' ]:
0120 if not shutil.which(cmd):
0121 print(f'Required command "{cmd}" is missing', file=sys.stderr)
0122 missing = True
0123 if missing:
0124 sys.exit(1)
0125
0126 if args.testdev:
0127 devname = os.path.basename(args.testdev)
0128 rdev = os.stat(f'/dev/{devname}').st_rdev
0129 devno = f'{os.major(rdev)}:{os.minor(rdev)}'
0130 testfile = f'/dev/{devname}'
0131 info(f'Test target: {devname}({devno})')
0132 else:
0133 devname, devno = dir_to_dev('.')
0134 testfile = 'iocost-coef-fio.testfile'
0135 testfile_size = int(args.testfile_size_gb * 2 ** 30)
0136 create_testfile(testfile, testfile_size)
0137 info(f'Test target: {testfile} on {devname}({devno})')
0138
0139 elevator_path = f'/sys/block/{devname}/queue/scheduler'
0140 nomerges_path = f'/sys/block/{devname}/queue/nomerges'
0141
0142 with open(elevator_path, 'r') as f:
0143 elevator = re.sub(r'.*\[(.*)\].*', r'\1', f.read().strip())
0144 with open(nomerges_path, 'r') as f:
0145 nomerges = f.read().strip()
0146
0147 info(f'Temporarily disabling elevator and merges')
0148 atexit.register(restore_elevator_nomerges)
0149 with open(elevator_path, 'w') as f:
0150 f.write('none')
0151 with open(nomerges_path, 'w') as f:
0152 f.write('1')
0153
0154 info('Determining rbps...')
0155 rbps = run_fio(testfile, args.duration, 'read',
0156 1, args.seqio_block_mb * (2 ** 20), args.numjobs)
0157 info(f'\nrbps={rbps}, determining rseqiops...')
0158 rseqiops = round(run_fio(testfile, args.duration, 'read',
0159 args.seq_depth, 4096, args.numjobs) / 4096)
0160 info(f'\nrseqiops={rseqiops}, determining rrandiops...')
0161 rrandiops = round(run_fio(testfile, args.duration, 'randread',
0162 args.rand_depth, 4096, args.numjobs) / 4096)
0163 info(f'\nrrandiops={rrandiops}, determining wbps...')
0164 wbps = run_fio(testfile, args.duration, 'write',
0165 1, args.seqio_block_mb * (2 ** 20), args.numjobs)
0166 info(f'\nwbps={wbps}, determining wseqiops...')
0167 wseqiops = round(run_fio(testfile, args.duration, 'write',
0168 args.seq_depth, 4096, args.numjobs) / 4096)
0169 info(f'\nwseqiops={wseqiops}, determining wrandiops...')
0170 wrandiops = round(run_fio(testfile, args.duration, 'randwrite',
0171 args.rand_depth, 4096, args.numjobs) / 4096)
0172 info(f'\nwrandiops={wrandiops}')
0173 restore_elevator_nomerges()
0174 atexit.unregister(restore_elevator_nomerges)
0175 info('')
0176
0177 print(f'{devno} rbps={rbps} rseqiops={rseqiops} rrandiops={rrandiops} '
0178 f'wbps={wbps} wseqiops={wseqiops} wrandiops={wrandiops}')