Back to home page

OSCL-LXR

 
 

    


0001 #!/usr/bin/env python3
0002 #
0003 # Copyright (C) 2019 Tejun Heo <tj@kernel.org>
0004 # Copyright (C) 2019 Andy Newell <newella@fb.com>
0005 # Copyright (C) 2019 Facebook
0006 
0007 desc = """
0008 Generate linear IO cost model coefficients used by the blk-iocost
0009 controller.  If the target raw testdev is specified, destructive tests
0010 are performed against the whole device; otherwise, on
0011 ./iocost-coef-fio.testfile.  The result can be written directly to
0012 /sys/fs/cgroup/io.cost.model.
0013 
0014 On high performance devices, --numjobs > 1 is needed to achieve
0015 saturation.
0016 
0017 See Documentation/admin-guide/cgroup-v2.rst and block/blk-iocost.c
0018 for more details.
0019 """
0020 
0021 import argparse
0022 import re
0023 import json
0024 import glob
0025 import os
0026 import sys
0027 import atexit
0028 import shutil
0029 import tempfile
0030 import subprocess
0031 
0032 parser = argparse.ArgumentParser(description=desc,
0033                                  formatter_class=argparse.RawTextHelpFormatter)
0034 parser.add_argument('--testdev', metavar='DEV',
0035                     help='Raw block device to use for testing, ignores --testfile-size')
0036 parser.add_argument('--testfile-size-gb', type=float, metavar='GIGABYTES', default=16,
0037                     help='Testfile size in gigabytes (default: %(default)s)')
0038 parser.add_argument('--duration', type=int, metavar='SECONDS', default=120,
0039                     help='Individual test run duration in seconds (default: %(default)s)')
0040 parser.add_argument('--seqio-block-mb', metavar='MEGABYTES', type=int, default=128,
0041                     help='Sequential test block size in megabytes (default: %(default)s)')
0042 parser.add_argument('--seq-depth', type=int, metavar='DEPTH', default=64,
0043                     help='Sequential test queue depth (default: %(default)s)')
0044 parser.add_argument('--rand-depth', type=int, metavar='DEPTH', default=64,
0045                     help='Random test queue depth (default: %(default)s)')
0046 parser.add_argument('--numjobs', type=int, metavar='JOBS', default=1,
0047                     help='Number of parallel fio jobs to run (default: %(default)s)')
0048 parser.add_argument('--quiet', action='store_true')
0049 parser.add_argument('--verbose', action='store_true')
0050 
0051 def info(msg):
0052     if not args.quiet:
0053         print(msg)
0054 
0055 def dbg(msg):
0056     if args.verbose and not args.quiet:
0057         print(msg)
0058 
0059 # determine ('DEVNAME', 'MAJ:MIN') for @path
0060 def dir_to_dev(path):
0061     # find the block device the current directory is on
0062     devname = subprocess.run(f'findmnt -nvo SOURCE -T{path}',
0063                              stdout=subprocess.PIPE, shell=True).stdout
0064     devname = os.path.basename(devname).decode('utf-8').strip()
0065 
0066     # partition -> whole device
0067     parents = glob.glob('/sys/block/*/' + devname)
0068     if len(parents):
0069         devname = os.path.basename(os.path.dirname(parents[0]))
0070     rdev = os.stat(f'/dev/{devname}').st_rdev
0071     return (devname, f'{os.major(rdev)}:{os.minor(rdev)}')
0072 
0073 def create_testfile(path, size):
0074     global args
0075 
0076     if os.path.isfile(path) and os.stat(path).st_size == size:
0077         return
0078 
0079     info(f'Creating testfile {path}')
0080     subprocess.check_call(f'rm -f {path}', shell=True)
0081     subprocess.check_call(f'touch {path}', shell=True)
0082     subprocess.call(f'chattr +C {path}', shell=True)
0083     subprocess.check_call(
0084         f'pv -s {size} -pr /dev/urandom {"-q" if args.quiet else ""} | '
0085         f'dd of={path} count={size} '
0086         f'iflag=count_bytes,fullblock oflag=direct bs=16M status=none',
0087         shell=True)
0088 
0089 def run_fio(testfile, duration, iotype, iodepth, blocksize, jobs):
0090     global args
0091 
0092     eta = 'never' if args.quiet else 'always'
0093     outfile = tempfile.NamedTemporaryFile()
0094     cmd = (f'fio --direct=1 --ioengine=libaio --name=coef '
0095            f'--filename={testfile} --runtime={round(duration)} '
0096            f'--readwrite={iotype} --iodepth={iodepth} --blocksize={blocksize} '
0097            f'--eta={eta} --output-format json --output={outfile.name} '
0098            f'--time_based --numjobs={jobs}')
0099     if args.verbose:
0100         dbg(f'Running {cmd}')
0101     subprocess.check_call(cmd, shell=True)
0102     with open(outfile.name, 'r') as f:
0103         d = json.loads(f.read())
0104     return sum(j['read']['bw_bytes'] + j['write']['bw_bytes'] for j in d['jobs'])
0105 
0106 def restore_elevator_nomerges():
0107     global elevator_path, nomerges_path, elevator, nomerges
0108 
0109     info(f'Restoring elevator to {elevator} and nomerges to {nomerges}')
0110     with open(elevator_path, 'w') as f:
0111         f.write(elevator)
0112     with open(nomerges_path, 'w') as f:
0113         f.write(nomerges)
0114 
0115 
0116 args = parser.parse_args()
0117 
0118 missing = False
0119 for cmd in [ 'findmnt', 'pv', 'dd', 'fio' ]:
0120     if not shutil.which(cmd):
0121         print(f'Required command "{cmd}" is missing', file=sys.stderr)
0122         missing = True
0123 if missing:
0124     sys.exit(1)
0125 
0126 if args.testdev:
0127     devname = os.path.basename(args.testdev)
0128     rdev = os.stat(f'/dev/{devname}').st_rdev
0129     devno = f'{os.major(rdev)}:{os.minor(rdev)}'
0130     testfile = f'/dev/{devname}'
0131     info(f'Test target: {devname}({devno})')
0132 else:
0133     devname, devno = dir_to_dev('.')
0134     testfile = 'iocost-coef-fio.testfile'
0135     testfile_size = int(args.testfile_size_gb * 2 ** 30)
0136     create_testfile(testfile, testfile_size)
0137     info(f'Test target: {testfile} on {devname}({devno})')
0138 
0139 elevator_path = f'/sys/block/{devname}/queue/scheduler'
0140 nomerges_path = f'/sys/block/{devname}/queue/nomerges'
0141 
0142 with open(elevator_path, 'r') as f:
0143     elevator = re.sub(r'.*\[(.*)\].*', r'\1', f.read().strip())
0144 with open(nomerges_path, 'r') as f:
0145     nomerges = f.read().strip()
0146 
0147 info(f'Temporarily disabling elevator and merges')
0148 atexit.register(restore_elevator_nomerges)
0149 with open(elevator_path, 'w') as f:
0150     f.write('none')
0151 with open(nomerges_path, 'w') as f:
0152     f.write('1')
0153 
0154 info('Determining rbps...')
0155 rbps = run_fio(testfile, args.duration, 'read',
0156                1, args.seqio_block_mb * (2 ** 20), args.numjobs)
0157 info(f'\nrbps={rbps}, determining rseqiops...')
0158 rseqiops = round(run_fio(testfile, args.duration, 'read',
0159                          args.seq_depth, 4096, args.numjobs) / 4096)
0160 info(f'\nrseqiops={rseqiops}, determining rrandiops...')
0161 rrandiops = round(run_fio(testfile, args.duration, 'randread',
0162                           args.rand_depth, 4096, args.numjobs) / 4096)
0163 info(f'\nrrandiops={rrandiops}, determining wbps...')
0164 wbps = run_fio(testfile, args.duration, 'write',
0165                1, args.seqio_block_mb * (2 ** 20), args.numjobs)
0166 info(f'\nwbps={wbps}, determining wseqiops...')
0167 wseqiops = round(run_fio(testfile, args.duration, 'write',
0168                          args.seq_depth, 4096, args.numjobs) / 4096)
0169 info(f'\nwseqiops={wseqiops}, determining wrandiops...')
0170 wrandiops = round(run_fio(testfile, args.duration, 'randwrite',
0171                           args.rand_depth, 4096, args.numjobs) / 4096)
0172 info(f'\nwrandiops={wrandiops}')
0173 restore_elevator_nomerges()
0174 atexit.unregister(restore_elevator_nomerges)
0175 info('')
0176 
0177 print(f'{devno} rbps={rbps} rseqiops={rseqiops} rrandiops={rrandiops} '
0178       f'wbps={wbps} wseqiops={wseqiops} wrandiops={wrandiops}')