1 #!/usr/bin/env python3 2 # 3 # Copyright (C) 2019 Tejun Heo <tj@kernel.org> 4 # Copyright (C) 2019 Andy Newell <newella@fb.com> 5 # Copyright (C) 2019 Facebook 6 7 desc = """ 8 Generate linear IO cost model coefficients used by the blk-iocost 9 controller. If the target raw testdev is specified, destructive tests 10 are performed against the whole device; otherwise, on 11 ./iocost-coef-fio.testfile. The result can be written directly to 12 /sys/fs/cgroup/io.cost.model. 13 14 On high performance devices, --numjobs > 1 is needed to achieve 15 saturation. 16 17 See Documentation/admin-guide/cgroup-v2.rst and block/blk-iocost.c 18 for more details. 19 """ 20 21 import argparse 22 import re 23 import json 24 import glob 25 import os 26 import sys 27 import atexit 28 import shutil 29 import tempfile 30 import subprocess 31 32 parser = argparse.ArgumentParser(description=desc, 33 formatter_class=argparse.RawTextHelpFormatter) 34 parser.add_argument('--testdev', metavar='DEV', 35 help='Raw block device to use for testing, ignores --testfile-size') 36 parser.add_argument('--testfile-size-gb', type=float, metavar='GIGABYTES', default=16, 37 help='Testfile size in gigabytes (default: %(default)s)') 38 parser.add_argument('--duration', type=int, metavar='SECONDS', default=120, 39 help='Individual test run duration in seconds (default: %(default)s)') 40 parser.add_argument('--seqio-block-mb', metavar='MEGABYTES', type=int, default=128, 41 help='Sequential test block size in megabytes (default: %(default)s)') 42 parser.add_argument('--seq-depth', type=int, metavar='DEPTH', default=64, 43 help='Sequential test queue depth (default: %(default)s)') 44 parser.add_argument('--rand-depth', type=int, metavar='DEPTH', default=64, 45 help='Random test queue depth (default: %(default)s)') 46 parser.add_argument('--numjobs', type=int, metavar='JOBS', default=1, 47 help='Number of parallel fio jobs to run (default: %(default)s)') 48 parser.add_argument('--quiet', action='store_true') 49 parser.add_argument('--verbose', action='store_true') 50 51 def info(msg): 52 if not args.quiet: 53 print(msg) 54 55 def dbg(msg): 56 if args.verbose and not args.quiet: 57 print(msg) 58 59 # determine ('DEVNAME', 'MAJ:MIN') for @path 60 def dir_to_dev(path): 61 # find the block device the current directory is on 62 devname = subprocess.run(f'findmnt -nvo SOURCE -T{path}', 63 stdout=subprocess.PIPE, shell=True).stdout 64 devname = os.path.basename(devname).decode('utf-8').strip() 65 66 # partition -> whole device 67 parents = glob.glob('/sys/block/*/' + devname) 68 if len(parents): 69 devname = os.path.basename(os.path.dirname(parents[0])) 70 rdev = os.stat(f'/dev/{devname}').st_rdev 71 return (devname, f'{os.major(rdev)}:{os.minor(rdev)}') 72 73 def create_testfile(path, size): 74 global args 75 76 if os.path.isfile(path) and os.stat(path).st_size == size: 77 return 78 79 info(f'Creating testfile {path}') 80 subprocess.check_call(f'rm -f {path}', shell=True) 81 subprocess.check_call(f'touch {path}', shell=True) 82 subprocess.call(f'chattr +C {path}', shell=True) 83 subprocess.check_call( 84 f'pv -s {size} -pr /dev/urandom {"-q" if args.quiet else ""} | ' 85 f'dd of={path} count={size} ' 86 f'iflag=count_bytes,fullblock oflag=direct bs=16M status=none', 87 shell=True) 88 89 def run_fio(testfile, duration, iotype, iodepth, blocksize, jobs): 90 global args 91 92 eta = 'never' if args.quiet else 'always' 93 outfile = tempfile.NamedTemporaryFile() 94 cmd = (f'fio --direct=1 --ioengine=libaio --name=coef ' 95 f'--filename={testfile} --runtime={round(duration)} ' 96 f'--readwrite={iotype} --iodepth={iodepth} --blocksize={blocksize} ' 97 f'--eta={eta} --output-format json --output={outfile.name} ' 98 f'--time_based --numjobs={jobs}') 99 if args.verbose: 100 dbg(f'Running {cmd}') 101 subprocess.check_call(cmd, shell=True) 102 with open(outfile.name, 'r') as f: 103 d = json.loads(f.read()) 104 return sum(j['read']['bw_bytes'] + j['write']['bw_bytes'] for j in d['jobs']) 105 106 def restore_elevator_nomerges(): 107 global elevator_path, nomerges_path, elevator, nomerges 108 109 info(f'Restoring elevator to {elevator} and nomerges to {nomerges}') 110 with open(elevator_path, 'w') as f: 111 f.write(elevator) 112 with open(nomerges_path, 'w') as f: 113 f.write(nomerges) 114 115 116 args = parser.parse_args() 117 118 missing = False 119 for cmd in [ 'findmnt', 'pv', 'dd', 'fio' ]: 120 if not shutil.which(cmd): 121 print(f'Required command "{cmd}" is missing', file=sys.stderr) 122 missing = True 123 if missing: 124 sys.exit(1) 125 126 if args.testdev: 127 devname = os.path.basename(args.testdev) 128 rdev = os.stat(f'/dev/{devname}').st_rdev 129 devno = f'{os.major(rdev)}:{os.minor(rdev)}' 130 testfile = f'/dev/{devname}' 131 info(f'Test target: {devname}({devno})') 132 else: 133 devname, devno = dir_to_dev('.') 134 testfile = 'iocost-coef-fio.testfile' 135 testfile_size = int(args.testfile_size_gb * 2 ** 30) 136 create_testfile(testfile, testfile_size) 137 info(f'Test target: {testfile} on {devname}({devno})') 138 139 elevator_path = f'/sys/block/{devname}/queue/scheduler' 140 nomerges_path = f'/sys/block/{devname}/queue/nomerges' 141 142 with open(elevator_path, 'r') as f: 143 elevator = re.sub(r'.*\[(.*)\].*', r'\1', f.read().strip()) 144 with open(nomerges_path, 'r') as f: 145 nomerges = f.read().strip() 146 147 info(f'Temporarily disabling elevator and merges') 148 atexit.register(restore_elevator_nomerges) 149 with open(elevator_path, 'w') as f: 150 f.write('none') 151 with open(nomerges_path, 'w') as f: 152 f.write('1') 153 154 info('Determining rbps...') 155 rbps = run_fio(testfile, args.duration, 'read', 156 1, args.seqio_block_mb * (2 ** 20), args.numjobs) 157 info(f'\nrbps={rbps}, determining rseqiops...') 158 rseqiops = round(run_fio(testfile, args.duration, 'read', 159 args.seq_depth, 4096, args.numjobs) / 4096) 160 info(f'\nrseqiops={rseqiops}, determining rrandiops...') 161 rrandiops = round(run_fio(testfile, args.duration, 'randread', 162 args.rand_depth, 4096, args.numjobs) / 4096) 163 info(f'\nrrandiops={rrandiops}, determining wbps...') 164 wbps = run_fio(testfile, args.duration, 'write', 165 1, args.seqio_block_mb * (2 ** 20), args.numjobs) 166 info(f'\nwbps={wbps}, determining wseqiops...') 167 wseqiops = round(run_fio(testfile, args.duration, 'write', 168 args.seq_depth, 4096, args.numjobs) / 4096) 169 info(f'\nwseqiops={wseqiops}, determining wrandiops...') 170 wrandiops = round(run_fio(testfile, args.duration, 'randwrite', 171 args.rand_depth, 4096, args.numjobs) / 4096) 172 info(f'\nwrandiops={wrandiops}') 173 restore_elevator_nomerges() 174 atexit.unregister(restore_elevator_nomerges) 175 info('') 176 177 print(f'{devno} rbps={rbps} rseqiops={rseqiops} rrandiops={rrandiops} ' 178 f'wbps={wbps} wseqiops={wseqiops} wrandiops={wrandiops}')
Linux® is a registered trademark of Linus Torvalds in the United States and other countries.
TOMOYO® is a registered trademark of NTT DATA CORPORATION.