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