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