forked from Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
178 lines
6.8 KiB
178 lines
6.8 KiB
#!/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}')
|
|
|