123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178 |
- #!/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}')
|