blob: 0f788edf0cdb69bd66388be7f1d162cd70936422 [file] [log] [blame]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +03001import os
2import sys
3import copy
4import os.path
5import argparse
6import itertools
7from collections import OrderedDict, namedtuple
8
9
koder aka kdanilov88407ff2015-05-26 15:35:57 +030010from wally.utils import sec_to_str, ssize2b
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030011
12
13SECTION = 0
14SETTING = 1
15INCLUDE = 2
16
17
18Var = namedtuple('Var', ('name',))
19CfgLine = namedtuple('CfgLine', ('fname', 'lineno', 'oline',
20 'tp', 'name', 'val'))
21
22
23class FioJobSection(object):
24 def __init__(self, name):
25 self.name = name
26 self.vals = OrderedDict()
27
28 def copy(self):
29 return copy.deepcopy(self)
30
31 def required_vars(self):
32 for name, val in self.vals.items():
33 if isinstance(val, Var):
34 yield name, val
35
36 def is_free(self):
37 return len(list(self.required_vars())) == 0
38
39 def __str__(self):
40 res = "[{0}]\n".format(self.name)
41
42 for name, val in self.vals.items():
43 if name.startswith('_') or name == name.upper():
44 continue
45 if isinstance(val, Var):
46 res += "{0}={{{1}}}\n".format(name, val.name)
47 else:
48 res += "{0}={1}\n".format(name, val)
49
50 return res
51
52
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030053class ParseError(ValueError):
54 def __init__(self, msg, fname, lineno, line_cont=""):
55 ValueError.__init__(self, msg)
56 self.file_name = fname
57 self.lineno = lineno
58 self.line_cont = line_cont
59
60 def __str__(self):
61 msg = "In {0}:{1} ({2}) : {3}"
62 return msg.format(self.file_name,
63 self.lineno,
64 self.line_cont,
65 super(ParseError, self).__str__())
66
67
68def is_name(name):
69 if len(name) == 0:
70 return False
71
72 if name[0] != '_' and not name[0].isalpha():
73 return False
74
75 for ch in name[1:]:
76 if name[0] != '_' and not name[0].isalnum():
77 return False
78
79 return True
80
81
82def parse_value(val):
83 try:
84 return int(val)
85 except ValueError:
86 pass
87
88 try:
89 return float(val)
90 except ValueError:
91 pass
92
93 if val.startswith('{%'):
94 assert val.endswith("%}")
95 content = val[2:-2]
96 vals = list(i.strip() for i in content.split(','))
97 return map(parse_value, vals)
98
99 if val.startswith('{'):
100 assert val.endswith("}")
101 assert is_name(val[1:-1])
102 return Var(val[1:-1])
103 return val
104
105
106def fio_config_lexer(fio_cfg, fname):
107 for lineno, oline in enumerate(fio_cfg.split("\n")):
108 try:
109 line = oline.strip()
110
111 if line.startswith("#") or line.startswith(";"):
112 continue
113
114 if line == "":
115 continue
116
117 if '#' in line:
118 raise ParseError("# isn't allowed inside line",
119 fname, lineno, oline)
120
121 if line.startswith('['):
122 yield CfgLine(fname, lineno, oline, SECTION,
123 line[1:-1].strip(), None)
124 elif '=' in line:
125 opt_name, opt_val = line.split('=', 1)
126 yield CfgLine(fname, lineno, oline, SETTING,
127 opt_name.strip(),
128 parse_value(opt_val.strip()))
129 elif line.startswith("include "):
130 yield CfgLine(fname, lineno, oline, INCLUDE,
131 line.split(" ", 1)[1], None)
132 else:
133 yield CfgLine(fname, lineno, oline, SETTING, line, '1')
134
135 except Exception as exc:
136 raise ParseError(str(exc), fname, lineno, oline)
137
138
139def fio_config_parse(lexer_iter):
140 in_globals = False
141 curr_section = None
142 glob_vals = OrderedDict()
143 sections_count = 0
144
145 lexed_lines = list(lexer_iter)
146 one_more = True
147 includes = {}
148
149 while one_more:
150 new_lines = []
151 one_more = False
152 for line in lexed_lines:
153 fname, lineno, oline, tp, name, val = line
154
155 if INCLUDE == tp:
156 if not os.path.exists(fname):
157 dirname = '.'
158 else:
159 dirname = os.path.dirname(fname)
160
161 new_fname = os.path.join(dirname, name)
162 includes[new_fname] = (fname, lineno)
163
164 try:
165 cont = open(new_fname).read()
166 except IOError as err:
167 msg = "Error while including file {0}: {1}"
168 raise ParseError(msg.format(new_fname, err),
169 fname, lineno, oline)
170
171 new_lines.extend(fio_config_lexer(cont, new_fname))
172 one_more = True
173 else:
174 new_lines.append(line)
175
176 lexed_lines = new_lines
177
178 for fname, lineno, oline, tp, name, val in lexed_lines:
179 if tp == SECTION:
180 if curr_section is not None:
181 yield curr_section
182 curr_section = None
183
184 if name == 'global':
185 if sections_count != 0:
186 raise ParseError("[global] section should" +
187 " be only one and first",
188 fname, lineno, oline)
189 in_globals = True
190 else:
191 in_globals = False
192 curr_section = FioJobSection(name)
193 curr_section.vals = glob_vals.copy()
194 sections_count += 1
195 else:
196 assert tp == SETTING
197 if in_globals:
198 glob_vals[name] = val
199 elif name == name.upper():
200 raise ParseError("Param '" + name +
201 "' not in [global] section",
202 fname, lineno, oline)
203 elif curr_section is None:
204 raise ParseError("Data outside section",
205 fname, lineno, oline)
206 else:
207 curr_section.vals[name] = val
208
209 if curr_section is not None:
210 yield curr_section
211
212
213def process_repeats(sec):
214 sec = sec.copy()
215 count = sec.vals.pop('NUM_ROUNDS', 1)
216 assert isinstance(count, (int, long))
217
218 for _ in range(count):
219 yield sec.copy()
220
221 if 'ramp_time' in sec.vals:
222 sec.vals['_ramp_time'] = sec.vals.pop('ramp_time')
223
224
225def process_cycles(sec):
226 cycles = OrderedDict()
227
228 for name, val in sec.vals.items():
229 if isinstance(val, list) and name.upper() != name:
230 cycles[name] = val
231
232 if len(cycles) == 0:
233 yield sec
234 else:
koder aka kdanilov170936a2015-06-27 22:51:17 +0300235 # thread should changes faster
236 numjobs = cycles.pop('numjobs', None)
237 items = cycles.items()
238
239 if len(items) > 0:
240 keys, vals = zip(*items)
241 keys = list(keys)
242 vals = list(vals)
243 else:
244 keys = []
245 vals = []
246
247 if numjobs is not None:
248 vals.append(numjobs)
249 keys.append('numjobs')
250
251 for combination in itertools.product(*vals):
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300252 new_sec = sec.copy()
koder aka kdanilov170936a2015-06-27 22:51:17 +0300253 new_sec.vals.update(zip(keys, combination))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300254 yield new_sec
255
256
257def apply_params(sec, params):
258 processed_vals = OrderedDict()
259 processed_vals.update(params)
260 for name, val in sec.vals.items():
261 if name in params:
262 continue
263
264 if isinstance(val, Var):
265 if val.name in params:
266 val = params[val.name]
267 elif val.name in processed_vals:
268 val = processed_vals[val.name]
269 processed_vals[name] = val
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300270
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300271 sec = sec.copy()
272 sec.vals = processed_vals
273 return sec
274
275
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300276MAGIC_OFFSET = 0.1885
277
278
279def abbv_name_to_full(name):
280 assert len(name) == 3
281
282 smode = {
283 'a': 'async',
284 's': 'sync',
285 'd': 'direct',
286 'x': 'sync direct'
287 }
288 off_mode = {'s': 'sequential', 'r': 'random'}
koder aka kdanilov7248c7b2015-05-31 22:53:03 +0300289 oper = {'r': 'read', 'w': 'write', 'm': 'mixed'}
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300290 return smode[name[2]] + " " + \
291 off_mode[name[0]] + " " + oper[name[1]]
292
293
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300294def finall_process(sec, counter=[0]):
295 sec = sec.copy()
296
297 if sec.vals.get('numjobs', '1') != 1:
298 msg = "Group reporting should be set if numjobs != 1"
299 assert 'group_reporting' in sec.vals, msg
300
301 sec.vals['unified_rw_reporting'] = '1'
302
koder aka kdanilovbb6d6cd2015-06-20 02:55:07 +0300303 if isinstance(sec.vals['size'], Var):
304 raise ValueError("Variable {0} isn't provided".format(
305 sec.vals['size'].name))
306
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300307 sz = ssize2b(sec.vals['size'])
308 offset = sz * ((MAGIC_OFFSET * counter[0]) % 1.0)
309 offset = int(offset) // 1024 ** 2
310 new_vars = {'UNIQ_OFFSET': str(offset) + "m"}
311
312 for name, val in sec.vals.items():
313 if isinstance(val, Var):
314 if val.name in new_vars:
315 sec.vals[name] = new_vars[val.name]
316
koder aka kdanilovbb6d6cd2015-06-20 02:55:07 +0300317 for vl in sec.vals.values():
318 if isinstance(vl, Var):
319 raise ValueError("Variable {0} isn't provided".format(vl.name))
320
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300321 params = sec.vals.copy()
322 params['UNIQ'] = 'UN{0}'.format(counter[0])
323 params['COUNTER'] = str(counter[0])
324 params['TEST_SUMM'] = get_test_summary(sec)
325 sec.name = sec.name.format(**params)
326 counter[0] += 1
327
328 return sec
329
330
331def get_test_sync_mode(sec):
koder aka kdanilovbc2c8982015-06-13 02:50:43 +0300332 if isinstance(sec, dict):
333 vals = sec
334 else:
335 vals = sec.vals
336
337 is_sync = str(vals.get("sync", "0")) == "1"
338 is_direct = str(vals.get("direct", "0")) == "1"
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300339
340 if is_sync and is_direct:
341 return 'x'
342 elif is_sync:
343 return 's'
344 elif is_direct:
345 return 'd'
346 else:
347 return 'a'
348
349
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300350TestSumm = namedtuple("TestSumm", ("oper", "mode", "bsize", "th_count", "vm_count"))
351
352
353def get_test_summary_tuple(sec, vm_count=None):
koder aka kdanilovbc2c8982015-06-13 02:50:43 +0300354 if isinstance(sec, dict):
355 vals = sec
356 else:
357 vals = sec.vals
358
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300359 rw = {"randread": "rr",
360 "randwrite": "rw",
361 "read": "sr",
koder aka kdanilov7248c7b2015-05-31 22:53:03 +0300362 "write": "sw",
363 "randrw": "rm",
364 "rw": "sm",
koder aka kdanilovbc2c8982015-06-13 02:50:43 +0300365 "readwrite": "sm"}[vals["rw"]]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300366
367 sync_mode = get_test_sync_mode(sec)
koder aka kdanilovbc2c8982015-06-13 02:50:43 +0300368 th_count = vals.get('numjobs')
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300369
370 if th_count is None:
koder aka kdanilovbc2c8982015-06-13 02:50:43 +0300371 th_count = vals.get('concurence', 1)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300372
koder aka kdanilov170936a2015-06-27 22:51:17 +0300373 return TestSumm(rw,
374 sync_mode,
375 vals['blocksize'],
376 th_count,
377 vm_count)
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300378
379
380def get_test_summary(sec, vm_count=None):
381 tpl = get_test_summary_tuple(sec, vm_count)
382 res = "{0.oper}{0.mode}{0.bsize}th{0.th_count}".format(tpl)
383
384 if tpl.vm_count is not None:
385 res += "vm" + str(tpl.vm_count)
386
387 return res
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300388
389
390def execution_time(sec):
391 return sec.vals.get('ramp_time', 0) + sec.vals.get('runtime', 0)
392
393
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300394def parse_all_in_1(source, fname=None):
395 return fio_config_parse(fio_config_lexer(source, fname))
396
397
398def flatmap(func, inp_iter):
399 for val in inp_iter:
400 for res in func(val):
401 yield res
402
403
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300404def fio_cfg_compile(source, fname, test_params):
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300405 it = parse_all_in_1(source, fname)
406 it = (apply_params(sec, test_params) for sec in it)
407 it = flatmap(process_cycles, it)
408 it = flatmap(process_repeats, it)
koder aka kdanilovbc2c8982015-06-13 02:50:43 +0300409 return itertools.imap(finall_process, it)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300410
411
412def parse_args(argv):
413 parser = argparse.ArgumentParser(
414 description="Run fio' and return result")
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300415 parser.add_argument("-p", "--params", nargs="*", metavar="PARAM=VAL",
416 default=[],
417 help="Provide set of pairs PARAM=VAL to" +
418 "format into job description")
419 parser.add_argument("action", choices=['estimate', 'compile', 'num_tests'])
420 parser.add_argument("jobfile")
421 return parser.parse_args(argv)
422
423
424def main(argv):
425 argv_obj = parse_args(argv)
426
427 if argv_obj.jobfile == '-':
428 job_cfg = sys.stdin.read()
429 else:
430 job_cfg = open(argv_obj.jobfile).read()
431
432 params = {}
433 for param_val in argv_obj.params:
434 assert '=' in param_val
435 name, val = param_val.split("=", 1)
436 params[name] = parse_value(val)
437
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300438 sec_it = fio_cfg_compile(job_cfg, argv_obj.jobfile, params)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300439
440 if argv_obj.action == 'estimate':
koder aka kdanilovbb6d6cd2015-06-20 02:55:07 +0300441 print sec_to_str(sum(map(execution_time, sec_it)))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300442 elif argv_obj.action == 'num_tests':
koder aka kdanilovbb6d6cd2015-06-20 02:55:07 +0300443 print sum(map(len, map(list, sec_it)))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300444 elif argv_obj.action == 'compile':
445 splitter = "\n#" + "-" * 70 + "\n\n"
koder aka kdanilovbb6d6cd2015-06-20 02:55:07 +0300446 print splitter.join(map(str, sec_it))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300447
448 return 0
449
450
451if __name__ == '__main__':
452 exit(main(sys.argv[1:]))