blob: 233f6e21d29998baaef6b38bc0ef36d4e8b46ee5 [file] [log] [blame]
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +03001#!/usr/bin/env python3
2
3import re
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +03004import os
5import sys
6import copy
7import os.path
8import argparse
9import itertools
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030010from typing import Optional, Generator, Union, Dict, Iterable, Any, List, TypeVar, Callable
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030011from collections import OrderedDict, namedtuple
12
13
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030014from ...utils import sec_to_str, ssize2b
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030015
16
17SECTION = 0
18SETTING = 1
19INCLUDE = 2
20
21
22Var = namedtuple('Var', ('name',))
23CfgLine = namedtuple('CfgLine', ('fname', 'lineno', 'oline',
24 'tp', 'name', 'val'))
25
26
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030027class FioJobSection:
28 def __init__(self, name: str):
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030029 self.name = name
30 self.vals = OrderedDict()
31
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030032 def copy(self) -> 'FioJobSection':
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030033 return copy.deepcopy(self)
34
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030035 def required_vars(self) -> Generator[str, Var]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030036 for name, val in self.vals.items():
37 if isinstance(val, Var):
38 yield name, val
39
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030040 def is_free(self) -> bool:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030041 return len(list(self.required_vars())) == 0
42
43 def __str__(self):
44 res = "[{0}]\n".format(self.name)
45
46 for name, val in self.vals.items():
47 if name.startswith('_') or name == name.upper():
48 continue
49 if isinstance(val, Var):
50 res += "{0}={{{1}}}\n".format(name, val.name)
51 else:
52 res += "{0}={1}\n".format(name, val)
53
54 return res
55
56
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030057class ParseError(ValueError):
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030058 def __init__(self, msg: str, fname: str, lineno: int, line_cont:Optional[str] =""):
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030059 ValueError.__init__(self, msg)
60 self.file_name = fname
61 self.lineno = lineno
62 self.line_cont = line_cont
63
64 def __str__(self):
65 msg = "In {0}:{1} ({2}) : {3}"
66 return msg.format(self.file_name,
67 self.lineno,
68 self.line_cont,
69 super(ParseError, self).__str__())
70
71
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030072def is_name(name: str) -> bool:
73 return re.match("[a-zA-Z_][a-zA-Z_0-9]*", name)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030074
75
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030076def parse_value(val: str) -> Union[int, str, Dict, Var]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030077 try:
78 return int(val)
79 except ValueError:
80 pass
81
82 try:
83 return float(val)
84 except ValueError:
85 pass
86
87 if val.startswith('{%'):
88 assert val.endswith("%}")
89 content = val[2:-2]
90 vals = list(i.strip() for i in content.split(','))
91 return map(parse_value, vals)
92
93 if val.startswith('{'):
94 assert val.endswith("}")
95 assert is_name(val[1:-1])
96 return Var(val[1:-1])
97 return val
98
99
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300100def fio_config_lexer(fio_cfg: str, fname: str) -> Generator[CfgLine]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300101 for lineno, oline in enumerate(fio_cfg.split("\n")):
102 try:
103 line = oline.strip()
104
105 if line.startswith("#") or line.startswith(";"):
106 continue
107
108 if line == "":
109 continue
110
111 if '#' in line:
112 raise ParseError("# isn't allowed inside line",
113 fname, lineno, oline)
114
115 if line.startswith('['):
116 yield CfgLine(fname, lineno, oline, SECTION,
117 line[1:-1].strip(), None)
118 elif '=' in line:
119 opt_name, opt_val = line.split('=', 1)
120 yield CfgLine(fname, lineno, oline, SETTING,
121 opt_name.strip(),
122 parse_value(opt_val.strip()))
123 elif line.startswith("include "):
124 yield CfgLine(fname, lineno, oline, INCLUDE,
125 line.split(" ", 1)[1], None)
126 else:
127 yield CfgLine(fname, lineno, oline, SETTING, line, '1')
128
129 except Exception as exc:
130 raise ParseError(str(exc), fname, lineno, oline)
131
132
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300133def fio_config_parse(lexer_iter: Iterable[CfgLine]) -> Generator[FioJobSection]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300134 in_globals = False
135 curr_section = None
136 glob_vals = OrderedDict()
137 sections_count = 0
138
139 lexed_lines = list(lexer_iter)
140 one_more = True
141 includes = {}
142
143 while one_more:
144 new_lines = []
145 one_more = False
146 for line in lexed_lines:
147 fname, lineno, oline, tp, name, val = line
148
149 if INCLUDE == tp:
150 if not os.path.exists(fname):
151 dirname = '.'
152 else:
153 dirname = os.path.dirname(fname)
154
155 new_fname = os.path.join(dirname, name)
156 includes[new_fname] = (fname, lineno)
157
158 try:
159 cont = open(new_fname).read()
160 except IOError as err:
161 msg = "Error while including file {0}: {1}"
162 raise ParseError(msg.format(new_fname, err),
163 fname, lineno, oline)
164
165 new_lines.extend(fio_config_lexer(cont, new_fname))
166 one_more = True
167 else:
168 new_lines.append(line)
169
170 lexed_lines = new_lines
171
172 for fname, lineno, oline, tp, name, val in lexed_lines:
173 if tp == SECTION:
174 if curr_section is not None:
175 yield curr_section
176 curr_section = None
177
178 if name == 'global':
179 if sections_count != 0:
180 raise ParseError("[global] section should" +
181 " be only one and first",
182 fname, lineno, oline)
183 in_globals = True
184 else:
185 in_globals = False
186 curr_section = FioJobSection(name)
187 curr_section.vals = glob_vals.copy()
188 sections_count += 1
189 else:
190 assert tp == SETTING
191 if in_globals:
192 glob_vals[name] = val
193 elif name == name.upper():
194 raise ParseError("Param '" + name +
195 "' not in [global] section",
196 fname, lineno, oline)
197 elif curr_section is None:
198 raise ParseError("Data outside section",
199 fname, lineno, oline)
200 else:
201 curr_section.vals[name] = val
202
203 if curr_section is not None:
204 yield curr_section
205
206
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300207def process_cycles(sec: FioJobSection) -> Generator[FioJobSection]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300208 cycles = OrderedDict()
209
210 for name, val in sec.vals.items():
211 if isinstance(val, list) and name.upper() != name:
212 cycles[name] = val
213
214 if len(cycles) == 0:
215 yield sec
216 else:
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300217 # qd should changes faster
218 numjobs = cycles.pop('qd', None)
koder aka kdanilov170936a2015-06-27 22:51:17 +0300219 items = cycles.items()
220
221 if len(items) > 0:
222 keys, vals = zip(*items)
223 keys = list(keys)
224 vals = list(vals)
225 else:
226 keys = []
227 vals = []
228
229 if numjobs is not None:
230 vals.append(numjobs)
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300231 keys.append('qd')
koder aka kdanilov170936a2015-06-27 22:51:17 +0300232
233 for combination in itertools.product(*vals):
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300234 new_sec = sec.copy()
koder aka kdanilov170936a2015-06-27 22:51:17 +0300235 new_sec.vals.update(zip(keys, combination))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300236 yield new_sec
237
238
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300239FIO_PARAM_VAL = Union[str, Var]
240FIO_PARAMS = Dict[str, FIO_PARAM_VAL]
241
242
243def apply_params(sec: FioJobSection, params: FIO_PARAMS) -> FioJobSection:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300244 processed_vals = OrderedDict()
245 processed_vals.update(params)
246 for name, val in sec.vals.items():
247 if name in params:
248 continue
249
250 if isinstance(val, Var):
251 if val.name in params:
252 val = params[val.name]
253 elif val.name in processed_vals:
254 val = processed_vals[val.name]
255 processed_vals[name] = val
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300256
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300257 sec = sec.copy()
258 sec.vals = processed_vals
259 return sec
260
261
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300262def abbv_name_to_full(name: str) -> str:
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300263 assert len(name) == 3
264
265 smode = {
266 'a': 'async',
267 's': 'sync',
268 'd': 'direct',
269 'x': 'sync direct'
270 }
271 off_mode = {'s': 'sequential', 'r': 'random'}
koder aka kdanilov7248c7b2015-05-31 22:53:03 +0300272 oper = {'r': 'read', 'w': 'write', 'm': 'mixed'}
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300273 return smode[name[2]] + " " + \
274 off_mode[name[0]] + " " + oper[name[1]]
275
276
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300277MAGIC_OFFSET = 0.1885
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300278
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300279
280def finall_process(sec: FioJobSection, counter: Optional[List[int]] = [0]) -> FioJobSection:
281 sec = sec.copy()
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300282
283 sec.vals['unified_rw_reporting'] = '1'
284
koder aka kdanilovbb6d6cd2015-06-20 02:55:07 +0300285 if isinstance(sec.vals['size'], Var):
286 raise ValueError("Variable {0} isn't provided".format(
287 sec.vals['size'].name))
288
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300289 sz = ssize2b(sec.vals['size'])
290 offset = sz * ((MAGIC_OFFSET * counter[0]) % 1.0)
291 offset = int(offset) // 1024 ** 2
292 new_vars = {'UNIQ_OFFSET': str(offset) + "m"}
293
294 for name, val in sec.vals.items():
295 if isinstance(val, Var):
296 if val.name in new_vars:
297 sec.vals[name] = new_vars[val.name]
298
koder aka kdanilovbb6d6cd2015-06-20 02:55:07 +0300299 for vl in sec.vals.values():
300 if isinstance(vl, Var):
301 raise ValueError("Variable {0} isn't provided".format(vl.name))
302
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300303 params = sec.vals.copy()
304 params['UNIQ'] = 'UN{0}'.format(counter[0])
305 params['COUNTER'] = str(counter[0])
306 params['TEST_SUMM'] = get_test_summary(sec)
307 sec.name = sec.name.format(**params)
308 counter[0] += 1
309
310 return sec
311
312
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300313def get_test_sync_mode(sec: FioJobSection) -> str:
koder aka kdanilovbc2c8982015-06-13 02:50:43 +0300314 if isinstance(sec, dict):
315 vals = sec
316 else:
317 vals = sec.vals
318
319 is_sync = str(vals.get("sync", "0")) == "1"
320 is_direct = str(vals.get("direct", "0")) == "1"
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300321
322 if is_sync and is_direct:
323 return 'x'
324 elif is_sync:
325 return 's'
326 elif is_direct:
327 return 'd'
328 else:
329 return 'a'
330
331
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300332TestSumm = namedtuple("TestSumm", ("oper", "mode", "bsize", "iodepth", "vm_count"))
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300333
334
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300335def get_test_summary_tuple(sec: FioJobSection, vm_count=None) -> TestSumm:
koder aka kdanilovbc2c8982015-06-13 02:50:43 +0300336 if isinstance(sec, dict):
337 vals = sec
338 else:
339 vals = sec.vals
340
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300341 rw = {"randread": "rr",
342 "randwrite": "rw",
343 "read": "sr",
koder aka kdanilov7248c7b2015-05-31 22:53:03 +0300344 "write": "sw",
345 "randrw": "rm",
346 "rw": "sm",
koder aka kdanilovbc2c8982015-06-13 02:50:43 +0300347 "readwrite": "sm"}[vals["rw"]]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300348
349 sync_mode = get_test_sync_mode(sec)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300350
koder aka kdanilov170936a2015-06-27 22:51:17 +0300351 return TestSumm(rw,
352 sync_mode,
353 vals['blocksize'],
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300354 vals['iodepth'],
koder aka kdanilov170936a2015-06-27 22:51:17 +0300355 vm_count)
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300356
357
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300358def get_test_summary(sec: FioJobSection, vm_count: int=None, noqd: Optional[bool]=False) -> str:
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300359 tpl = get_test_summary_tuple(sec, vm_count)
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300360
361 res = "{0.oper}{0.mode}{0.bsize}".format(tpl)
362 if not noqd:
363 res += "qd{}".format(tpl.qd)
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300364
365 if tpl.vm_count is not None:
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300366 res += "vm{}".format(tpl.vm_count)
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300367
368 return res
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300369
370
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300371def execution_time(sec: FioJobSection) -> int:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300372 return sec.vals.get('ramp_time', 0) + sec.vals.get('runtime', 0)
373
374
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300375def parse_all_in_1(source:str, fname: str=None) -> Generator[FioJobSection]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300376 return fio_config_parse(fio_config_lexer(source, fname))
377
378
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300379FM_FUNC_INPUT = TypeVar("FM_FUNC_INPUT")
380FM_FUNC_RES = TypeVar("FM_FUNC_RES")
381
382
383def flatmap(func: Callable[[FM_FUNC_INPUT], Iterable[FM_FUNC_RES]],
384 inp_iter: Iterable[FM_FUNC_INPUT]) -> Generator[FM_FUNC_RES]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300385 for val in inp_iter:
386 for res in func(val):
387 yield res
388
389
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300390def fio_cfg_compile(source: str, fname: str, test_params: FIO_PARAMS) -> Generator[FioJobSection]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300391 it = parse_all_in_1(source, fname)
392 it = (apply_params(sec, test_params) for sec in it)
393 it = flatmap(process_cycles, it)
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300394 return map(finall_process, it)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300395
396
397def parse_args(argv):
398 parser = argparse.ArgumentParser(
399 description="Run fio' and return result")
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300400 parser.add_argument("-p", "--params", nargs="*", metavar="PARAM=VAL",
401 default=[],
402 help="Provide set of pairs PARAM=VAL to" +
403 "format into job description")
404 parser.add_argument("action", choices=['estimate', 'compile', 'num_tests'])
405 parser.add_argument("jobfile")
406 return parser.parse_args(argv)
407
408
409def main(argv):
410 argv_obj = parse_args(argv)
411
412 if argv_obj.jobfile == '-':
413 job_cfg = sys.stdin.read()
414 else:
415 job_cfg = open(argv_obj.jobfile).read()
416
417 params = {}
418 for param_val in argv_obj.params:
419 assert '=' in param_val
420 name, val = param_val.split("=", 1)
421 params[name] = parse_value(val)
422
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300423 sec_it = fio_cfg_compile(job_cfg, argv_obj.jobfile, params)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300424
425 if argv_obj.action == 'estimate':
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300426 print(sec_to_str(sum(map(execution_time, sec_it))))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300427 elif argv_obj.action == 'num_tests':
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300428 print(sum(map(len, map(list, sec_it))))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300429 elif argv_obj.action == 'compile':
430 splitter = "\n#" + "-" * 70 + "\n\n"
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300431 print(splitter.join(map(str, sec_it)))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300432
433 return 0
434
435
436if __name__ == '__main__':
437 exit(main(sys.argv[1:]))