iocost_coef_gen.py 6.8 KB

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