blob: 4f7cbcfa148bc64ff16209ba9ee7bd3a08843b34 [file] [log] [blame]
koder aka kdanilov4af80852015-02-01 23:36:38 +02001import re
2import os
3import sys
4import stat
5import time
6import json
7import os.path
8import argparse
9import warnings
10import subprocess
11
12
13class BenchmarkOption(object):
14 def __init__(self, concurence, iodepth, action, blocksize, size):
15 self.iodepth = iodepth
16 self.action = action
17 self.blocksize = blocksize
18 self.concurence = concurence
19 self.size = size
20 self.direct_io = False
21 self.use_hight_io_priority = True
22 self.sync = False
23
24
koder aka kdanilov98615bf2015-02-02 00:59:07 +020025def which(program):
26 def is_exe(fpath):
27 return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
28
29 fpath, fname = os.path.split(program)
30 if fpath:
31 if is_exe(program):
32 return program
33 else:
34 for path in os.environ["PATH"].split(os.pathsep):
35 path = path.strip('"')
36 exe_file = os.path.join(path, program)
37 if is_exe(exe_file):
38 return exe_file
39
40 return None
41
42
43# ------------------------------ IOZONE SUPPORT ------------------------------
44
45
koder aka kdanilov4af80852015-02-01 23:36:38 +020046class IOZoneParser(object):
47 "class to parse iozone results"
48
49 start_tests = re.compile(r"^\s+KB\s+reclen\s+")
50 resuts = re.compile(r"[\s0-9]+")
51 mt_iozone_re = re.compile(r"\s+Children see throughput " +
52 r"for\s+\d+\s+(?P<cmd>.*?)\s+=\s+" +
53 r"(?P<perf>[\d.]+)\s+KB/sec")
54
55 cmap = {'initial writers': 'write',
56 'rewriters': 'rewrite',
57 'initial readers': 'read',
58 're-readers': 'reread',
59 'random readers': 'random read',
60 'random writers': 'random write'}
61
62 string1 = " " + \
63 " random random " + \
64 "bkwd record stride "
65
66 string2 = "KB reclen write rewrite " + \
67 "read reread read write " + \
68 "read rewrite read fwrite frewrite fread freread"
69
70 @classmethod
71 def apply_parts(cls, parts, string, sep=' \t\n'):
72 add_offset = 0
73 for part in parts:
74 _, start, stop = part
75 start += add_offset
76 add_offset = 0
77
78 # condition splited to make pylint happy
79 while stop + add_offset < len(string):
80
81 # condition splited to make pylint happy
82 if not (string[stop + add_offset] not in sep):
83 break
84
85 add_offset += 1
86
87 yield part, string[start:stop + add_offset]
88
89 @classmethod
90 def make_positions(cls):
91 items = [i for i in cls.string2.split() if i]
92
93 pos = 0
94 cls.positions = []
95
96 for item in items:
97 npos = cls.string2.index(item, 0 if pos == 0 else pos + 1)
98 cls.positions.append([item, pos, npos + len(item)])
99 pos = npos + len(item)
100
101 for itm, val in cls.apply_parts(cls.positions, cls.string1):
102 if val.strip():
103 itm[0] = val.strip() + " " + itm[0]
104
105 @classmethod
106 def parse_iozone_res(cls, res, mthreads=False):
107 parsed_res = None
108
109 sres = res.split('\n')
110
111 if not mthreads:
112 for pos, line in enumerate(sres[1:]):
113 if line.strip() == cls.string2 and \
114 sres[pos].strip() == cls.string1.strip():
115 add_pos = line.index(cls.string2)
116 parsed_res = {}
117
118 npos = [(name, start + add_pos, stop + add_pos)
119 for name, start, stop in cls.positions]
120
121 for itm, res in cls.apply_parts(npos, sres[pos + 2]):
122 if res.strip() != '':
123 parsed_res[itm[0]] = int(res.strip())
124
125 del parsed_res['KB']
126 del parsed_res['reclen']
127 else:
128 parsed_res = {}
129 for line in sres:
130 rr = cls.mt_iozone_re.match(line)
131 if rr is not None:
132 cmd = rr.group('cmd')
133 key = cls.cmap.get(cmd, cmd)
134 perf = int(float(rr.group('perf')))
135 parsed_res[key] = perf
136 return parsed_res
137
138
koder aka kdanilov4af80852015-02-01 23:36:38 +0200139IOZoneParser.make_positions()
140
141
koder aka kdanilov3f356262015-02-13 08:06:14 -0800142def iozone_do_prepare(params, filename, pattern_base):
koder aka kdanilov4af80852015-02-01 23:36:38 +0200143 all_files = []
144 threads = int(params.concurence)
145 if 1 != threads:
koder aka kdanilov3f356262015-02-13 08:06:14 -0800146 filename = filename + "_{0}"
147 all_files.extend(filename .format(i) for i in range(threads))
koder aka kdanilov4af80852015-02-01 23:36:38 +0200148 else:
koder aka kdanilov4af80852015-02-01 23:36:38 +0200149 all_files.append(filename)
150
151 bsz = 1024 if params.size > 1024 else params.size
152 if params.size % bsz != 0:
153 fsz = (params.size // bsz + 1) * bsz
154 else:
155 fsz = params.size
156
157 for fname in all_files:
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200158 with open(fname, "wb") as fd:
159 if fsz > 1024:
koder aka kdanilov3f356262015-02-13 08:06:14 -0800160 pattern = pattern_base * 1024 * 1024
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200161 for _ in range(int(fsz / 1024) + 1):
162 fd.write(pattern)
163 else:
koder aka kdanilov3f356262015-02-13 08:06:14 -0800164 fd.write(pattern_base * 1024 * fsz)
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200165 fd.flush()
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800166 return all_files
167
168
169VERIFY_PATTERN = "\x6d"
170
171
172def do_run_iozone(params, filename, timeout, iozone_path='iozone',
173 microsecond_mode=False,
174 prepare_only=False):
175
176 cmd = [iozone_path, "-V", str(ord(VERIFY_PATTERN))]
177
178 if params.sync:
179 cmd.append('-o')
180
181 if params.direct_io:
182 cmd.append('-I')
183
184 if microsecond_mode:
185 cmd.append('-N')
186
187 all_files = iozone_do_prepare(params, filename, VERIFY_PATTERN)
188
189 threads = int(params.concurence)
190 if 1 != threads:
191 cmd.extend(('-t', str(threads), '-F'))
192 cmd.extend(all_files)
193 else:
194 cmd.extend(('-f', all_files[0]))
koder aka kdanilov4af80852015-02-01 23:36:38 +0200195
196 cmd.append('-i')
197
198 if params.action == 'write':
199 cmd.append("0")
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200200 elif params.action == 'read':
201 cmd.append("1")
202 elif params.action == 'randwrite' or params.action == 'randread':
koder aka kdanilov4af80852015-02-01 23:36:38 +0200203 cmd.append("2")
204 else:
205 raise ValueError("Unknown action {0!r}".format(params.action))
206
207 cmd.extend(('-s', str(params.size)))
208 cmd.extend(('-r', str(params.blocksize)))
209
koder aka kdanilov78ba8952015-02-03 01:11:23 +0200210 # no retest
211 cmd.append('-+n')
212
koder aka kdanilov4af80852015-02-01 23:36:38 +0200213 raw_res = subprocess.check_output(cmd)
214
215 try:
216 parsed_res = IOZoneParser.parse_iozone_res(raw_res, threads > 1)
217
218 res = {}
219
220 if params.action == 'write':
221 res['bw_mean'] = parsed_res['write']
222 elif params.action == 'randwrite':
223 res['bw_mean'] = parsed_res['random write']
224 elif params.action == 'read':
225 res['bw_mean'] = parsed_res['read']
226 elif params.action == 'randread':
227 res['bw_mean'] = parsed_res['random read']
228 except:
koder aka kdanilov4af80852015-02-01 23:36:38 +0200229 raise
230
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200231 return res, " ".join(cmd)
koder aka kdanilov4af80852015-02-01 23:36:38 +0200232
233
koder aka kdanilov3f356262015-02-13 08:06:14 -0800234def run_iozone(benchmark, iozone_path, tmpname,
235 prepare_only=False,
236 timeout=None):
237
koder aka kdanilov4af80852015-02-01 23:36:38 +0200238 if timeout is not None:
239 benchmark.size = benchmark.blocksize * 50
240 res_time = do_run_iozone(benchmark, tmpname, timeout,
241 iozone_path=iozone_path,
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200242 microsecond_mode=True)[0]
koder aka kdanilov4af80852015-02-01 23:36:38 +0200243
244 size = (benchmark.blocksize * timeout * 1000000)
245 size /= res_time["bw_mean"]
246 size = (size // benchmark.blocksize + 1) * benchmark.blocksize
247 benchmark.size = size
248
249 return do_run_iozone(benchmark, tmpname, timeout,
koder aka kdanilov3f356262015-02-13 08:06:14 -0800250 iozone_path=iozone_path, prepare_only=prepare_only)
koder aka kdanilov4af80852015-02-01 23:36:38 +0200251
252
koder aka kdanilov4af80852015-02-01 23:36:38 +0200253def install_iozone_package():
254 if which('iozone'):
255 return
256
257 is_redhat = os.path.exists('/etc/centos-release')
258 is_redhat = is_redhat or os.path.exists('/etc/fedora-release')
259 is_redhat = is_redhat or os.path.exists('/etc/redhat-release')
260
261 if is_redhat:
262 subprocess.check_output(["yum", "install", 'iozone3'])
263 return
264
265 try:
266 os_release_cont = open('/etc/os-release').read()
267
268 is_ubuntu = "Ubuntu" in os_release_cont
269
270 if is_ubuntu or "Debian GNU/Linux" in os_release_cont:
271 subprocess.check_output(["apt-get", "install", "iozone3"])
272 return
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200273 except (IOError, OSError):
koder aka kdanilov4af80852015-02-01 23:36:38 +0200274 pass
275
276 raise RuntimeError("Unknown host OS.")
277
278
279def install_iozone_static(iozone_url, dst):
280 if not os.path.isfile(dst):
281 import urllib
282 urllib.urlretrieve(iozone_url, dst)
283
284 st = os.stat(dst)
285 os.chmod(dst, st.st_mode | stat.S_IEXEC)
286
287
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200288def locate_iozone():
289 binary_path = which('iozone')
290
291 if binary_path is None:
292 binary_path = which('iozone3')
293
294 if binary_path is None:
295 sys.stderr.write("Can't found neither iozone not iozone3 binary"
Kostiantyn Danylov aka koder02adc1d2015-02-02 01:10:04 +0200296 "Provide --bonary-path or --binary-url option")
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200297 return False, None
298
299 return False, binary_path
300
301# ------------------------------ FIO SUPPORT ---------------------------------
302
303
304def run_fio_once(benchmark, fio_path, tmpname, timeout=None):
koder aka kdanilov3f356262015-02-13 08:06:14 -0800305 if benchmark.size is None:
306 raise ValueError("You need to specify file size for fio")
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200307
308 cmd_line = [fio_path,
309 "--name=%s" % benchmark.action,
310 "--rw=%s" % benchmark.action,
311 "--blocksize=%sk" % benchmark.blocksize,
312 "--iodepth=%d" % benchmark.iodepth,
313 "--filename=%s" % tmpname,
314 "--size={0}k".format(benchmark.size),
315 "--numjobs={0}".format(benchmark.concurence),
316 "--output-format=json",
317 "--sync=" + ('1' if benchmark.sync else '0')]
318
319 if timeout is not None:
320 cmd_line.append("--timeout=%d" % timeout)
321 cmd_line.append("--runtime=%d" % timeout)
322
323 if benchmark.direct_io:
324 cmd_line.append("--direct=1")
325
326 if benchmark.use_hight_io_priority:
327 cmd_line.append("--prio=0")
328
329 raw_out = subprocess.check_output(cmd_line)
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200330 return json.loads(raw_out)["jobs"][0], " ".join(cmd_line)
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200331
332
koder aka kdanilov3f356262015-02-13 08:06:14 -0800333def run_fio(benchmark, fio_path, tmpname, prepare_only=False, timeout=None):
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800334 if prepare_only:
335 return {}, ""
336
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200337 job_output, cmd_line = run_fio_once(benchmark, fio_path, tmpname, timeout)
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200338
339 if benchmark.action in ('write', 'randwrite'):
340 raw_result = job_output['write']
341 else:
342 raw_result = job_output['read']
343
344 res = {}
koder aka kdanilov78ba8952015-02-03 01:11:23 +0200345
346 # 'bw_dev bw_mean bw_max bw_min'.split()
347 for field in ["bw_mean"]:
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200348 res[field] = raw_result[field]
349
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200350 return res, cmd_line
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200351
352
353def locate_fio():
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200354 return False, which('fio')
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200355
356
357# ----------------------------------------------------------------------------
358
359
360def locate_binary(binary_tp, binary_url, binary_path):
361 remove_binary = False
362
363 if binary_url is not None:
364 if binary_path is not None:
365 sys.stderr.write("At most one option from --binary-path and "
366 "--binary-url should be provided")
367 return False, None
368
369 binary_path = os.tmpnam()
370 install_iozone_static(binary_url, binary_path)
371 remove_binary = True
372
373 elif binary_path is not None:
374 if os.path.isfile(binary_path):
375 if not os.access(binary_path, os.X_OK):
376 st = os.stat(binary_path)
377 os.chmod(binary_path, st.st_mode | stat.S_IEXEC)
378 else:
379 binary_path = None
380
381 if binary_path is not None:
382 return remove_binary, binary_path
383
384 if 'iozone' == binary_tp:
385 return locate_iozone()
386 else:
387 return locate_fio()
388
389
390def run_benchmark(binary_tp, *argv, **kwargs):
391 if 'iozone' == binary_tp:
392 return run_iozone(*argv, **kwargs)
393 else:
394 return run_fio(*argv, **kwargs)
395
396
koder aka kdanilov4af80852015-02-01 23:36:38 +0200397def type_size(string):
398 try:
399 return re.match("\d+[KGBM]?", string, re.I).group(0)
400 except:
401 msg = "{0!r} don't looks like size-description string".format(string)
402 raise ValueError(msg)
403
404
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200405def type_size_ext(string):
406 if string.startswith("x"):
407 int(string[1:])
408 return string
409
410 if string.startswith("r"):
411 int(string[1:])
412 return string
413
414 try:
415 return re.match("\d+[KGBM]?", string, re.I).group(0)
416 except:
417 msg = "{0!r} don't looks like size-description string".format(string)
418 raise ValueError(msg)
419
420
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200421def ssize_to_kb(ssize):
422 try:
423 smap = dict(k=1, K=1, M=1024, m=1024, G=1024**2, g=1024**2)
424 for ext, coef in smap.items():
425 if ssize.endswith(ext):
426 return int(ssize[:-1]) * coef
427
428 if int(ssize) % 1024 != 0:
429 raise ValueError()
430
431 return int(ssize) / 1024
432
433 except (ValueError, TypeError, AttributeError):
434 tmpl = "Unknow size format {0!r} (or size not multiples 1024)"
435 raise ValueError(tmpl.format(ssize))
436
437
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200438def get_ram_size():
439 try:
440 with open("/proc/meminfo") as fd:
441 for ln in fd:
442 if "MemTotal:" in ln:
443 sz, kb = ln.split(':')[1].strip().split(" ")
444 assert kb == 'kB'
445 return int(sz)
446 except (ValueError, TypeError, AssertionError):
447 raise
448 # return None
449
450
koder aka kdanilov4af80852015-02-01 23:36:38 +0200451def parse_args(argv):
452 parser = argparse.ArgumentParser(
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200453 description="Run 'iozone' or 'fio' and return result")
454 parser.add_argument(
455 "--type", metavar="BINARY_TYPE",
456 choices=['iozone', 'fio'], required=True)
koder aka kdanilov4af80852015-02-01 23:36:38 +0200457 parser.add_argument(
458 "--iodepth", metavar="IODEPTH", type=int,
459 help="I/O depths to test in kb", required=True)
460 parser.add_argument(
461 '-a', "--action", metavar="ACTION", type=str,
462 help="actions to run", required=True,
463 choices=["read", "write", "randread", "randwrite"])
464 parser.add_argument(
465 "--blocksize", metavar="BLOCKSIZE", type=type_size,
466 help="single operation block size", required=True)
467 parser.add_argument(
468 "--timeout", metavar="TIMEOUT", type=int,
469 help="runtime of a single run", default=None)
470 parser.add_argument(
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200471 "--iosize", metavar="SIZE", type=type_size_ext,
koder aka kdanilov4af80852015-02-01 23:36:38 +0200472 help="file size", default=None)
473 parser.add_argument(
474 "-s", "--sync", default=False, action="store_true",
475 help="exec sync after each write")
476 parser.add_argument(
477 "-d", "--direct-io", default=False, action="store_true",
478 help="use O_DIRECT", dest='directio')
479 parser.add_argument(
480 "-t", "--sync-time", default=None, type=int,
481 help="sleep till sime utc time", dest='sync_time')
482 parser.add_argument(
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200483 "--binary-url", help="static binary url",
484 dest="binary_url", default=None)
koder aka kdanilov4af80852015-02-01 23:36:38 +0200485 parser.add_argument(
486 "--test-file", help="file path to run test on",
487 default=None, dest='test_file')
488 parser.add_argument(
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200489 "--binary-path", help="binary path",
490 default=None, dest='binary_path')
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800491 parser.add_argument(
492 "--prepare-only", default=False, dest='prepare_only',
493 action="store_true")
koder aka kdanilov3f356262015-02-13 08:06:14 -0800494 parser.add_argument("--concurrency", default=1, type=int)
koder aka kdanilov4af80852015-02-01 23:36:38 +0200495 return parser.parse_args(argv)
496
497
498def main(argv):
499 argv_obj = parse_args(argv)
500 argv_obj.blocksize = ssize_to_kb(argv_obj.blocksize)
501
502 if argv_obj.iosize is not None:
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200503 if argv_obj.iosize.startswith('x'):
504 argv_obj.iosize = argv_obj.blocksize * int(argv_obj.iosize[1:])
505 elif argv_obj.iosize.startswith('r'):
506 rs = get_ram_size()
507 if rs is None:
508 sys.stderr.write("Can't determine ram size\n")
509 exit(1)
510 argv_obj.iosize = rs * int(argv_obj.iosize[1:])
511 else:
512 argv_obj.iosize = ssize_to_kb(argv_obj.iosize)
koder aka kdanilov4af80852015-02-01 23:36:38 +0200513
koder aka kdanilov3f356262015-02-13 08:06:14 -0800514 benchmark = BenchmarkOption(argv_obj.concurrency,
koder aka kdanilov4af80852015-02-01 23:36:38 +0200515 argv_obj.iodepth,
516 argv_obj.action,
517 argv_obj.blocksize,
518 argv_obj.iosize)
519
520 benchmark.direct_io = argv_obj.directio
521
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200522 if argv_obj.sync:
523 benchmark.sync = True
524
koder aka kdanilov4af80852015-02-01 23:36:38 +0200525 test_file_name = argv_obj.test_file
526 if test_file_name is None:
527 with warnings.catch_warnings():
528 warnings.simplefilter("ignore")
529 test_file_name = os.tmpnam()
530
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200531 remove_binary, binary_path = locate_binary(argv_obj.type,
532 argv_obj.binary_url,
533 argv_obj.binary_path)
koder aka kdanilov4af80852015-02-01 23:36:38 +0200534
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200535 if binary_path is None:
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200536 sys.stderr.write("Can't locate binary {0}\n".format(argv_obj.type))
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200537 return 1
koder aka kdanilov4af80852015-02-01 23:36:38 +0200538
539 try:
540 if argv_obj.sync_time is not None:
541 dt = argv_obj.sync_time - time.time()
542 if dt > 0:
543 time.sleep(dt)
544
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200545 res, cmd = run_benchmark(argv_obj.type,
546 benchmark,
547 binary_path,
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800548 test_file_name,
koder aka kdanilov3f356262015-02-13 08:06:14 -0800549 argv_obj.prepare_only,
550 argv_obj.timeout)
551
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800552 if not argv_obj.prepare_only:
553 res['__meta__'] = benchmark.__dict__.copy()
554 res['__meta__']['cmdline'] = cmd
555
556 sys.stdout.write(json.dumps(res))
557
558 if not argv_obj.prepare_only:
559 sys.stdout.write("\n")
560
koder aka kdanilov4af80852015-02-01 23:36:38 +0200561 finally:
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200562 if remove_binary:
563 os.unlink(binary_path)
koder aka kdanilov4af80852015-02-01 23:36:38 +0200564
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800565 if os.path.isfile(test_file_name) and not argv_obj.prepare_only:
koder aka kdanilov4af80852015-02-01 23:36:38 +0200566 os.unlink(test_file_name)
567
568
koder aka kdanilov4af80852015-02-01 23:36:38 +0200569if __name__ == '__main__':
koder aka kdanilov4643fd62015-02-10 16:20:13 -0800570 exit(main(sys.argv[1:]))