blob: 8a3509aedbb980de6673bdf1df4734b3b06811ca [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
142def do_run_iozone(params, filename, timeout, iozone_path='iozone',
143 microsecond_mode=False):
144
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200145 PATTERN = "\x6d"
146
147 cmd = [iozone_path, "-V", "109"]
koder aka kdanilov4af80852015-02-01 23:36:38 +0200148
149 if params.sync:
150 cmd.append('-o')
151
152 if params.direct_io:
153 cmd.append('-I')
154
155 if microsecond_mode:
156 cmd.append('-N')
157
158 all_files = []
159 threads = int(params.concurence)
160 if 1 != threads:
161 cmd.extend(('-t', str(threads), '-F'))
162 filename = filename + "_{}"
163 cmd.extend(filename % i for i in range(threads))
164 all_files.extend(filename % i for i in range(threads))
165 else:
166 cmd.extend(('-f', filename))
167 all_files.append(filename)
168
169 bsz = 1024 if params.size > 1024 else params.size
170 if params.size % bsz != 0:
171 fsz = (params.size // bsz + 1) * bsz
172 else:
173 fsz = params.size
174
175 for fname in all_files:
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200176 with open(fname, "wb") as fd:
177 if fsz > 1024:
178 pattern = PATTERN * 1024 * 1024
179 for _ in range(int(fsz / 1024) + 1):
180 fd.write(pattern)
181 else:
182 fd.write(PATTERN * 1024 * fsz)
183 fd.flush()
koder aka kdanilov4af80852015-02-01 23:36:38 +0200184
185 cmd.append('-i')
186
187 if params.action == 'write':
188 cmd.append("0")
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200189 elif params.action == 'read':
190 cmd.append("1")
191 elif params.action == 'randwrite' or params.action == 'randread':
koder aka kdanilov4af80852015-02-01 23:36:38 +0200192 cmd.append("2")
193 else:
194 raise ValueError("Unknown action {0!r}".format(params.action))
195
196 cmd.extend(('-s', str(params.size)))
197 cmd.extend(('-r', str(params.blocksize)))
198
koder aka kdanilov78ba8952015-02-03 01:11:23 +0200199 # no retest
200 cmd.append('-+n')
201
koder aka kdanilov4af80852015-02-01 23:36:38 +0200202 raw_res = subprocess.check_output(cmd)
203
204 try:
205 parsed_res = IOZoneParser.parse_iozone_res(raw_res, threads > 1)
206
207 res = {}
208
209 if params.action == 'write':
210 res['bw_mean'] = parsed_res['write']
211 elif params.action == 'randwrite':
212 res['bw_mean'] = parsed_res['random write']
213 elif params.action == 'read':
214 res['bw_mean'] = parsed_res['read']
215 elif params.action == 'randread':
216 res['bw_mean'] = parsed_res['random read']
217 except:
koder aka kdanilov4af80852015-02-01 23:36:38 +0200218 raise
219
220 # res['bw_dev'] = 0
221 # res['bw_max'] = res["bw_mean"]
222 # res['bw_min'] = res["bw_mean"]
223
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200224 return res, " ".join(cmd)
koder aka kdanilov4af80852015-02-01 23:36:38 +0200225
226
227def run_iozone(benchmark, iozone_path, tmpname, timeout=None):
228 if timeout is not None:
229 benchmark.size = benchmark.blocksize * 50
230 res_time = do_run_iozone(benchmark, tmpname, timeout,
231 iozone_path=iozone_path,
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200232 microsecond_mode=True)[0]
koder aka kdanilov4af80852015-02-01 23:36:38 +0200233
234 size = (benchmark.blocksize * timeout * 1000000)
235 size /= res_time["bw_mean"]
236 size = (size // benchmark.blocksize + 1) * benchmark.blocksize
237 benchmark.size = size
238
239 return do_run_iozone(benchmark, tmpname, timeout,
240 iozone_path=iozone_path)
241
242
koder aka kdanilov4af80852015-02-01 23:36:38 +0200243def install_iozone_package():
244 if which('iozone'):
245 return
246
247 is_redhat = os.path.exists('/etc/centos-release')
248 is_redhat = is_redhat or os.path.exists('/etc/fedora-release')
249 is_redhat = is_redhat or os.path.exists('/etc/redhat-release')
250
251 if is_redhat:
252 subprocess.check_output(["yum", "install", 'iozone3'])
253 return
254
255 try:
256 os_release_cont = open('/etc/os-release').read()
257
258 is_ubuntu = "Ubuntu" in os_release_cont
259
260 if is_ubuntu or "Debian GNU/Linux" in os_release_cont:
261 subprocess.check_output(["apt-get", "install", "iozone3"])
262 return
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200263 except (IOError, OSError):
koder aka kdanilov4af80852015-02-01 23:36:38 +0200264 pass
265
266 raise RuntimeError("Unknown host OS.")
267
268
269def install_iozone_static(iozone_url, dst):
270 if not os.path.isfile(dst):
271 import urllib
272 urllib.urlretrieve(iozone_url, dst)
273
274 st = os.stat(dst)
275 os.chmod(dst, st.st_mode | stat.S_IEXEC)
276
277
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200278def locate_iozone():
279 binary_path = which('iozone')
280
281 if binary_path is None:
282 binary_path = which('iozone3')
283
284 if binary_path is None:
285 sys.stderr.write("Can't found neither iozone not iozone3 binary"
Kostiantyn Danylov aka koder02adc1d2015-02-02 01:10:04 +0200286 "Provide --bonary-path or --binary-url option")
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200287 return False, None
288
289 return False, binary_path
290
291# ------------------------------ FIO SUPPORT ---------------------------------
292
293
294def run_fio_once(benchmark, fio_path, tmpname, timeout=None):
295
296 cmd_line = [fio_path,
297 "--name=%s" % benchmark.action,
298 "--rw=%s" % benchmark.action,
299 "--blocksize=%sk" % benchmark.blocksize,
300 "--iodepth=%d" % benchmark.iodepth,
301 "--filename=%s" % tmpname,
302 "--size={0}k".format(benchmark.size),
303 "--numjobs={0}".format(benchmark.concurence),
304 "--output-format=json",
305 "--sync=" + ('1' if benchmark.sync else '0')]
306
307 if timeout is not None:
308 cmd_line.append("--timeout=%d" % timeout)
309 cmd_line.append("--runtime=%d" % timeout)
310
311 if benchmark.direct_io:
312 cmd_line.append("--direct=1")
313
314 if benchmark.use_hight_io_priority:
315 cmd_line.append("--prio=0")
316
317 raw_out = subprocess.check_output(cmd_line)
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200318 return json.loads(raw_out)["jobs"][0], " ".join(cmd_line)
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200319
320
321def run_fio(benchmark, fio_path, tmpname, timeout=None):
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200322 job_output, cmd_line = run_fio_once(benchmark, fio_path, tmpname, timeout)
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200323
324 if benchmark.action in ('write', 'randwrite'):
325 raw_result = job_output['write']
326 else:
327 raw_result = job_output['read']
328
329 res = {}
koder aka kdanilov78ba8952015-02-03 01:11:23 +0200330
331 # 'bw_dev bw_mean bw_max bw_min'.split()
332 for field in ["bw_mean"]:
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200333 res[field] = raw_result[field]
334
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200335 return res, cmd_line
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200336
337
338def locate_fio():
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200339 return False, which('fio')
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200340
341
342# ----------------------------------------------------------------------------
343
344
345def locate_binary(binary_tp, binary_url, binary_path):
346 remove_binary = False
347
348 if binary_url is not None:
349 if binary_path is not None:
350 sys.stderr.write("At most one option from --binary-path and "
351 "--binary-url should be provided")
352 return False, None
353
354 binary_path = os.tmpnam()
355 install_iozone_static(binary_url, binary_path)
356 remove_binary = True
357
358 elif binary_path is not None:
359 if os.path.isfile(binary_path):
360 if not os.access(binary_path, os.X_OK):
361 st = os.stat(binary_path)
362 os.chmod(binary_path, st.st_mode | stat.S_IEXEC)
363 else:
364 binary_path = None
365
366 if binary_path is not None:
367 return remove_binary, binary_path
368
369 if 'iozone' == binary_tp:
370 return locate_iozone()
371 else:
372 return locate_fio()
373
374
375def run_benchmark(binary_tp, *argv, **kwargs):
376 if 'iozone' == binary_tp:
377 return run_iozone(*argv, **kwargs)
378 else:
379 return run_fio(*argv, **kwargs)
380
381
koder aka kdanilov4af80852015-02-01 23:36:38 +0200382def type_size(string):
383 try:
384 return re.match("\d+[KGBM]?", string, re.I).group(0)
385 except:
386 msg = "{0!r} don't looks like size-description string".format(string)
387 raise ValueError(msg)
388
389
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200390def type_size_ext(string):
391 if string.startswith("x"):
392 int(string[1:])
393 return string
394
395 if string.startswith("r"):
396 int(string[1:])
397 return string
398
399 try:
400 return re.match("\d+[KGBM]?", string, re.I).group(0)
401 except:
402 msg = "{0!r} don't looks like size-description string".format(string)
403 raise ValueError(msg)
404
405
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200406def ssize_to_kb(ssize):
407 try:
408 smap = dict(k=1, K=1, M=1024, m=1024, G=1024**2, g=1024**2)
409 for ext, coef in smap.items():
410 if ssize.endswith(ext):
411 return int(ssize[:-1]) * coef
412
413 if int(ssize) % 1024 != 0:
414 raise ValueError()
415
416 return int(ssize) / 1024
417
418 except (ValueError, TypeError, AttributeError):
419 tmpl = "Unknow size format {0!r} (or size not multiples 1024)"
420 raise ValueError(tmpl.format(ssize))
421
422
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200423def get_ram_size():
424 try:
425 with open("/proc/meminfo") as fd:
426 for ln in fd:
427 if "MemTotal:" in ln:
428 sz, kb = ln.split(':')[1].strip().split(" ")
429 assert kb == 'kB'
430 return int(sz)
431 except (ValueError, TypeError, AssertionError):
432 raise
433 # return None
434
435
koder aka kdanilov4af80852015-02-01 23:36:38 +0200436def parse_args(argv):
437 parser = argparse.ArgumentParser(
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200438 description="Run 'iozone' or 'fio' and return result")
439 parser.add_argument(
440 "--type", metavar="BINARY_TYPE",
441 choices=['iozone', 'fio'], required=True)
koder aka kdanilov4af80852015-02-01 23:36:38 +0200442 parser.add_argument(
443 "--iodepth", metavar="IODEPTH", type=int,
444 help="I/O depths to test in kb", required=True)
445 parser.add_argument(
446 '-a', "--action", metavar="ACTION", type=str,
447 help="actions to run", required=True,
448 choices=["read", "write", "randread", "randwrite"])
449 parser.add_argument(
450 "--blocksize", metavar="BLOCKSIZE", type=type_size,
451 help="single operation block size", required=True)
452 parser.add_argument(
453 "--timeout", metavar="TIMEOUT", type=int,
454 help="runtime of a single run", default=None)
455 parser.add_argument(
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200456 "--iosize", metavar="SIZE", type=type_size_ext,
koder aka kdanilov4af80852015-02-01 23:36:38 +0200457 help="file size", default=None)
458 parser.add_argument(
459 "-s", "--sync", default=False, action="store_true",
460 help="exec sync after each write")
461 parser.add_argument(
462 "-d", "--direct-io", default=False, action="store_true",
463 help="use O_DIRECT", dest='directio')
464 parser.add_argument(
465 "-t", "--sync-time", default=None, type=int,
466 help="sleep till sime utc time", dest='sync_time')
467 parser.add_argument(
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200468 "--binary-url", help="static binary url",
469 dest="binary_url", default=None)
koder aka kdanilov4af80852015-02-01 23:36:38 +0200470 parser.add_argument(
471 "--test-file", help="file path to run test on",
472 default=None, dest='test_file')
473 parser.add_argument(
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200474 "--binary-path", help="binary path",
475 default=None, dest='binary_path')
koder aka kdanilov4af80852015-02-01 23:36:38 +0200476 return parser.parse_args(argv)
477
478
479def main(argv):
480 argv_obj = parse_args(argv)
481 argv_obj.blocksize = ssize_to_kb(argv_obj.blocksize)
482
483 if argv_obj.iosize is not None:
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200484 if argv_obj.iosize.startswith('x'):
485 argv_obj.iosize = argv_obj.blocksize * int(argv_obj.iosize[1:])
486 elif argv_obj.iosize.startswith('r'):
487 rs = get_ram_size()
488 if rs is None:
489 sys.stderr.write("Can't determine ram size\n")
490 exit(1)
491 argv_obj.iosize = rs * int(argv_obj.iosize[1:])
492 else:
493 argv_obj.iosize = ssize_to_kb(argv_obj.iosize)
koder aka kdanilov4af80852015-02-01 23:36:38 +0200494
495 benchmark = BenchmarkOption(1,
496 argv_obj.iodepth,
497 argv_obj.action,
498 argv_obj.blocksize,
499 argv_obj.iosize)
500
501 benchmark.direct_io = argv_obj.directio
502
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200503 if argv_obj.sync:
504 benchmark.sync = True
505
koder aka kdanilov4af80852015-02-01 23:36:38 +0200506 test_file_name = argv_obj.test_file
507 if test_file_name is None:
508 with warnings.catch_warnings():
509 warnings.simplefilter("ignore")
510 test_file_name = os.tmpnam()
511
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200512 remove_binary, binary_path = locate_binary(argv_obj.type,
513 argv_obj.binary_url,
514 argv_obj.binary_path)
koder aka kdanilov4af80852015-02-01 23:36:38 +0200515
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200516 if binary_path is None:
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200517 sys.stderr.write("Can't locate binary {0}\n".format(argv_obj.type))
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200518 return 1
koder aka kdanilov4af80852015-02-01 23:36:38 +0200519
520 try:
521 if argv_obj.sync_time is not None:
522 dt = argv_obj.sync_time - time.time()
523 if dt > 0:
524 time.sleep(dt)
525
koder aka kdanilov4a72f122015-02-09 12:25:54 +0200526 res, cmd = run_benchmark(argv_obj.type,
527 benchmark,
528 binary_path,
529 test_file_name)
530 res['__meta__'] = benchmark.__dict__.copy()
531 res['__meta__']['cmdline'] = cmd
koder aka kdanilov4af80852015-02-01 23:36:38 +0200532 sys.stdout.write(json.dumps(res) + "\n")
533 finally:
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200534 if remove_binary:
535 os.unlink(binary_path)
koder aka kdanilov4af80852015-02-01 23:36:38 +0200536
537 if os.path.isfile(test_file_name):
538 os.unlink(test_file_name)
539
540
541# function-marker for patching, don't 'optimize' it
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200542def INSERT_TOOL_ARGS(x):
koder aka kdanilov78ba8952015-02-03 01:11:23 +0200543 return [x]
koder aka kdanilov4af80852015-02-01 23:36:38 +0200544
545
546if __name__ == '__main__':
547 # this line would be patched in case of run under rally
548 # don't modify it!
koder aka kdanilov78ba8952015-02-03 01:11:23 +0200549 argvs = INSERT_TOOL_ARGS(sys.argv[1:])
koder aka kdanilov78ba8952015-02-03 01:11:23 +0200550 code = 0
551 for argv in argvs:
552 tcode = main(argv)
553 if tcode != 0:
554 code = tcode
555
556 exit(code)