blob: 5969062e63e90515ee047d5760c98e0c2da66217 [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
25class IOZoneParser(object):
26 "class to parse iozone results"
27
28 start_tests = re.compile(r"^\s+KB\s+reclen\s+")
29 resuts = re.compile(r"[\s0-9]+")
30 mt_iozone_re = re.compile(r"\s+Children see throughput " +
31 r"for\s+\d+\s+(?P<cmd>.*?)\s+=\s+" +
32 r"(?P<perf>[\d.]+)\s+KB/sec")
33
34 cmap = {'initial writers': 'write',
35 'rewriters': 'rewrite',
36 'initial readers': 'read',
37 're-readers': 'reread',
38 'random readers': 'random read',
39 'random writers': 'random write'}
40
41 string1 = " " + \
42 " random random " + \
43 "bkwd record stride "
44
45 string2 = "KB reclen write rewrite " + \
46 "read reread read write " + \
47 "read rewrite read fwrite frewrite fread freread"
48
49 @classmethod
50 def apply_parts(cls, parts, string, sep=' \t\n'):
51 add_offset = 0
52 for part in parts:
53 _, start, stop = part
54 start += add_offset
55 add_offset = 0
56
57 # condition splited to make pylint happy
58 while stop + add_offset < len(string):
59
60 # condition splited to make pylint happy
61 if not (string[stop + add_offset] not in sep):
62 break
63
64 add_offset += 1
65
66 yield part, string[start:stop + add_offset]
67
68 @classmethod
69 def make_positions(cls):
70 items = [i for i in cls.string2.split() if i]
71
72 pos = 0
73 cls.positions = []
74
75 for item in items:
76 npos = cls.string2.index(item, 0 if pos == 0 else pos + 1)
77 cls.positions.append([item, pos, npos + len(item)])
78 pos = npos + len(item)
79
80 for itm, val in cls.apply_parts(cls.positions, cls.string1):
81 if val.strip():
82 itm[0] = val.strip() + " " + itm[0]
83
84 @classmethod
85 def parse_iozone_res(cls, res, mthreads=False):
86 parsed_res = None
87
88 sres = res.split('\n')
89
90 if not mthreads:
91 for pos, line in enumerate(sres[1:]):
92 if line.strip() == cls.string2 and \
93 sres[pos].strip() == cls.string1.strip():
94 add_pos = line.index(cls.string2)
95 parsed_res = {}
96
97 npos = [(name, start + add_pos, stop + add_pos)
98 for name, start, stop in cls.positions]
99
100 for itm, res in cls.apply_parts(npos, sres[pos + 2]):
101 if res.strip() != '':
102 parsed_res[itm[0]] = int(res.strip())
103
104 del parsed_res['KB']
105 del parsed_res['reclen']
106 else:
107 parsed_res = {}
108 for line in sres:
109 rr = cls.mt_iozone_re.match(line)
110 if rr is not None:
111 cmd = rr.group('cmd')
112 key = cls.cmap.get(cmd, cmd)
113 perf = int(float(rr.group('perf')))
114 parsed_res[key] = perf
115 return parsed_res
116
117
118def ssize_to_kb(ssize):
119 try:
120 smap = dict(k=1, K=1, M=1024, m=1024, G=1024**2, g=1024**2)
121 for ext, coef in smap.items():
122 if ssize.endswith(ext):
123 return int(ssize[:-1]) * coef
124
125 if int(ssize) % 1024 != 0:
126 raise ValueError()
127
128 return int(ssize) / 1024
129
130 except (ValueError, TypeError, AttributeError):
131 tmpl = "Unknow size format {0!r} (or size not multiples 1024)"
132 raise ValueError(tmpl.format(ssize))
133
134
135IOZoneParser.make_positions()
136
137
138def do_run_iozone(params, filename, timeout, iozone_path='iozone',
139 microsecond_mode=False):
140
141 cmd = [iozone_path]
142
143 if params.sync:
144 cmd.append('-o')
145
146 if params.direct_io:
147 cmd.append('-I')
148
149 if microsecond_mode:
150 cmd.append('-N')
151
152 all_files = []
153 threads = int(params.concurence)
154 if 1 != threads:
155 cmd.extend(('-t', str(threads), '-F'))
156 filename = filename + "_{}"
157 cmd.extend(filename % i for i in range(threads))
158 all_files.extend(filename % i for i in range(threads))
159 else:
160 cmd.extend(('-f', filename))
161 all_files.append(filename)
162
163 bsz = 1024 if params.size > 1024 else params.size
164 if params.size % bsz != 0:
165 fsz = (params.size // bsz + 1) * bsz
166 else:
167 fsz = params.size
168
169 for fname in all_files:
170 ccmd = [iozone_path, "-f", fname, "-i", "0",
171 "-s", str(fsz), "-r", str(bsz), "-w"]
172 subprocess.check_output(ccmd)
173
174 cmd.append('-i')
175
176 if params.action == 'write':
177 cmd.append("0")
178 elif params.action == 'randwrite':
179 cmd.append("2")
180 else:
181 raise ValueError("Unknown action {0!r}".format(params.action))
182
183 cmd.extend(('-s', str(params.size)))
184 cmd.extend(('-r', str(params.blocksize)))
185
186 raw_res = subprocess.check_output(cmd)
187
188 try:
189 parsed_res = IOZoneParser.parse_iozone_res(raw_res, threads > 1)
190
191 res = {}
192
193 if params.action == 'write':
194 res['bw_mean'] = parsed_res['write']
195 elif params.action == 'randwrite':
196 res['bw_mean'] = parsed_res['random write']
197 elif params.action == 'read':
198 res['bw_mean'] = parsed_res['read']
199 elif params.action == 'randread':
200 res['bw_mean'] = parsed_res['random read']
201 except:
202 print raw_res
203 raise
204
205 # res['bw_dev'] = 0
206 # res['bw_max'] = res["bw_mean"]
207 # res['bw_min'] = res["bw_mean"]
208
209 return res
210
211
212def run_iozone(benchmark, iozone_path, tmpname, timeout=None):
213 if timeout is not None:
214 benchmark.size = benchmark.blocksize * 50
215 res_time = do_run_iozone(benchmark, tmpname, timeout,
216 iozone_path=iozone_path,
217 microsecond_mode=True)
218
219 size = (benchmark.blocksize * timeout * 1000000)
220 size /= res_time["bw_mean"]
221 size = (size // benchmark.blocksize + 1) * benchmark.blocksize
222 benchmark.size = size
223
224 return do_run_iozone(benchmark, tmpname, timeout,
225 iozone_path=iozone_path)
226
227
228def which(program):
229 def is_exe(fpath):
230 return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
231
232 fpath, fname = os.path.split(program)
233 if fpath:
234 if is_exe(program):
235 return program
236 else:
237 for path in os.environ["PATH"].split(os.pathsep):
238 path = path.strip('"')
239 exe_file = os.path.join(path, program)
240 if is_exe(exe_file):
241 return exe_file
242
243 return None
244
245
246def install_iozone_package():
247 if which('iozone'):
248 return
249
250 is_redhat = os.path.exists('/etc/centos-release')
251 is_redhat = is_redhat or os.path.exists('/etc/fedora-release')
252 is_redhat = is_redhat or os.path.exists('/etc/redhat-release')
253
254 if is_redhat:
255 subprocess.check_output(["yum", "install", 'iozone3'])
256 return
257
258 try:
259 os_release_cont = open('/etc/os-release').read()
260
261 is_ubuntu = "Ubuntu" in os_release_cont
262
263 if is_ubuntu or "Debian GNU/Linux" in os_release_cont:
264 subprocess.check_output(["apt-get", "install", "iozone3"])
265 return
266 except (IOError, OSError) as exc:
267 print exc
268 pass
269
270 raise RuntimeError("Unknown host OS.")
271
272
273def install_iozone_static(iozone_url, dst):
274 if not os.path.isfile(dst):
275 import urllib
276 urllib.urlretrieve(iozone_url, dst)
277
278 st = os.stat(dst)
279 os.chmod(dst, st.st_mode | stat.S_IEXEC)
280
281
282def type_size(string):
283 try:
284 return re.match("\d+[KGBM]?", string, re.I).group(0)
285 except:
286 msg = "{0!r} don't looks like size-description string".format(string)
287 raise ValueError(msg)
288
289
290def parse_args(argv):
291 parser = argparse.ArgumentParser(
292 description="Run set of `iozone` invocations and return result")
293 parser.add_argument(
294 "--iodepth", metavar="IODEPTH", type=int,
295 help="I/O depths to test in kb", required=True)
296 parser.add_argument(
297 '-a', "--action", metavar="ACTION", type=str,
298 help="actions to run", required=True,
299 choices=["read", "write", "randread", "randwrite"])
300 parser.add_argument(
301 "--blocksize", metavar="BLOCKSIZE", type=type_size,
302 help="single operation block size", required=True)
303 parser.add_argument(
304 "--timeout", metavar="TIMEOUT", type=int,
305 help="runtime of a single run", default=None)
306 parser.add_argument(
307 "--iosize", metavar="SIZE", type=type_size,
308 help="file size", default=None)
309 parser.add_argument(
310 "-s", "--sync", default=False, action="store_true",
311 help="exec sync after each write")
312 parser.add_argument(
313 "-d", "--direct-io", default=False, action="store_true",
314 help="use O_DIRECT", dest='directio')
315 parser.add_argument(
316 "-t", "--sync-time", default=None, type=int,
317 help="sleep till sime utc time", dest='sync_time')
318 parser.add_argument(
319 "--iozone-url", help="iozone static binary url",
320 dest="iozone_url", default=None)
321 parser.add_argument(
322 "--test-file", help="file path to run test on",
323 default=None, dest='test_file')
324 parser.add_argument(
325 "--iozone-path", help="iozone path",
326 default=None, dest='iozone_path')
327 return parser.parse_args(argv)
328
329
330def main(argv):
331 argv_obj = parse_args(argv)
332 argv_obj.blocksize = ssize_to_kb(argv_obj.blocksize)
333
334 if argv_obj.iosize is not None:
335 argv_obj.iosize = ssize_to_kb(argv_obj.iosize)
336
337 benchmark = BenchmarkOption(1,
338 argv_obj.iodepth,
339 argv_obj.action,
340 argv_obj.blocksize,
341 argv_obj.iosize)
342
343 benchmark.direct_io = argv_obj.directio
344
345 test_file_name = argv_obj.test_file
346 if test_file_name is None:
347 with warnings.catch_warnings():
348 warnings.simplefilter("ignore")
349 test_file_name = os.tmpnam()
350
351 remove_iozone_bin = False
352 if argv_obj.iozone_url is not None:
353 if argv_obj.iozone_path is not None:
354 sys.stderr.write("At most one option from --iozone-path and "
355 "--iozone-url should be provided")
356 exit(1)
357 iozone_binary = os.tmpnam()
358 install_iozone_static(argv_obj.iozone_url, iozone_binary)
359 remove_iozone_bin = True
360 elif argv_obj.iozone_path is not None:
361 iozone_binary = argv_obj.iozone_path
362 if os.path.isfile(iozone_binary):
363 if not os.access(iozone_binary, os.X_OK):
364 st = os.stat(iozone_binary)
365 os.chmod(iozone_binary, st.st_mode | stat.S_IEXEC)
366
367 else:
368 iozone_binary = which('iozone')
369
370 if iozone_binary is None:
371 iozone_binary = which('iozone3')
372
373 if iozone_binary is None:
374 sys.stderr.write("Can't found neither iozone not iozone3 binary"
375 "Provide --iozone-path or --iozone-url option")
376 exit(1)
377
378 try:
379 if argv_obj.sync_time is not None:
380 dt = argv_obj.sync_time - time.time()
381 if dt > 0:
382 time.sleep(dt)
383
384 res = run_iozone(benchmark, iozone_binary, test_file_name)
385 sys.stdout.write(json.dumps(res) + "\n")
386 finally:
387 if remove_iozone_bin:
388 os.unlink(iozone_binary)
389
390 if os.path.isfile(test_file_name):
391 os.unlink(test_file_name)
392
393
394# function-marker for patching, don't 'optimize' it
395def INSERT_IOZONE_ARGS(x):
396 return x
397
398
399if __name__ == '__main__':
400 # this line would be patched in case of run under rally
401 # don't modify it!
402 argv = INSERT_IOZONE_ARGS(sys.argv[1:])
403 exit(main(argv))