|  | #!/usr/bin/env python3 | 
|  | # | 
|  | # Copyright (C) 2019 Tejun Heo <[email protected]> | 
|  | # Copyright (C) 2019 Andy Newell <[email protected]> | 
|  | # Copyright (C) 2019 Facebook | 
|  |  | 
|  | desc = """ | 
|  | Generate linear IO cost model coefficients used by the blk-iocost | 
|  | controller.  If the target raw testdev is specified, destructive tests | 
|  | are performed against the whole device; otherwise, on | 
|  | ./iocost-coef-fio.testfile.  The result can be written directly to | 
|  | /sys/fs/cgroup/io.cost.model. | 
|  |  | 
|  | On high performance devices, --numjobs > 1 is needed to achieve | 
|  | saturation. | 
|  |  | 
|  | See Documentation/admin-guide/cgroup-v2.rst and block/blk-iocost.c | 
|  | for more details. | 
|  | """ | 
|  |  | 
|  | import argparse | 
|  | import re | 
|  | import json | 
|  | import glob | 
|  | import os | 
|  | import sys | 
|  | import atexit | 
|  | import shutil | 
|  | import tempfile | 
|  | import subprocess | 
|  |  | 
|  | parser = argparse.ArgumentParser(description=desc, | 
|  | formatter_class=argparse.RawTextHelpFormatter) | 
|  | parser.add_argument('--testdev', metavar='DEV', | 
|  | help='Raw block device to use for testing, ignores --testfile-size') | 
|  | parser.add_argument('--testfile-size-gb', type=float, metavar='GIGABYTES', default=16, | 
|  | help='Testfile size in gigabytes (default: %(default)s)') | 
|  | parser.add_argument('--duration', type=int, metavar='SECONDS', default=120, | 
|  | help='Individual test run duration in seconds (default: %(default)s)') | 
|  | parser.add_argument('--seqio-block-mb', metavar='MEGABYTES', type=int, default=128, | 
|  | help='Sequential test block size in megabytes (default: %(default)s)') | 
|  | parser.add_argument('--seq-depth', type=int, metavar='DEPTH', default=64, | 
|  | help='Sequential test queue depth (default: %(default)s)') | 
|  | parser.add_argument('--rand-depth', type=int, metavar='DEPTH', default=64, | 
|  | help='Random test queue depth (default: %(default)s)') | 
|  | parser.add_argument('--numjobs', type=int, metavar='JOBS', default=1, | 
|  | help='Number of parallel fio jobs to run (default: %(default)s)') | 
|  | parser.add_argument('--quiet', action='store_true') | 
|  | parser.add_argument('--verbose', action='store_true') | 
|  |  | 
|  | def info(msg): | 
|  | if not args.quiet: | 
|  | print(msg) | 
|  |  | 
|  | def dbg(msg): | 
|  | if args.verbose and not args.quiet: | 
|  | print(msg) | 
|  |  | 
|  | # determine ('DEVNAME', 'MAJ:MIN') for @path | 
|  | def dir_to_dev(path): | 
|  | # find the block device the current directory is on | 
|  | devname = subprocess.run(f'findmnt -nvo SOURCE -T{path}', | 
|  | stdout=subprocess.PIPE, shell=True).stdout | 
|  | devname = os.path.basename(devname).decode('utf-8').strip() | 
|  |  | 
|  | # partition -> whole device | 
|  | parents = glob.glob('/sys/block/*/' + devname) | 
|  | if len(parents): | 
|  | devname = os.path.basename(os.path.dirname(parents[0])) | 
|  | rdev = os.stat(f'/dev/{devname}').st_rdev | 
|  | return (devname, f'{os.major(rdev)}:{os.minor(rdev)}') | 
|  |  | 
|  | def create_testfile(path, size): | 
|  | global args | 
|  |  | 
|  | if os.path.isfile(path) and os.stat(path).st_size == size: | 
|  | return | 
|  |  | 
|  | info(f'Creating testfile {path}') | 
|  | subprocess.check_call(f'rm -f {path}', shell=True) | 
|  | subprocess.check_call(f'touch {path}', shell=True) | 
|  | subprocess.call(f'chattr +C {path}', shell=True) | 
|  | subprocess.check_call( | 
|  | f'pv -s {size} -pr /dev/urandom {"-q" if args.quiet else ""} | ' | 
|  | f'dd of={path} count={size} ' | 
|  | f'iflag=count_bytes,fullblock oflag=direct bs=16M status=none', | 
|  | shell=True) | 
|  |  | 
|  | def run_fio(testfile, duration, iotype, iodepth, blocksize, jobs): | 
|  | global args | 
|  |  | 
|  | eta = 'never' if args.quiet else 'always' | 
|  | outfile = tempfile.NamedTemporaryFile() | 
|  | cmd = (f'fio --direct=1 --ioengine=libaio --name=coef ' | 
|  | f'--filename={testfile} --runtime={round(duration)} ' | 
|  | f'--readwrite={iotype} --iodepth={iodepth} --blocksize={blocksize} ' | 
|  | f'--eta={eta} --output-format json --output={outfile.name} ' | 
|  | f'--time_based --numjobs={jobs}') | 
|  | if args.verbose: | 
|  | dbg(f'Running {cmd}') | 
|  | subprocess.check_call(cmd, shell=True) | 
|  | with open(outfile.name, 'r') as f: | 
|  | d = json.loads(f.read()) | 
|  | return sum(j['read']['bw_bytes'] + j['write']['bw_bytes'] for j in d['jobs']) | 
|  |  | 
|  | def restore_elevator_nomerges(): | 
|  | global elevator_path, nomerges_path, elevator, nomerges | 
|  |  | 
|  | info(f'Restoring elevator to {elevator} and nomerges to {nomerges}') | 
|  | with open(elevator_path, 'w') as f: | 
|  | f.write(elevator) | 
|  | with open(nomerges_path, 'w') as f: | 
|  | f.write(nomerges) | 
|  |  | 
|  |  | 
|  | args = parser.parse_args() | 
|  |  | 
|  | missing = False | 
|  | for cmd in [ 'findmnt', 'pv', 'dd', 'fio' ]: | 
|  | if not shutil.which(cmd): | 
|  | print(f'Required command "{cmd}" is missing', file=sys.stderr) | 
|  | missing = True | 
|  | if missing: | 
|  | sys.exit(1) | 
|  |  | 
|  | if args.testdev: | 
|  | devname = os.path.basename(args.testdev) | 
|  | rdev = os.stat(f'/dev/{devname}').st_rdev | 
|  | devno = f'{os.major(rdev)}:{os.minor(rdev)}' | 
|  | testfile = f'/dev/{devname}' | 
|  | info(f'Test target: {devname}({devno})') | 
|  | else: | 
|  | devname, devno = dir_to_dev('.') | 
|  | testfile = 'iocost-coef-fio.testfile' | 
|  | testfile_size = int(args.testfile_size_gb * 2 ** 30) | 
|  | create_testfile(testfile, testfile_size) | 
|  | info(f'Test target: {testfile} on {devname}({devno})') | 
|  |  | 
|  | elevator_path = f'/sys/block/{devname}/queue/scheduler' | 
|  | nomerges_path = f'/sys/block/{devname}/queue/nomerges' | 
|  |  | 
|  | with open(elevator_path, 'r') as f: | 
|  | elevator = re.sub(r'.*\[(.*)\].*', r'\1', f.read().strip()) | 
|  | with open(nomerges_path, 'r') as f: | 
|  | nomerges = f.read().strip() | 
|  |  | 
|  | info(f'Temporarily disabling elevator and merges') | 
|  | atexit.register(restore_elevator_nomerges) | 
|  | with open(elevator_path, 'w') as f: | 
|  | f.write('none') | 
|  | with open(nomerges_path, 'w') as f: | 
|  | f.write('1') | 
|  |  | 
|  | info('Determining rbps...') | 
|  | rbps = run_fio(testfile, args.duration, 'read', | 
|  | 1, args.seqio_block_mb * (2 ** 20), args.numjobs) | 
|  | info(f'\nrbps={rbps}, determining rseqiops...') | 
|  | rseqiops = round(run_fio(testfile, args.duration, 'read', | 
|  | args.seq_depth, 4096, args.numjobs) / 4096) | 
|  | info(f'\nrseqiops={rseqiops}, determining rrandiops...') | 
|  | rrandiops = round(run_fio(testfile, args.duration, 'randread', | 
|  | args.rand_depth, 4096, args.numjobs) / 4096) | 
|  | info(f'\nrrandiops={rrandiops}, determining wbps...') | 
|  | wbps = run_fio(testfile, args.duration, 'write', | 
|  | 1, args.seqio_block_mb * (2 ** 20), args.numjobs) | 
|  | info(f'\nwbps={wbps}, determining wseqiops...') | 
|  | wseqiops = round(run_fio(testfile, args.duration, 'write', | 
|  | args.seq_depth, 4096, args.numjobs) / 4096) | 
|  | info(f'\nwseqiops={wseqiops}, determining wrandiops...') | 
|  | wrandiops = round(run_fio(testfile, args.duration, 'randwrite', | 
|  | args.rand_depth, 4096, args.numjobs) / 4096) | 
|  | info(f'\nwrandiops={wrandiops}') | 
|  | restore_elevator_nomerges() | 
|  | atexit.unregister(restore_elevator_nomerges) | 
|  | info('') | 
|  |  | 
|  | print(f'{devno} rbps={rbps} rseqiops={rseqiops} rrandiops={rrandiops} ' | 
|  | f'wbps={wbps} wseqiops={wseqiops} wrandiops={wrandiops}') |