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