blob: 1bdbb15e1fa83794b422547462dacfedb8d06de6 [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 kdanilov70227062016-11-26 23:23:21 +020010from typing import Optional, Iterator, Union, Dict, Iterable, List, TypeVar, Callable, Tuple, NamedTuple, Any
11from collections import OrderedDict
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030012
13
koder aka kdanilov70227062016-11-26 23:23:21 +020014from ..itest import IterationConfig
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030015from ...utils import sec_to_str, ssize2b
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030016
17
18SECTION = 0
19SETTING = 1
20INCLUDE = 2
21
22
koder aka kdanilov70227062016-11-26 23:23:21 +020023Var = NamedTuple('Var', [('name', str)])
24CfgLine = NamedTuple('CfgLine',
25 [('fname', str),
26 ('lineno', int),
27 ('oline', str),
28 ('tp', int),
29 ('name', str),
30 ('val', Any)])
31
32TestSumm = NamedTuple("TestSumm",
33 [("oper", str),
34 ("mode", str),
35 ("bsize", int),
36 ("iodepth", int),
37 ("vm_count", int)])
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030038
39
koder aka kdanilov70227062016-11-26 23:23:21 +020040class FioJobSection(IterationConfig):
41 def __init__(self, name: str) -> None:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030042 self.name = name
koder aka kdanilov70227062016-11-26 23:23:21 +020043 self.vals = OrderedDict() # type: Dict[str, Any]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030044
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030045 def copy(self) -> 'FioJobSection':
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030046 return copy.deepcopy(self)
47
koder aka kdanilov22d134e2016-11-08 11:33:19 +020048 def required_vars(self) -> Iterator[Tuple[str, Var]]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030049 for name, val in self.vals.items():
50 if isinstance(val, Var):
51 yield name, val
52
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030053 def is_free(self) -> bool:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030054 return len(list(self.required_vars())) == 0
55
koder aka kdanilov70227062016-11-26 23:23:21 +020056 def __str__(self) -> str:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030057 res = "[{0}]\n".format(self.name)
58
59 for name, val in self.vals.items():
60 if name.startswith('_') or name == name.upper():
61 continue
62 if isinstance(val, Var):
63 res += "{0}={{{1}}}\n".format(name, val.name)
64 else:
65 res += "{0}={1}\n".format(name, val)
66
67 return res
68
69
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030070class ParseError(ValueError):
koder aka kdanilov70227062016-11-26 23:23:21 +020071 def __init__(self, msg: str, fname: str, lineno: int, line_cont:Optional[str] = "") -> None:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030072 ValueError.__init__(self, msg)
73 self.file_name = fname
74 self.lineno = lineno
75 self.line_cont = line_cont
76
koder aka kdanilov70227062016-11-26 23:23:21 +020077 def __str__(self) -> str:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030078 msg = "In {0}:{1} ({2}) : {3}"
79 return msg.format(self.file_name,
80 self.lineno,
81 self.line_cont,
82 super(ParseError, self).__str__())
83
84
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030085def is_name(name: str) -> bool:
koder aka kdanilov70227062016-11-26 23:23:21 +020086 return re.match("[a-zA-Z_][a-zA-Z_0-9]*", name) is not None
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030087
88
koder aka kdanilov70227062016-11-26 23:23:21 +020089def parse_value(val: str) -> Union[int, str, float, List, Var]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030090 try:
91 return int(val)
92 except ValueError:
93 pass
94
95 try:
96 return float(val)
97 except ValueError:
98 pass
99
100 if val.startswith('{%'):
101 assert val.endswith("%}")
102 content = val[2:-2]
103 vals = list(i.strip() for i in content.split(','))
koder aka kdanilov70227062016-11-26 23:23:21 +0200104 return list(map(parse_value, vals))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300105
106 if val.startswith('{'):
107 assert val.endswith("}")
108 assert is_name(val[1:-1])
109 return Var(val[1:-1])
koder aka kdanilov70227062016-11-26 23:23:21 +0200110
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300111 return val
112
113
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200114def fio_config_lexer(fio_cfg: str, fname: str) -> Iterator[CfgLine]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300115 for lineno, oline in enumerate(fio_cfg.split("\n")):
116 try:
117 line = oline.strip()
118
119 if line.startswith("#") or line.startswith(";"):
120 continue
121
122 if line == "":
123 continue
124
125 if '#' in line:
126 raise ParseError("# isn't allowed inside line",
127 fname, lineno, oline)
128
129 if line.startswith('['):
130 yield CfgLine(fname, lineno, oline, SECTION,
131 line[1:-1].strip(), None)
132 elif '=' in line:
133 opt_name, opt_val = line.split('=', 1)
134 yield CfgLine(fname, lineno, oline, SETTING,
135 opt_name.strip(),
136 parse_value(opt_val.strip()))
137 elif line.startswith("include "):
138 yield CfgLine(fname, lineno, oline, INCLUDE,
139 line.split(" ", 1)[1], None)
140 else:
141 yield CfgLine(fname, lineno, oline, SETTING, line, '1')
142
143 except Exception as exc:
144 raise ParseError(str(exc), fname, lineno, oline)
145
146
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200147def fio_config_parse(lexer_iter: Iterable[CfgLine]) -> Iterator[FioJobSection]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300148 in_globals = False
149 curr_section = None
koder aka kdanilov70227062016-11-26 23:23:21 +0200150 glob_vals = OrderedDict() # type: Dict[str, Any]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300151 sections_count = 0
152
koder aka kdanilov70227062016-11-26 23:23:21 +0200153 lexed_lines = list(lexer_iter) # type: List[CfgLine]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300154 one_more = True
155 includes = {}
156
157 while one_more:
koder aka kdanilov70227062016-11-26 23:23:21 +0200158 new_lines = [] # type: List[CfgLine]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300159 one_more = False
160 for line in lexed_lines:
161 fname, lineno, oline, tp, name, val = line
162
163 if INCLUDE == tp:
164 if not os.path.exists(fname):
165 dirname = '.'
166 else:
167 dirname = os.path.dirname(fname)
168
169 new_fname = os.path.join(dirname, name)
170 includes[new_fname] = (fname, lineno)
171
172 try:
173 cont = open(new_fname).read()
174 except IOError as err:
175 msg = "Error while including file {0}: {1}"
176 raise ParseError(msg.format(new_fname, err),
177 fname, lineno, oline)
178
179 new_lines.extend(fio_config_lexer(cont, new_fname))
180 one_more = True
181 else:
182 new_lines.append(line)
183
184 lexed_lines = new_lines
185
186 for fname, lineno, oline, tp, name, val in lexed_lines:
187 if tp == SECTION:
188 if curr_section is not None:
189 yield curr_section
190 curr_section = None
191
192 if name == 'global':
193 if sections_count != 0:
194 raise ParseError("[global] section should" +
195 " be only one and first",
196 fname, lineno, oline)
197 in_globals = True
198 else:
199 in_globals = False
200 curr_section = FioJobSection(name)
201 curr_section.vals = glob_vals.copy()
202 sections_count += 1
203 else:
204 assert tp == SETTING
205 if in_globals:
206 glob_vals[name] = val
207 elif name == name.upper():
208 raise ParseError("Param '" + name +
209 "' not in [global] section",
210 fname, lineno, oline)
211 elif curr_section is None:
212 raise ParseError("Data outside section",
213 fname, lineno, oline)
214 else:
215 curr_section.vals[name] = val
216
217 if curr_section is not None:
218 yield curr_section
219
220
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200221def process_cycles(sec: FioJobSection) -> Iterator[FioJobSection]:
koder aka kdanilov70227062016-11-26 23:23:21 +0200222 cycles = OrderedDict() # type: Dict[str, Any]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300223
224 for name, val in sec.vals.items():
225 if isinstance(val, list) and name.upper() != name:
226 cycles[name] = val
227
228 if len(cycles) == 0:
229 yield sec
230 else:
koder aka kdanilov70227062016-11-26 23:23:21 +0200231 # iodepth should changes faster
232 numjobs = cycles.pop('iodepth', None)
233 items = list(cycles.items())
koder aka kdanilov170936a2015-06-27 22:51:17 +0300234
koder aka kdanilov70227062016-11-26 23:23:21 +0200235 if items:
koder aka kdanilov170936a2015-06-27 22:51:17 +0300236 keys, vals = zip(*items)
237 keys = list(keys)
238 vals = list(vals)
239 else:
240 keys = []
241 vals = []
242
243 if numjobs is not None:
244 vals.append(numjobs)
koder aka kdanilov70227062016-11-26 23:23:21 +0200245 keys.append('iodepth')
koder aka kdanilov170936a2015-06-27 22:51:17 +0300246
247 for combination in itertools.product(*vals):
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300248 new_sec = sec.copy()
koder aka kdanilov170936a2015-06-27 22:51:17 +0300249 new_sec.vals.update(zip(keys, combination))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300250 yield new_sec
251
252
koder aka kdanilov70227062016-11-26 23:23:21 +0200253FioParamsVal = Union[str, Var]
254FioParams = Dict[str, FioParamsVal]
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300255
256
koder aka kdanilov70227062016-11-26 23:23:21 +0200257def apply_params(sec: FioJobSection, params: FioParams) -> FioJobSection:
258 processed_vals = OrderedDict() # type: Dict[str, Any]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300259 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 kdanilov3b4da8b2016-10-17 00:17:53 +0300276def abbv_name_to_full(name: str) -> str:
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300277 assert len(name) == 3
278
279 smode = {
280 'a': 'async',
281 's': 'sync',
282 'd': 'direct',
283 'x': 'sync direct'
284 }
285 off_mode = {'s': 'sequential', 'r': 'random'}
koder aka kdanilov7248c7b2015-05-31 22:53:03 +0300286 oper = {'r': 'read', 'w': 'write', 'm': 'mixed'}
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300287 return smode[name[2]] + " " + \
288 off_mode[name[0]] + " " + oper[name[1]]
289
290
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300291MAGIC_OFFSET = 0.1885
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300292
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300293
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200294def finall_process(sec: FioJobSection, counter: List[int] = [0]) -> FioJobSection:
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300295 sec = sec.copy()
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300296
297 sec.vals['unified_rw_reporting'] = '1'
298
koder aka kdanilovbb6d6cd2015-06-20 02:55:07 +0300299 if isinstance(sec.vals['size'], Var):
300 raise ValueError("Variable {0} isn't provided".format(
301 sec.vals['size'].name))
302
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300303 sz = ssize2b(sec.vals['size'])
304 offset = sz * ((MAGIC_OFFSET * counter[0]) % 1.0)
305 offset = int(offset) // 1024 ** 2
306 new_vars = {'UNIQ_OFFSET': str(offset) + "m"}
307
308 for name, val in sec.vals.items():
309 if isinstance(val, Var):
310 if val.name in new_vars:
311 sec.vals[name] = new_vars[val.name]
312
koder aka kdanilovbb6d6cd2015-06-20 02:55:07 +0300313 for vl in sec.vals.values():
314 if isinstance(vl, Var):
315 raise ValueError("Variable {0} isn't provided".format(vl.name))
316
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300317 params = sec.vals.copy()
318 params['UNIQ'] = 'UN{0}'.format(counter[0])
319 params['COUNTER'] = str(counter[0])
320 params['TEST_SUMM'] = get_test_summary(sec)
321 sec.name = sec.name.format(**params)
322 counter[0] += 1
323
324 return sec
325
326
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300327def get_test_sync_mode(sec: FioJobSection) -> str:
koder aka kdanilovbc2c8982015-06-13 02:50:43 +0300328 if isinstance(sec, dict):
329 vals = sec
330 else:
331 vals = sec.vals
332
333 is_sync = str(vals.get("sync", "0")) == "1"
334 is_direct = str(vals.get("direct", "0")) == "1"
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300335
336 if is_sync and is_direct:
337 return 'x'
338 elif is_sync:
339 return 's'
340 elif is_direct:
341 return 'd'
342 else:
343 return 'a'
344
345
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200346def get_test_summary_tuple(sec: FioJobSection, vm_count: int = None) -> TestSumm:
koder aka kdanilovbc2c8982015-06-13 02:50:43 +0300347 if isinstance(sec, dict):
348 vals = sec
349 else:
350 vals = sec.vals
351
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300352 rw = {"randread": "rr",
353 "randwrite": "rw",
354 "read": "sr",
koder aka kdanilov7248c7b2015-05-31 22:53:03 +0300355 "write": "sw",
356 "randrw": "rm",
357 "rw": "sm",
koder aka kdanilovbc2c8982015-06-13 02:50:43 +0300358 "readwrite": "sm"}[vals["rw"]]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300359
360 sync_mode = get_test_sync_mode(sec)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300361
koder aka kdanilov170936a2015-06-27 22:51:17 +0300362 return TestSumm(rw,
363 sync_mode,
364 vals['blocksize'],
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300365 vals['iodepth'],
koder aka kdanilov170936a2015-06-27 22:51:17 +0300366 vm_count)
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300367
368
koder aka kdanilov70227062016-11-26 23:23:21 +0200369def get_test_summary(sec: FioJobSection, vm_count: int = None, noiodepth: bool = False) -> str:
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300370 tpl = get_test_summary_tuple(sec, vm_count)
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300371
372 res = "{0.oper}{0.mode}{0.bsize}".format(tpl)
koder aka kdanilov70227062016-11-26 23:23:21 +0200373 if not noiodepth:
374 res += "qd{}".format(tpl.iodepth)
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300375
376 if tpl.vm_count is not None:
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300377 res += "vm{}".format(tpl.vm_count)
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300378
379 return res
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300380
381
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300382def execution_time(sec: FioJobSection) -> int:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300383 return sec.vals.get('ramp_time', 0) + sec.vals.get('runtime', 0)
384
385
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200386def parse_all_in_1(source:str, fname: str = None) -> Iterator[FioJobSection]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300387 return fio_config_parse(fio_config_lexer(source, fname))
388
389
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300390FM_FUNC_INPUT = TypeVar("FM_FUNC_INPUT")
391FM_FUNC_RES = TypeVar("FM_FUNC_RES")
392
393
394def flatmap(func: Callable[[FM_FUNC_INPUT], Iterable[FM_FUNC_RES]],
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200395 inp_iter: Iterable[FM_FUNC_INPUT]) -> Iterator[FM_FUNC_RES]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300396 for val in inp_iter:
397 for res in func(val):
398 yield res
399
400
koder aka kdanilov70227062016-11-26 23:23:21 +0200401def fio_cfg_compile(source: str, fname: str, test_params: FioParams) -> Iterator[FioJobSection]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300402 it = parse_all_in_1(source, fname)
403 it = (apply_params(sec, test_params) for sec in it)
404 it = flatmap(process_cycles, it)
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300405 return map(finall_process, it)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300406
407
408def parse_args(argv):
409 parser = argparse.ArgumentParser(
410 description="Run fio' and return result")
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300411 parser.add_argument("-p", "--params", nargs="*", metavar="PARAM=VAL",
412 default=[],
413 help="Provide set of pairs PARAM=VAL to" +
414 "format into job description")
415 parser.add_argument("action", choices=['estimate', 'compile', 'num_tests'])
416 parser.add_argument("jobfile")
417 return parser.parse_args(argv)
418
419
420def main(argv):
421 argv_obj = parse_args(argv)
422
423 if argv_obj.jobfile == '-':
424 job_cfg = sys.stdin.read()
425 else:
426 job_cfg = open(argv_obj.jobfile).read()
427
428 params = {}
429 for param_val in argv_obj.params:
430 assert '=' in param_val
431 name, val = param_val.split("=", 1)
432 params[name] = parse_value(val)
433
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300434 sec_it = fio_cfg_compile(job_cfg, argv_obj.jobfile, params)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300435
436 if argv_obj.action == 'estimate':
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300437 print(sec_to_str(sum(map(execution_time, sec_it))))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300438 elif argv_obj.action == 'num_tests':
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300439 print(sum(map(len, map(list, sec_it))))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300440 elif argv_obj.action == 'compile':
441 splitter = "\n#" + "-" * 70 + "\n\n"
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300442 print(splitter.join(map(str, sec_it)))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300443
444 return 0
445
446
447if __name__ == '__main__':
448 exit(main(sys.argv[1:]))