blob: 8390e3aa57264c59cbe8db9e7cedfe3aa88f3801 [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 kdanilov23e6bdf2016-12-24 02:18:54 +020044 self.summary = None
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030045
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030046 def copy(self) -> 'FioJobSection':
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030047 return copy.deepcopy(self)
48
koder aka kdanilov22d134e2016-11-08 11:33:19 +020049 def required_vars(self) -> Iterator[Tuple[str, Var]]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030050 for name, val in self.vals.items():
51 if isinstance(val, Var):
52 yield name, val
53
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030054 def is_free(self) -> bool:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030055 return len(list(self.required_vars())) == 0
56
koder aka kdanilov70227062016-11-26 23:23:21 +020057 def __str__(self) -> str:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030058 res = "[{0}]\n".format(self.name)
59
60 for name, val in self.vals.items():
61 if name.startswith('_') or name == name.upper():
62 continue
63 if isinstance(val, Var):
64 res += "{0}={{{1}}}\n".format(name, val.name)
65 else:
66 res += "{0}={1}\n".format(name, val)
67
68 return res
69
70
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030071class ParseError(ValueError):
koder aka kdanilov70227062016-11-26 23:23:21 +020072 def __init__(self, msg: str, fname: str, lineno: int, line_cont:Optional[str] = "") -> None:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030073 ValueError.__init__(self, msg)
74 self.file_name = fname
75 self.lineno = lineno
76 self.line_cont = line_cont
77
koder aka kdanilov70227062016-11-26 23:23:21 +020078 def __str__(self) -> str:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030079 msg = "In {0}:{1} ({2}) : {3}"
80 return msg.format(self.file_name,
81 self.lineno,
82 self.line_cont,
83 super(ParseError, self).__str__())
84
85
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030086def is_name(name: str) -> bool:
koder aka kdanilov70227062016-11-26 23:23:21 +020087 return re.match("[a-zA-Z_][a-zA-Z_0-9]*", name) is not None
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030088
89
koder aka kdanilov70227062016-11-26 23:23:21 +020090def parse_value(val: str) -> Union[int, str, float, List, Var]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030091 try:
92 return int(val)
93 except ValueError:
94 pass
95
96 try:
97 return float(val)
98 except ValueError:
99 pass
100
101 if val.startswith('{%'):
102 assert val.endswith("%}")
103 content = val[2:-2]
104 vals = list(i.strip() for i in content.split(','))
koder aka kdanilov70227062016-11-26 23:23:21 +0200105 return list(map(parse_value, vals))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300106
107 if val.startswith('{'):
108 assert val.endswith("}")
109 assert is_name(val[1:-1])
110 return Var(val[1:-1])
koder aka kdanilov70227062016-11-26 23:23:21 +0200111
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300112 return val
113
114
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200115def fio_config_lexer(fio_cfg: str, fname: str) -> Iterator[CfgLine]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300116 for lineno, oline in enumerate(fio_cfg.split("\n")):
117 try:
118 line = oline.strip()
119
120 if line.startswith("#") or line.startswith(";"):
121 continue
122
123 if line == "":
124 continue
125
126 if '#' in line:
127 raise ParseError("# isn't allowed inside line",
128 fname, lineno, oline)
129
130 if line.startswith('['):
131 yield CfgLine(fname, lineno, oline, SECTION,
132 line[1:-1].strip(), None)
133 elif '=' in line:
134 opt_name, opt_val = line.split('=', 1)
135 yield CfgLine(fname, lineno, oline, SETTING,
136 opt_name.strip(),
137 parse_value(opt_val.strip()))
138 elif line.startswith("include "):
139 yield CfgLine(fname, lineno, oline, INCLUDE,
140 line.split(" ", 1)[1], None)
141 else:
142 yield CfgLine(fname, lineno, oline, SETTING, line, '1')
143
144 except Exception as exc:
145 raise ParseError(str(exc), fname, lineno, oline)
146
147
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200148def fio_config_parse(lexer_iter: Iterable[CfgLine]) -> Iterator[FioJobSection]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300149 in_globals = False
150 curr_section = None
koder aka kdanilov70227062016-11-26 23:23:21 +0200151 glob_vals = OrderedDict() # type: Dict[str, Any]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300152 sections_count = 0
153
koder aka kdanilov70227062016-11-26 23:23:21 +0200154 lexed_lines = list(lexer_iter) # type: List[CfgLine]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300155 one_more = True
156 includes = {}
157
158 while one_more:
koder aka kdanilov70227062016-11-26 23:23:21 +0200159 new_lines = [] # type: List[CfgLine]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300160 one_more = False
161 for line in lexed_lines:
162 fname, lineno, oline, tp, name, val = line
163
164 if INCLUDE == tp:
165 if not os.path.exists(fname):
166 dirname = '.'
167 else:
168 dirname = os.path.dirname(fname)
169
170 new_fname = os.path.join(dirname, name)
171 includes[new_fname] = (fname, lineno)
172
173 try:
174 cont = open(new_fname).read()
175 except IOError as err:
176 msg = "Error while including file {0}: {1}"
177 raise ParseError(msg.format(new_fname, err),
178 fname, lineno, oline)
179
180 new_lines.extend(fio_config_lexer(cont, new_fname))
181 one_more = True
182 else:
183 new_lines.append(line)
184
185 lexed_lines = new_lines
186
187 for fname, lineno, oline, tp, name, val in lexed_lines:
188 if tp == SECTION:
189 if curr_section is not None:
190 yield curr_section
191 curr_section = None
192
193 if name == 'global':
194 if sections_count != 0:
195 raise ParseError("[global] section should" +
196 " be only one and first",
197 fname, lineno, oline)
198 in_globals = True
199 else:
200 in_globals = False
201 curr_section = FioJobSection(name)
202 curr_section.vals = glob_vals.copy()
203 sections_count += 1
204 else:
205 assert tp == SETTING
206 if in_globals:
207 glob_vals[name] = val
208 elif name == name.upper():
209 raise ParseError("Param '" + name +
210 "' not in [global] section",
211 fname, lineno, oline)
212 elif curr_section is None:
213 raise ParseError("Data outside section",
214 fname, lineno, oline)
215 else:
216 curr_section.vals[name] = val
217
218 if curr_section is not None:
219 yield curr_section
220
221
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200222def process_cycles(sec: FioJobSection) -> Iterator[FioJobSection]:
koder aka kdanilov70227062016-11-26 23:23:21 +0200223 cycles = OrderedDict() # type: Dict[str, Any]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300224
225 for name, val in sec.vals.items():
226 if isinstance(val, list) and name.upper() != name:
227 cycles[name] = val
228
229 if len(cycles) == 0:
230 yield sec
231 else:
koder aka kdanilov70227062016-11-26 23:23:21 +0200232 # iodepth should changes faster
233 numjobs = cycles.pop('iodepth', None)
234 items = list(cycles.items())
koder aka kdanilov170936a2015-06-27 22:51:17 +0300235
koder aka kdanilov70227062016-11-26 23:23:21 +0200236 if items:
koder aka kdanilov170936a2015-06-27 22:51:17 +0300237 keys, vals = zip(*items)
238 keys = list(keys)
239 vals = list(vals)
240 else:
241 keys = []
242 vals = []
243
244 if numjobs is not None:
245 vals.append(numjobs)
koder aka kdanilov70227062016-11-26 23:23:21 +0200246 keys.append('iodepth')
koder aka kdanilov170936a2015-06-27 22:51:17 +0300247
248 for combination in itertools.product(*vals):
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300249 new_sec = sec.copy()
koder aka kdanilov170936a2015-06-27 22:51:17 +0300250 new_sec.vals.update(zip(keys, combination))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300251 yield new_sec
252
253
koder aka kdanilov70227062016-11-26 23:23:21 +0200254FioParamsVal = Union[str, Var]
255FioParams = Dict[str, FioParamsVal]
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300256
257
koder aka kdanilov70227062016-11-26 23:23:21 +0200258def apply_params(sec: FioJobSection, params: FioParams) -> FioJobSection:
259 processed_vals = OrderedDict() # type: Dict[str, Any]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300260 processed_vals.update(params)
261 for name, val in sec.vals.items():
262 if name in params:
263 continue
264
265 if isinstance(val, Var):
266 if val.name in params:
267 val = params[val.name]
268 elif val.name in processed_vals:
269 val = processed_vals[val.name]
270 processed_vals[name] = val
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300271
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300272 sec = sec.copy()
273 sec.vals = processed_vals
274 return sec
275
276
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300277def abbv_name_to_full(name: str) -> str:
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300278 assert len(name) == 3
279
280 smode = {
281 'a': 'async',
282 's': 'sync',
283 'd': 'direct',
284 'x': 'sync direct'
285 }
286 off_mode = {'s': 'sequential', 'r': 'random'}
koder aka kdanilov7248c7b2015-05-31 22:53:03 +0300287 oper = {'r': 'read', 'w': 'write', 'm': 'mixed'}
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300288 return smode[name[2]] + " " + \
289 off_mode[name[0]] + " " + oper[name[1]]
290
291
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300292MAGIC_OFFSET = 0.1885
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300293
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300294
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +0200295def final_process(sec: FioJobSection, counter: List[int] = [0]) -> FioJobSection:
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300296 sec = sec.copy()
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300297
298 sec.vals['unified_rw_reporting'] = '1'
299
koder aka kdanilovbb6d6cd2015-06-20 02:55:07 +0300300 if isinstance(sec.vals['size'], Var):
301 raise ValueError("Variable {0} isn't provided".format(
302 sec.vals['size'].name))
303
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300304 sz = ssize2b(sec.vals['size'])
305 offset = sz * ((MAGIC_OFFSET * counter[0]) % 1.0)
306 offset = int(offset) // 1024 ** 2
307 new_vars = {'UNIQ_OFFSET': str(offset) + "m"}
308
309 for name, val in sec.vals.items():
310 if isinstance(val, Var):
311 if val.name in new_vars:
312 sec.vals[name] = new_vars[val.name]
313
koder aka kdanilovbb6d6cd2015-06-20 02:55:07 +0300314 for vl in sec.vals.values():
315 if isinstance(vl, Var):
316 raise ValueError("Variable {0} isn't provided".format(vl.name))
317
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300318 params = sec.vals.copy()
319 params['UNIQ'] = 'UN{0}'.format(counter[0])
320 params['COUNTER'] = str(counter[0])
321 params['TEST_SUMM'] = get_test_summary(sec)
322 sec.name = sec.name.format(**params)
323 counter[0] += 1
324
325 return sec
326
327
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300328def get_test_sync_mode(sec: FioJobSection) -> str:
koder aka kdanilovbc2c8982015-06-13 02:50:43 +0300329 if isinstance(sec, dict):
330 vals = sec
331 else:
332 vals = sec.vals
333
334 is_sync = str(vals.get("sync", "0")) == "1"
335 is_direct = str(vals.get("direct", "0")) == "1"
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300336
337 if is_sync and is_direct:
338 return 'x'
339 elif is_sync:
340 return 's'
341 elif is_direct:
342 return 'd'
343 else:
344 return 'a'
345
346
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200347def get_test_summary_tuple(sec: FioJobSection, vm_count: int = None) -> TestSumm:
koder aka kdanilovbc2c8982015-06-13 02:50:43 +0300348 if isinstance(sec, dict):
349 vals = sec
350 else:
351 vals = sec.vals
352
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300353 rw = {"randread": "rr",
354 "randwrite": "rw",
355 "read": "sr",
koder aka kdanilov7248c7b2015-05-31 22:53:03 +0300356 "write": "sw",
357 "randrw": "rm",
358 "rw": "sm",
koder aka kdanilovbc2c8982015-06-13 02:50:43 +0300359 "readwrite": "sm"}[vals["rw"]]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300360
361 sync_mode = get_test_sync_mode(sec)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300362
koder aka kdanilov170936a2015-06-27 22:51:17 +0300363 return TestSumm(rw,
364 sync_mode,
365 vals['blocksize'],
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +0200366 vals.get('iodepth', '1'),
koder aka kdanilov170936a2015-06-27 22:51:17 +0300367 vm_count)
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300368
369
koder aka kdanilov70227062016-11-26 23:23:21 +0200370def get_test_summary(sec: FioJobSection, vm_count: int = None, noiodepth: bool = False) -> str:
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300371 tpl = get_test_summary_tuple(sec, vm_count)
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300372
373 res = "{0.oper}{0.mode}{0.bsize}".format(tpl)
koder aka kdanilov70227062016-11-26 23:23:21 +0200374 if not noiodepth:
375 res += "qd{}".format(tpl.iodepth)
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300376
377 if tpl.vm_count is not None:
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300378 res += "vm{}".format(tpl.vm_count)
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300379
380 return res
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300381
382
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300383def execution_time(sec: FioJobSection) -> int:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300384 return sec.vals.get('ramp_time', 0) + sec.vals.get('runtime', 0)
385
386
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200387def parse_all_in_1(source:str, fname: str = None) -> Iterator[FioJobSection]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300388 return fio_config_parse(fio_config_lexer(source, fname))
389
390
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300391FM_FUNC_INPUT = TypeVar("FM_FUNC_INPUT")
392FM_FUNC_RES = TypeVar("FM_FUNC_RES")
393
394
395def flatmap(func: Callable[[FM_FUNC_INPUT], Iterable[FM_FUNC_RES]],
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200396 inp_iter: Iterable[FM_FUNC_INPUT]) -> Iterator[FM_FUNC_RES]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300397 for val in inp_iter:
398 for res in func(val):
399 yield res
400
401
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200402def get_log_files(sec: FioJobSection) -> List[Tuple[str, str]]:
403 res = [] # type: List[Tuple[str, str]]
404 for key, name in (('write_iops_log', 'iops'), ('write_bw_log', 'bw'), ('write_hist_log', 'lat')):
405 log = sec.vals.get(key)
406 if log is not None:
407 res.append((name, log))
408 return res
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +0200409
410
koder aka kdanilov70227062016-11-26 23:23:21 +0200411def fio_cfg_compile(source: str, fname: str, test_params: FioParams) -> Iterator[FioJobSection]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300412 it = parse_all_in_1(source, fname)
413 it = (apply_params(sec, test_params) for sec in it)
414 it = flatmap(process_cycles, it)
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200415 for sec in map(final_process, it):
416 sec.summary = get_test_summary(sec)
417 yield sec
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300418
419
420def parse_args(argv):
421 parser = argparse.ArgumentParser(
422 description="Run fio' and return result")
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300423 parser.add_argument("-p", "--params", nargs="*", metavar="PARAM=VAL",
424 default=[],
425 help="Provide set of pairs PARAM=VAL to" +
426 "format into job description")
427 parser.add_argument("action", choices=['estimate', 'compile', 'num_tests'])
428 parser.add_argument("jobfile")
429 return parser.parse_args(argv)
430
431
432def main(argv):
433 argv_obj = parse_args(argv)
434
435 if argv_obj.jobfile == '-':
436 job_cfg = sys.stdin.read()
437 else:
438 job_cfg = open(argv_obj.jobfile).read()
439
440 params = {}
441 for param_val in argv_obj.params:
442 assert '=' in param_val
443 name, val = param_val.split("=", 1)
444 params[name] = parse_value(val)
445
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300446 sec_it = fio_cfg_compile(job_cfg, argv_obj.jobfile, params)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300447
448 if argv_obj.action == 'estimate':
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300449 print(sec_to_str(sum(map(execution_time, sec_it))))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300450 elif argv_obj.action == 'num_tests':
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300451 print(sum(map(len, map(list, sec_it))))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300452 elif argv_obj.action == 'compile':
453 splitter = "\n#" + "-" * 70 + "\n\n"
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300454 print(splitter.join(map(str, sec_it)))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300455
456 return 0
457
458
459if __name__ == '__main__':
460 exit(main(sys.argv[1:]))