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 7desc = """ 8Generate linear IO cost model coefficients used by the blk-iocost 9controller. If the target raw testdev is specified, destructive tests 10are 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 14On high performance devices, --numjobs > 1 is needed to achieve 15saturation. 16 17See Documentation/admin-guide/cgroup-v2.rst and block/blk-iocost.c 18for more details. 19""" 20 21import argparse 22import re 23import json 24import glob 25import os 26import sys 27import atexit 28import shutil 29import tempfile 30import subprocess 31 32parser = argparse.ArgumentParser(description=desc, 33 formatter_class=argparse.RawTextHelpFormatter) 34parser.add_argument('--testdev', metavar='DEV', 35 help='Raw block device to use for testing, ignores --testfile-size') 36parser.add_argument('--testfile-size-gb', type=float, metavar='GIGABYTES', default=16, 37 help='Testfile size in gigabytes (default: %(default)s)') 38parser.add_argument('--duration', type=int, metavar='SECONDS', default=120, 39 help='Individual test run duration in seconds (default: %(default)s)') 40parser.add_argument('--seqio-block-mb', metavar='MEGABYTES', type=int, default=128, 41 help='Sequential test block size in megabytes (default: %(default)s)') 42parser.add_argument('--seq-depth', type=int, metavar='DEPTH', default=64, 43 help='Sequential test queue depth (default: %(default)s)') 44parser.add_argument('--rand-depth', type=int, metavar='DEPTH', default=64, 45 help='Random test queue depth (default: %(default)s)') 46parser.add_argument('--numjobs', type=int, metavar='JOBS', default=1, 47 help='Number of parallel fio jobs to run (default: %(default)s)') 48parser.add_argument('--quiet', action='store_true') 49parser.add_argument('--verbose', action='store_true') 50 51def info(msg): 52 if not args.quiet: 53 print(msg) 54 55def dbg(msg): 56 if args.verbose and not args.quiet: 57 print(msg) 58 59# determine ('DEVNAME', 'MAJ:MIN') for @path 60def 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 73def 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 89def 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 106def 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 116args = parser.parse_args() 117 118missing = False 119for 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 123if missing: 124 sys.exit(1) 125 126if 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})') 132else: 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 139elevator_path = f'/sys/block/{devname}/queue/scheduler' 140nomerges_path = f'/sys/block/{devname}/queue/nomerges' 141 142with open(elevator_path, 'r') as f: 143 elevator = re.sub(r'.*\[(.*)\].*', r'\1', f.read().strip()) 144with open(nomerges_path, 'r') as f: 145 nomerges = f.read().strip() 146 147info(f'Temporarily disabling elevator and merges') 148atexit.register(restore_elevator_nomerges) 149with open(elevator_path, 'w') as f: 150 f.write('none') 151with open(nomerges_path, 'w') as f: 152 f.write('1') 153 154info('Determining rbps...') 155rbps = run_fio(testfile, args.duration, 'read', 156 1, args.seqio_block_mb * (2 ** 20), args.numjobs) 157info(f'\nrbps={rbps}, determining rseqiops...') 158rseqiops = round(run_fio(testfile, args.duration, 'read', 159 args.seq_depth, 4096, args.numjobs) / 4096) 160info(f'\nrseqiops={rseqiops}, determining rrandiops...') 161rrandiops = round(run_fio(testfile, args.duration, 'randread', 162 args.rand_depth, 4096, args.numjobs) / 4096) 163info(f'\nrrandiops={rrandiops}, determining wbps...') 164wbps = run_fio(testfile, args.duration, 'write', 165 1, args.seqio_block_mb * (2 ** 20), args.numjobs) 166info(f'\nwbps={wbps}, determining wseqiops...') 167wseqiops = round(run_fio(testfile, args.duration, 'write', 168 args.seq_depth, 4096, args.numjobs) / 4096) 169info(f'\nwseqiops={wseqiops}, determining wrandiops...') 170wrandiops = round(run_fio(testfile, args.duration, 'randwrite', 171 args.rand_depth, 4096, args.numjobs) / 4096) 172info(f'\nwrandiops={wrandiops}') 173restore_elevator_nomerges() 174atexit.unregister(restore_elevator_nomerges) 175info('') 176 177print(f'{devno} rbps={rbps} rseqiops={rseqiops} rrandiops={rrandiops} ' 178 f'wbps={wbps} wseqiops={wseqiops} wrandiops={wrandiops}') 179