blob: 3b62967bd5a18803f894a5bdebc42febb8f95250 [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
145 cmd = [iozone_path]
146
147 if params.sync:
148 cmd.append('-o')
149
150 if params.direct_io:
151 cmd.append('-I')
152
153 if microsecond_mode:
154 cmd.append('-N')
155
156 all_files = []
157 threads = int(params.concurence)
158 if 1 != threads:
159 cmd.extend(('-t', str(threads), '-F'))
160 filename = filename + "_{}"
161 cmd.extend(filename % i for i in range(threads))
162 all_files.extend(filename % i for i in range(threads))
163 else:
164 cmd.extend(('-f', filename))
165 all_files.append(filename)
166
167 bsz = 1024 if params.size > 1024 else params.size
168 if params.size % bsz != 0:
169 fsz = (params.size // bsz + 1) * bsz
170 else:
171 fsz = params.size
172
173 for fname in all_files:
174 ccmd = [iozone_path, "-f", fname, "-i", "0",
175 "-s", str(fsz), "-r", str(bsz), "-w"]
176 subprocess.check_output(ccmd)
177
178 cmd.append('-i')
179
180 if params.action == 'write':
181 cmd.append("0")
182 elif params.action == 'randwrite':
183 cmd.append("2")
184 else:
185 raise ValueError("Unknown action {0!r}".format(params.action))
186
187 cmd.extend(('-s', str(params.size)))
188 cmd.extend(('-r', str(params.blocksize)))
189
koder aka kdanilov78ba8952015-02-03 01:11:23 +0200190 # no retest
191 cmd.append('-+n')
192
koder aka kdanilov4af80852015-02-01 23:36:38 +0200193 raw_res = subprocess.check_output(cmd)
194
195 try:
196 parsed_res = IOZoneParser.parse_iozone_res(raw_res, threads > 1)
197
198 res = {}
199
200 if params.action == 'write':
201 res['bw_mean'] = parsed_res['write']
202 elif params.action == 'randwrite':
203 res['bw_mean'] = parsed_res['random write']
204 elif params.action == 'read':
205 res['bw_mean'] = parsed_res['read']
206 elif params.action == 'randread':
207 res['bw_mean'] = parsed_res['random read']
208 except:
209 print raw_res
210 raise
211
212 # res['bw_dev'] = 0
213 # res['bw_max'] = res["bw_mean"]
214 # res['bw_min'] = res["bw_mean"]
215
216 return res
217
218
219def run_iozone(benchmark, iozone_path, tmpname, timeout=None):
220 if timeout is not None:
221 benchmark.size = benchmark.blocksize * 50
222 res_time = do_run_iozone(benchmark, tmpname, timeout,
223 iozone_path=iozone_path,
224 microsecond_mode=True)
225
226 size = (benchmark.blocksize * timeout * 1000000)
227 size /= res_time["bw_mean"]
228 size = (size // benchmark.blocksize + 1) * benchmark.blocksize
229 benchmark.size = size
230
231 return do_run_iozone(benchmark, tmpname, timeout,
232 iozone_path=iozone_path)
233
234
koder aka kdanilov4af80852015-02-01 23:36:38 +0200235def install_iozone_package():
236 if which('iozone'):
237 return
238
239 is_redhat = os.path.exists('/etc/centos-release')
240 is_redhat = is_redhat or os.path.exists('/etc/fedora-release')
241 is_redhat = is_redhat or os.path.exists('/etc/redhat-release')
242
243 if is_redhat:
244 subprocess.check_output(["yum", "install", 'iozone3'])
245 return
246
247 try:
248 os_release_cont = open('/etc/os-release').read()
249
250 is_ubuntu = "Ubuntu" in os_release_cont
251
252 if is_ubuntu or "Debian GNU/Linux" in os_release_cont:
253 subprocess.check_output(["apt-get", "install", "iozone3"])
254 return
255 except (IOError, OSError) as exc:
256 print exc
257 pass
258
259 raise RuntimeError("Unknown host OS.")
260
261
262def install_iozone_static(iozone_url, dst):
263 if not os.path.isfile(dst):
264 import urllib
265 urllib.urlretrieve(iozone_url, dst)
266
267 st = os.stat(dst)
268 os.chmod(dst, st.st_mode | stat.S_IEXEC)
269
270
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200271def locate_iozone():
272 binary_path = which('iozone')
273
274 if binary_path is None:
275 binary_path = which('iozone3')
276
277 if binary_path is None:
278 sys.stderr.write("Can't found neither iozone not iozone3 binary"
Kostiantyn Danylov aka koder02adc1d2015-02-02 01:10:04 +0200279 "Provide --bonary-path or --binary-url option")
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200280 return False, None
281
282 return False, binary_path
283
284# ------------------------------ FIO SUPPORT ---------------------------------
285
286
287def run_fio_once(benchmark, fio_path, tmpname, timeout=None):
288
289 cmd_line = [fio_path,
290 "--name=%s" % benchmark.action,
291 "--rw=%s" % benchmark.action,
292 "--blocksize=%sk" % benchmark.blocksize,
293 "--iodepth=%d" % benchmark.iodepth,
294 "--filename=%s" % tmpname,
295 "--size={0}k".format(benchmark.size),
296 "--numjobs={0}".format(benchmark.concurence),
297 "--output-format=json",
298 "--sync=" + ('1' if benchmark.sync else '0')]
299
300 if timeout is not None:
301 cmd_line.append("--timeout=%d" % timeout)
302 cmd_line.append("--runtime=%d" % timeout)
303
304 if benchmark.direct_io:
305 cmd_line.append("--direct=1")
306
307 if benchmark.use_hight_io_priority:
308 cmd_line.append("--prio=0")
309
310 raw_out = subprocess.check_output(cmd_line)
311 return json.loads(raw_out)["jobs"][0]
312
313
314def run_fio(benchmark, fio_path, tmpname, timeout=None):
315 job_output = run_fio_once(benchmark, fio_path, tmpname, timeout)
316
317 if benchmark.action in ('write', 'randwrite'):
318 raw_result = job_output['write']
319 else:
320 raw_result = job_output['read']
321
322 res = {}
koder aka kdanilov78ba8952015-02-03 01:11:23 +0200323
324 # 'bw_dev bw_mean bw_max bw_min'.split()
325 for field in ["bw_mean"]:
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200326 res[field] = raw_result[field]
327
328 return res
329
330
331def locate_fio():
332 return False, None
333
334
335# ----------------------------------------------------------------------------
336
337
338def locate_binary(binary_tp, binary_url, binary_path):
339 remove_binary = False
340
341 if binary_url is not None:
342 if binary_path is not None:
343 sys.stderr.write("At most one option from --binary-path and "
344 "--binary-url should be provided")
345 return False, None
346
347 binary_path = os.tmpnam()
348 install_iozone_static(binary_url, binary_path)
349 remove_binary = True
350
351 elif binary_path is not None:
352 if os.path.isfile(binary_path):
353 if not os.access(binary_path, os.X_OK):
354 st = os.stat(binary_path)
355 os.chmod(binary_path, st.st_mode | stat.S_IEXEC)
356 else:
357 binary_path = None
358
359 if binary_path is not None:
360 return remove_binary, binary_path
361
362 if 'iozone' == binary_tp:
363 return locate_iozone()
364 else:
365 return locate_fio()
366
367
368def run_benchmark(binary_tp, *argv, **kwargs):
369 if 'iozone' == binary_tp:
370 return run_iozone(*argv, **kwargs)
371 else:
372 return run_fio(*argv, **kwargs)
373
374
koder aka kdanilov4af80852015-02-01 23:36:38 +0200375def type_size(string):
376 try:
377 return re.match("\d+[KGBM]?", string, re.I).group(0)
378 except:
379 msg = "{0!r} don't looks like size-description string".format(string)
380 raise ValueError(msg)
381
382
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200383def ssize_to_kb(ssize):
384 try:
385 smap = dict(k=1, K=1, M=1024, m=1024, G=1024**2, g=1024**2)
386 for ext, coef in smap.items():
387 if ssize.endswith(ext):
388 return int(ssize[:-1]) * coef
389
390 if int(ssize) % 1024 != 0:
391 raise ValueError()
392
393 return int(ssize) / 1024
394
395 except (ValueError, TypeError, AttributeError):
396 tmpl = "Unknow size format {0!r} (or size not multiples 1024)"
397 raise ValueError(tmpl.format(ssize))
398
399
koder aka kdanilov4af80852015-02-01 23:36:38 +0200400def parse_args(argv):
401 parser = argparse.ArgumentParser(
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200402 description="Run 'iozone' or 'fio' and return result")
403 parser.add_argument(
404 "--type", metavar="BINARY_TYPE",
405 choices=['iozone', 'fio'], required=True)
koder aka kdanilov4af80852015-02-01 23:36:38 +0200406 parser.add_argument(
407 "--iodepth", metavar="IODEPTH", type=int,
408 help="I/O depths to test in kb", required=True)
409 parser.add_argument(
410 '-a', "--action", metavar="ACTION", type=str,
411 help="actions to run", required=True,
412 choices=["read", "write", "randread", "randwrite"])
413 parser.add_argument(
414 "--blocksize", metavar="BLOCKSIZE", type=type_size,
415 help="single operation block size", required=True)
416 parser.add_argument(
417 "--timeout", metavar="TIMEOUT", type=int,
418 help="runtime of a single run", default=None)
419 parser.add_argument(
420 "--iosize", metavar="SIZE", type=type_size,
421 help="file size", default=None)
422 parser.add_argument(
423 "-s", "--sync", default=False, action="store_true",
424 help="exec sync after each write")
425 parser.add_argument(
426 "-d", "--direct-io", default=False, action="store_true",
427 help="use O_DIRECT", dest='directio')
428 parser.add_argument(
429 "-t", "--sync-time", default=None, type=int,
430 help="sleep till sime utc time", dest='sync_time')
431 parser.add_argument(
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200432 "--binary-url", help="static binary url",
433 dest="binary_url", default=None)
koder aka kdanilov4af80852015-02-01 23:36:38 +0200434 parser.add_argument(
435 "--test-file", help="file path to run test on",
436 default=None, dest='test_file')
437 parser.add_argument(
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200438 "--binary-path", help="binary path",
439 default=None, dest='binary_path')
koder aka kdanilov4af80852015-02-01 23:36:38 +0200440 return parser.parse_args(argv)
441
442
443def main(argv):
444 argv_obj = parse_args(argv)
445 argv_obj.blocksize = ssize_to_kb(argv_obj.blocksize)
446
447 if argv_obj.iosize is not None:
448 argv_obj.iosize = ssize_to_kb(argv_obj.iosize)
449
450 benchmark = BenchmarkOption(1,
451 argv_obj.iodepth,
452 argv_obj.action,
453 argv_obj.blocksize,
454 argv_obj.iosize)
455
456 benchmark.direct_io = argv_obj.directio
457
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200458 if argv_obj.sync:
459 benchmark.sync = True
460
koder aka kdanilov4af80852015-02-01 23:36:38 +0200461 test_file_name = argv_obj.test_file
462 if test_file_name is None:
463 with warnings.catch_warnings():
464 warnings.simplefilter("ignore")
465 test_file_name = os.tmpnam()
466
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200467 remove_binary, binary_path = locate_binary(argv_obj.type,
468 argv_obj.binary_url,
469 argv_obj.binary_path)
koder aka kdanilov4af80852015-02-01 23:36:38 +0200470
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200471 if binary_path is None:
472 return 1
koder aka kdanilov4af80852015-02-01 23:36:38 +0200473
474 try:
475 if argv_obj.sync_time is not None:
476 dt = argv_obj.sync_time - time.time()
477 if dt > 0:
478 time.sleep(dt)
479
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200480 res = run_benchmark(argv_obj.type,
481 benchmark,
482 binary_path,
483 test_file_name)
koder aka kdanilov78ba8952015-02-03 01:11:23 +0200484 res['__meta__'] = benchmark.__dict__
koder aka kdanilov4af80852015-02-01 23:36:38 +0200485 sys.stdout.write(json.dumps(res) + "\n")
486 finally:
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200487 if remove_binary:
488 os.unlink(binary_path)
koder aka kdanilov4af80852015-02-01 23:36:38 +0200489
490 if os.path.isfile(test_file_name):
491 os.unlink(test_file_name)
492
493
494# function-marker for patching, don't 'optimize' it
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200495def INSERT_TOOL_ARGS(x):
koder aka kdanilov78ba8952015-02-03 01:11:23 +0200496 return [x]
koder aka kdanilov4af80852015-02-01 23:36:38 +0200497
498
499if __name__ == '__main__':
500 # this line would be patched in case of run under rally
501 # don't modify it!
koder aka kdanilov78ba8952015-02-03 01:11:23 +0200502 argvs = INSERT_TOOL_ARGS(sys.argv[1:])
503
504 code = 0
505 for argv in argvs:
506 tcode = main(argv)
507 if tcode != 0:
508 code = tcode
509
510 exit(code)