blob: 5a74309e370b7ccd92fdfbecfcc9cc742ae9a7b2 [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
190 raw_res = subprocess.check_output(cmd)
191
192 try:
193 parsed_res = IOZoneParser.parse_iozone_res(raw_res, threads > 1)
194
195 res = {}
196
197 if params.action == 'write':
198 res['bw_mean'] = parsed_res['write']
199 elif params.action == 'randwrite':
200 res['bw_mean'] = parsed_res['random write']
201 elif params.action == 'read':
202 res['bw_mean'] = parsed_res['read']
203 elif params.action == 'randread':
204 res['bw_mean'] = parsed_res['random read']
205 except:
206 print raw_res
207 raise
208
209 # res['bw_dev'] = 0
210 # res['bw_max'] = res["bw_mean"]
211 # res['bw_min'] = res["bw_mean"]
212
213 return res
214
215
216def run_iozone(benchmark, iozone_path, tmpname, timeout=None):
217 if timeout is not None:
218 benchmark.size = benchmark.blocksize * 50
219 res_time = do_run_iozone(benchmark, tmpname, timeout,
220 iozone_path=iozone_path,
221 microsecond_mode=True)
222
223 size = (benchmark.blocksize * timeout * 1000000)
224 size /= res_time["bw_mean"]
225 size = (size // benchmark.blocksize + 1) * benchmark.blocksize
226 benchmark.size = size
227
228 return do_run_iozone(benchmark, tmpname, timeout,
229 iozone_path=iozone_path)
230
231
koder aka kdanilov4af80852015-02-01 23:36:38 +0200232def install_iozone_package():
233 if which('iozone'):
234 return
235
236 is_redhat = os.path.exists('/etc/centos-release')
237 is_redhat = is_redhat or os.path.exists('/etc/fedora-release')
238 is_redhat = is_redhat or os.path.exists('/etc/redhat-release')
239
240 if is_redhat:
241 subprocess.check_output(["yum", "install", 'iozone3'])
242 return
243
244 try:
245 os_release_cont = open('/etc/os-release').read()
246
247 is_ubuntu = "Ubuntu" in os_release_cont
248
249 if is_ubuntu or "Debian GNU/Linux" in os_release_cont:
250 subprocess.check_output(["apt-get", "install", "iozone3"])
251 return
252 except (IOError, OSError) as exc:
253 print exc
254 pass
255
256 raise RuntimeError("Unknown host OS.")
257
258
259def install_iozone_static(iozone_url, dst):
260 if not os.path.isfile(dst):
261 import urllib
262 urllib.urlretrieve(iozone_url, dst)
263
264 st = os.stat(dst)
265 os.chmod(dst, st.st_mode | stat.S_IEXEC)
266
267
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200268def locate_iozone():
269 binary_path = which('iozone')
270
271 if binary_path is None:
272 binary_path = which('iozone3')
273
274 if binary_path is None:
275 sys.stderr.write("Can't found neither iozone not iozone3 binary"
Kostiantyn Danylov aka koder02adc1d2015-02-02 01:10:04 +0200276 "Provide --bonary-path or --binary-url option")
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200277 return False, None
278
279 return False, binary_path
280
281# ------------------------------ FIO SUPPORT ---------------------------------
282
283
284def run_fio_once(benchmark, fio_path, tmpname, timeout=None):
285
286 cmd_line = [fio_path,
287 "--name=%s" % benchmark.action,
288 "--rw=%s" % benchmark.action,
289 "--blocksize=%sk" % benchmark.blocksize,
290 "--iodepth=%d" % benchmark.iodepth,
291 "--filename=%s" % tmpname,
292 "--size={0}k".format(benchmark.size),
293 "--numjobs={0}".format(benchmark.concurence),
294 "--output-format=json",
295 "--sync=" + ('1' if benchmark.sync else '0')]
296
297 if timeout is not None:
298 cmd_line.append("--timeout=%d" % timeout)
299 cmd_line.append("--runtime=%d" % timeout)
300
301 if benchmark.direct_io:
302 cmd_line.append("--direct=1")
303
304 if benchmark.use_hight_io_priority:
305 cmd_line.append("--prio=0")
306
307 raw_out = subprocess.check_output(cmd_line)
308 return json.loads(raw_out)["jobs"][0]
309
310
311def run_fio(benchmark, fio_path, tmpname, timeout=None):
312 job_output = run_fio_once(benchmark, fio_path, tmpname, timeout)
313
314 if benchmark.action in ('write', 'randwrite'):
315 raw_result = job_output['write']
316 else:
317 raw_result = job_output['read']
318
319 res = {}
320 for field in 'bw_dev bw_mean bw_max bw_min'.split():
321 res[field] = raw_result[field]
322
323 return res
324
325
326def locate_fio():
327 return False, None
328
329
330# ----------------------------------------------------------------------------
331
332
333def locate_binary(binary_tp, binary_url, binary_path):
334 remove_binary = False
335
336 if binary_url is not None:
337 if binary_path is not None:
338 sys.stderr.write("At most one option from --binary-path and "
339 "--binary-url should be provided")
340 return False, None
341
342 binary_path = os.tmpnam()
343 install_iozone_static(binary_url, binary_path)
344 remove_binary = True
345
346 elif binary_path is not None:
347 if os.path.isfile(binary_path):
348 if not os.access(binary_path, os.X_OK):
349 st = os.stat(binary_path)
350 os.chmod(binary_path, st.st_mode | stat.S_IEXEC)
351 else:
352 binary_path = None
353
354 if binary_path is not None:
355 return remove_binary, binary_path
356
357 if 'iozone' == binary_tp:
358 return locate_iozone()
359 else:
360 return locate_fio()
361
362
363def run_benchmark(binary_tp, *argv, **kwargs):
364 if 'iozone' == binary_tp:
365 return run_iozone(*argv, **kwargs)
366 else:
367 return run_fio(*argv, **kwargs)
368
369
koder aka kdanilov4af80852015-02-01 23:36:38 +0200370def type_size(string):
371 try:
372 return re.match("\d+[KGBM]?", string, re.I).group(0)
373 except:
374 msg = "{0!r} don't looks like size-description string".format(string)
375 raise ValueError(msg)
376
377
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200378def ssize_to_kb(ssize):
379 try:
380 smap = dict(k=1, K=1, M=1024, m=1024, G=1024**2, g=1024**2)
381 for ext, coef in smap.items():
382 if ssize.endswith(ext):
383 return int(ssize[:-1]) * coef
384
385 if int(ssize) % 1024 != 0:
386 raise ValueError()
387
388 return int(ssize) / 1024
389
390 except (ValueError, TypeError, AttributeError):
391 tmpl = "Unknow size format {0!r} (or size not multiples 1024)"
392 raise ValueError(tmpl.format(ssize))
393
394
koder aka kdanilov4af80852015-02-01 23:36:38 +0200395def parse_args(argv):
396 parser = argparse.ArgumentParser(
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200397 description="Run 'iozone' or 'fio' and return result")
398 parser.add_argument(
399 "--type", metavar="BINARY_TYPE",
400 choices=['iozone', 'fio'], required=True)
koder aka kdanilov4af80852015-02-01 23:36:38 +0200401 parser.add_argument(
402 "--iodepth", metavar="IODEPTH", type=int,
403 help="I/O depths to test in kb", required=True)
404 parser.add_argument(
405 '-a', "--action", metavar="ACTION", type=str,
406 help="actions to run", required=True,
407 choices=["read", "write", "randread", "randwrite"])
408 parser.add_argument(
409 "--blocksize", metavar="BLOCKSIZE", type=type_size,
410 help="single operation block size", required=True)
411 parser.add_argument(
412 "--timeout", metavar="TIMEOUT", type=int,
413 help="runtime of a single run", default=None)
414 parser.add_argument(
415 "--iosize", metavar="SIZE", type=type_size,
416 help="file size", default=None)
417 parser.add_argument(
418 "-s", "--sync", default=False, action="store_true",
419 help="exec sync after each write")
420 parser.add_argument(
421 "-d", "--direct-io", default=False, action="store_true",
422 help="use O_DIRECT", dest='directio')
423 parser.add_argument(
424 "-t", "--sync-time", default=None, type=int,
425 help="sleep till sime utc time", dest='sync_time')
426 parser.add_argument(
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200427 "--binary-url", help="static binary url",
428 dest="binary_url", default=None)
koder aka kdanilov4af80852015-02-01 23:36:38 +0200429 parser.add_argument(
430 "--test-file", help="file path to run test on",
431 default=None, dest='test_file')
432 parser.add_argument(
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200433 "--binary-path", help="binary path",
434 default=None, dest='binary_path')
koder aka kdanilov4af80852015-02-01 23:36:38 +0200435 return parser.parse_args(argv)
436
437
438def main(argv):
439 argv_obj = parse_args(argv)
440 argv_obj.blocksize = ssize_to_kb(argv_obj.blocksize)
441
442 if argv_obj.iosize is not None:
443 argv_obj.iosize = ssize_to_kb(argv_obj.iosize)
444
445 benchmark = BenchmarkOption(1,
446 argv_obj.iodepth,
447 argv_obj.action,
448 argv_obj.blocksize,
449 argv_obj.iosize)
450
451 benchmark.direct_io = argv_obj.directio
452
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200453 if argv_obj.sync:
454 benchmark.sync = True
455
koder aka kdanilov4af80852015-02-01 23:36:38 +0200456 test_file_name = argv_obj.test_file
457 if test_file_name is None:
458 with warnings.catch_warnings():
459 warnings.simplefilter("ignore")
460 test_file_name = os.tmpnam()
461
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200462 remove_binary, binary_path = locate_binary(argv_obj.type,
463 argv_obj.binary_url,
464 argv_obj.binary_path)
koder aka kdanilov4af80852015-02-01 23:36:38 +0200465
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200466 if binary_path is None:
467 return 1
koder aka kdanilov4af80852015-02-01 23:36:38 +0200468
469 try:
470 if argv_obj.sync_time is not None:
471 dt = argv_obj.sync_time - time.time()
472 if dt > 0:
473 time.sleep(dt)
474
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200475 res = run_benchmark(argv_obj.type,
476 benchmark,
477 binary_path,
478 test_file_name)
479
koder aka kdanilov4af80852015-02-01 23:36:38 +0200480 sys.stdout.write(json.dumps(res) + "\n")
481 finally:
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200482 if remove_binary:
483 os.unlink(binary_path)
koder aka kdanilov4af80852015-02-01 23:36:38 +0200484
485 if os.path.isfile(test_file_name):
486 os.unlink(test_file_name)
487
488
489# function-marker for patching, don't 'optimize' it
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200490def INSERT_TOOL_ARGS(x):
koder aka kdanilov4af80852015-02-01 23:36:38 +0200491 return x
492
493
494if __name__ == '__main__':
495 # this line would be patched in case of run under rally
496 # don't modify it!
koder aka kdanilov98615bf2015-02-02 00:59:07 +0200497 argv = INSERT_TOOL_ARGS(sys.argv[1:])
koder aka kdanilov4af80852015-02-01 23:36:38 +0200498 exit(main(argv))