blob: 6790c97cd8906a0407c17f7ffbbf30feb8103c8d [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 kdanilov7f59d562016-12-26 01:34:23 +020014from ...result_classes import IStorable
koder aka kdanilov70227062016-11-26 23:23:21 +020015from ..itest import IterationConfig
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030016from ...utils import sec_to_str, ssize2b
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030017
18
19SECTION = 0
20SETTING = 1
21INCLUDE = 2
22
23
koder aka kdanilov70227062016-11-26 23:23:21 +020024Var = NamedTuple('Var', [('name', str)])
25CfgLine = NamedTuple('CfgLine',
26 [('fname', str),
27 ('lineno', int),
28 ('oline', str),
29 ('tp', int),
30 ('name', str),
31 ('val', Any)])
32
33TestSumm = NamedTuple("TestSumm",
34 [("oper", str),
35 ("mode", str),
36 ("bsize", int),
37 ("iodepth", int),
38 ("vm_count", int)])
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030039
40
koder aka kdanilov7f59d562016-12-26 01:34:23 +020041class FioJobSection(IterationConfig, IStorable):
42 yaml_tag = 'fio_job'
43
koder aka kdanilov70227062016-11-26 23:23:21 +020044 def __init__(self, name: str) -> None:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030045 self.name = name
koder aka kdanilov70227062016-11-26 23:23:21 +020046 self.vals = OrderedDict() # type: Dict[str, Any]
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +020047 self.summary = None
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030048
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030049 def copy(self) -> 'FioJobSection':
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030050 return copy.deepcopy(self)
51
koder aka kdanilov22d134e2016-11-08 11:33:19 +020052 def required_vars(self) -> Iterator[Tuple[str, Var]]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030053 for name, val in self.vals.items():
54 if isinstance(val, Var):
55 yield name, val
56
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030057 def is_free(self) -> bool:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030058 return len(list(self.required_vars())) == 0
59
koder aka kdanilov70227062016-11-26 23:23:21 +020060 def __str__(self) -> str:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030061 res = "[{0}]\n".format(self.name)
62
63 for name, val in self.vals.items():
64 if name.startswith('_') or name == name.upper():
65 continue
66 if isinstance(val, Var):
67 res += "{0}={{{1}}}\n".format(name, val.name)
68 else:
69 res += "{0}={1}\n".format(name, val)
70
71 return res
72
koder aka kdanilov7f59d562016-12-26 01:34:23 +020073 def raw(self) -> Dict[str, Any]:
74 return {
75 'name': self.name,
76 'vals': list(map(list, self.vals.items())),
77 'summary': self.summary
78 }
79
80 @classmethod
81 def fromraw(cls, data: Dict[str, Any]) -> 'FioJobSection':
82 obj = cls(data['name'])
83 obj.summary = data['summary']
84 obj.vals.update(data['vals'])
85 return obj
86
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030087
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030088class ParseError(ValueError):
koder aka kdanilov70227062016-11-26 23:23:21 +020089 def __init__(self, msg: str, fname: str, lineno: int, line_cont:Optional[str] = "") -> None:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030090 ValueError.__init__(self, msg)
91 self.file_name = fname
92 self.lineno = lineno
93 self.line_cont = line_cont
94
koder aka kdanilov70227062016-11-26 23:23:21 +020095 def __str__(self) -> str:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030096 msg = "In {0}:{1} ({2}) : {3}"
97 return msg.format(self.file_name,
98 self.lineno,
99 self.line_cont,
100 super(ParseError, self).__str__())
101
102
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300103def is_name(name: str) -> bool:
koder aka kdanilov70227062016-11-26 23:23:21 +0200104 return re.match("[a-zA-Z_][a-zA-Z_0-9]*", name) is not None
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300105
106
koder aka kdanilov70227062016-11-26 23:23:21 +0200107def parse_value(val: str) -> Union[int, str, float, List, Var]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300108 try:
109 return int(val)
110 except ValueError:
111 pass
112
113 try:
114 return float(val)
115 except ValueError:
116 pass
117
118 if val.startswith('{%'):
119 assert val.endswith("%}")
120 content = val[2:-2]
121 vals = list(i.strip() for i in content.split(','))
koder aka kdanilov70227062016-11-26 23:23:21 +0200122 return list(map(parse_value, vals))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300123
124 if val.startswith('{'):
125 assert val.endswith("}")
126 assert is_name(val[1:-1])
127 return Var(val[1:-1])
koder aka kdanilov70227062016-11-26 23:23:21 +0200128
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300129 return val
130
131
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200132def fio_config_lexer(fio_cfg: str, fname: str) -> Iterator[CfgLine]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300133 for lineno, oline in enumerate(fio_cfg.split("\n")):
134 try:
135 line = oline.strip()
136
137 if line.startswith("#") or line.startswith(";"):
138 continue
139
140 if line == "":
141 continue
142
143 if '#' in line:
144 raise ParseError("# isn't allowed inside line",
145 fname, lineno, oline)
146
147 if line.startswith('['):
148 yield CfgLine(fname, lineno, oline, SECTION,
149 line[1:-1].strip(), None)
150 elif '=' in line:
151 opt_name, opt_val = line.split('=', 1)
152 yield CfgLine(fname, lineno, oline, SETTING,
153 opt_name.strip(),
154 parse_value(opt_val.strip()))
155 elif line.startswith("include "):
156 yield CfgLine(fname, lineno, oline, INCLUDE,
157 line.split(" ", 1)[1], None)
158 else:
159 yield CfgLine(fname, lineno, oline, SETTING, line, '1')
160
161 except Exception as exc:
162 raise ParseError(str(exc), fname, lineno, oline)
163
164
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200165def fio_config_parse(lexer_iter: Iterable[CfgLine]) -> Iterator[FioJobSection]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300166 in_globals = False
167 curr_section = None
koder aka kdanilov70227062016-11-26 23:23:21 +0200168 glob_vals = OrderedDict() # type: Dict[str, Any]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300169 sections_count = 0
170
koder aka kdanilov70227062016-11-26 23:23:21 +0200171 lexed_lines = list(lexer_iter) # type: List[CfgLine]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300172 one_more = True
173 includes = {}
174
175 while one_more:
koder aka kdanilov70227062016-11-26 23:23:21 +0200176 new_lines = [] # type: List[CfgLine]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300177 one_more = False
178 for line in lexed_lines:
179 fname, lineno, oline, tp, name, val = line
180
181 if INCLUDE == tp:
182 if not os.path.exists(fname):
183 dirname = '.'
184 else:
185 dirname = os.path.dirname(fname)
186
187 new_fname = os.path.join(dirname, name)
188 includes[new_fname] = (fname, lineno)
189
190 try:
191 cont = open(new_fname).read()
192 except IOError as err:
193 msg = "Error while including file {0}: {1}"
194 raise ParseError(msg.format(new_fname, err),
195 fname, lineno, oline)
196
197 new_lines.extend(fio_config_lexer(cont, new_fname))
198 one_more = True
199 else:
200 new_lines.append(line)
201
202 lexed_lines = new_lines
203
204 for fname, lineno, oline, tp, name, val in lexed_lines:
205 if tp == SECTION:
206 if curr_section is not None:
207 yield curr_section
208 curr_section = None
209
210 if name == 'global':
211 if sections_count != 0:
212 raise ParseError("[global] section should" +
213 " be only one and first",
214 fname, lineno, oline)
215 in_globals = True
216 else:
217 in_globals = False
218 curr_section = FioJobSection(name)
219 curr_section.vals = glob_vals.copy()
220 sections_count += 1
221 else:
222 assert tp == SETTING
223 if in_globals:
224 glob_vals[name] = val
225 elif name == name.upper():
226 raise ParseError("Param '" + name +
227 "' not in [global] section",
228 fname, lineno, oline)
229 elif curr_section is None:
230 raise ParseError("Data outside section",
231 fname, lineno, oline)
232 else:
233 curr_section.vals[name] = val
234
235 if curr_section is not None:
236 yield curr_section
237
238
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200239def process_cycles(sec: FioJobSection) -> Iterator[FioJobSection]:
koder aka kdanilov70227062016-11-26 23:23:21 +0200240 cycles = OrderedDict() # type: Dict[str, Any]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300241
242 for name, val in sec.vals.items():
243 if isinstance(val, list) and name.upper() != name:
244 cycles[name] = val
245
246 if len(cycles) == 0:
247 yield sec
248 else:
koder aka kdanilov70227062016-11-26 23:23:21 +0200249 # iodepth should changes faster
250 numjobs = cycles.pop('iodepth', None)
251 items = list(cycles.items())
koder aka kdanilov170936a2015-06-27 22:51:17 +0300252
koder aka kdanilov70227062016-11-26 23:23:21 +0200253 if items:
koder aka kdanilov170936a2015-06-27 22:51:17 +0300254 keys, vals = zip(*items)
255 keys = list(keys)
256 vals = list(vals)
257 else:
258 keys = []
259 vals = []
260
261 if numjobs is not None:
262 vals.append(numjobs)
koder aka kdanilov70227062016-11-26 23:23:21 +0200263 keys.append('iodepth')
koder aka kdanilov170936a2015-06-27 22:51:17 +0300264
265 for combination in itertools.product(*vals):
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300266 new_sec = sec.copy()
koder aka kdanilov170936a2015-06-27 22:51:17 +0300267 new_sec.vals.update(zip(keys, combination))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300268 yield new_sec
269
270
koder aka kdanilov70227062016-11-26 23:23:21 +0200271FioParamsVal = Union[str, Var]
272FioParams = Dict[str, FioParamsVal]
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300273
274
koder aka kdanilov70227062016-11-26 23:23:21 +0200275def apply_params(sec: FioJobSection, params: FioParams) -> FioJobSection:
276 processed_vals = OrderedDict() # type: Dict[str, Any]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300277 processed_vals.update(params)
278 for name, val in sec.vals.items():
279 if name in params:
280 continue
281
282 if isinstance(val, Var):
283 if val.name in params:
284 val = params[val.name]
285 elif val.name in processed_vals:
286 val = processed_vals[val.name]
287 processed_vals[name] = val
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300288
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300289 sec = sec.copy()
290 sec.vals = processed_vals
291 return sec
292
293
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300294def abbv_name_to_full(name: str) -> str:
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300295 assert len(name) == 3
296
297 smode = {
298 'a': 'async',
299 's': 'sync',
300 'd': 'direct',
301 'x': 'sync direct'
302 }
303 off_mode = {'s': 'sequential', 'r': 'random'}
koder aka kdanilov7248c7b2015-05-31 22:53:03 +0300304 oper = {'r': 'read', 'w': 'write', 'm': 'mixed'}
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300305 return smode[name[2]] + " " + \
306 off_mode[name[0]] + " " + oper[name[1]]
307
308
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300309MAGIC_OFFSET = 0.1885
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300310
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300311
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +0200312def final_process(sec: FioJobSection, counter: List[int] = [0]) -> FioJobSection:
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300313 sec = sec.copy()
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300314
315 sec.vals['unified_rw_reporting'] = '1'
316
koder aka kdanilovbb6d6cd2015-06-20 02:55:07 +0300317 if isinstance(sec.vals['size'], Var):
318 raise ValueError("Variable {0} isn't provided".format(
319 sec.vals['size'].name))
320
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300321 sz = ssize2b(sec.vals['size'])
322 offset = sz * ((MAGIC_OFFSET * counter[0]) % 1.0)
323 offset = int(offset) // 1024 ** 2
324 new_vars = {'UNIQ_OFFSET': str(offset) + "m"}
325
326 for name, val in sec.vals.items():
327 if isinstance(val, Var):
328 if val.name in new_vars:
329 sec.vals[name] = new_vars[val.name]
330
koder aka kdanilovbb6d6cd2015-06-20 02:55:07 +0300331 for vl in sec.vals.values():
332 if isinstance(vl, Var):
333 raise ValueError("Variable {0} isn't provided".format(vl.name))
334
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300335 params = sec.vals.copy()
336 params['UNIQ'] = 'UN{0}'.format(counter[0])
337 params['COUNTER'] = str(counter[0])
338 params['TEST_SUMM'] = get_test_summary(sec)
339 sec.name = sec.name.format(**params)
340 counter[0] += 1
341
342 return sec
343
344
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300345def get_test_sync_mode(sec: FioJobSection) -> str:
koder aka kdanilovbc2c8982015-06-13 02:50:43 +0300346 if isinstance(sec, dict):
347 vals = sec
348 else:
349 vals = sec.vals
350
351 is_sync = str(vals.get("sync", "0")) == "1"
352 is_direct = str(vals.get("direct", "0")) == "1"
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300353
354 if is_sync and is_direct:
355 return 'x'
356 elif is_sync:
357 return 's'
358 elif is_direct:
359 return 'd'
360 else:
361 return 'a'
362
363
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200364def get_test_summary_tuple(sec: FioJobSection, vm_count: int = None) -> TestSumm:
koder aka kdanilovbc2c8982015-06-13 02:50:43 +0300365 if isinstance(sec, dict):
366 vals = sec
367 else:
368 vals = sec.vals
369
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300370 rw = {"randread": "rr",
371 "randwrite": "rw",
372 "read": "sr",
koder aka kdanilov7248c7b2015-05-31 22:53:03 +0300373 "write": "sw",
374 "randrw": "rm",
375 "rw": "sm",
koder aka kdanilovbc2c8982015-06-13 02:50:43 +0300376 "readwrite": "sm"}[vals["rw"]]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300377
378 sync_mode = get_test_sync_mode(sec)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300379
koder aka kdanilov170936a2015-06-27 22:51:17 +0300380 return TestSumm(rw,
381 sync_mode,
382 vals['blocksize'],
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +0200383 vals.get('iodepth', '1'),
koder aka kdanilov170936a2015-06-27 22:51:17 +0300384 vm_count)
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300385
386
koder aka kdanilov70227062016-11-26 23:23:21 +0200387def get_test_summary(sec: FioJobSection, vm_count: int = None, noiodepth: bool = False) -> str:
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300388 tpl = get_test_summary_tuple(sec, vm_count)
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300389
390 res = "{0.oper}{0.mode}{0.bsize}".format(tpl)
koder aka kdanilov70227062016-11-26 23:23:21 +0200391 if not noiodepth:
392 res += "qd{}".format(tpl.iodepth)
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300393
394 if tpl.vm_count is not None:
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300395 res += "vm{}".format(tpl.vm_count)
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300396
397 return res
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300398
399
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300400def execution_time(sec: FioJobSection) -> int:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300401 return sec.vals.get('ramp_time', 0) + sec.vals.get('runtime', 0)
402
403
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200404def parse_all_in_1(source:str, fname: str = None) -> Iterator[FioJobSection]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300405 return fio_config_parse(fio_config_lexer(source, fname))
406
407
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300408FM_FUNC_INPUT = TypeVar("FM_FUNC_INPUT")
409FM_FUNC_RES = TypeVar("FM_FUNC_RES")
410
411
412def flatmap(func: Callable[[FM_FUNC_INPUT], Iterable[FM_FUNC_RES]],
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200413 inp_iter: Iterable[FM_FUNC_INPUT]) -> Iterator[FM_FUNC_RES]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300414 for val in inp_iter:
415 for res in func(val):
416 yield res
417
418
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200419def get_log_files(sec: FioJobSection) -> List[Tuple[str, str]]:
420 res = [] # type: List[Tuple[str, str]]
421 for key, name in (('write_iops_log', 'iops'), ('write_bw_log', 'bw'), ('write_hist_log', 'lat')):
422 log = sec.vals.get(key)
423 if log is not None:
424 res.append((name, log))
425 return res
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +0200426
427
koder aka kdanilov70227062016-11-26 23:23:21 +0200428def fio_cfg_compile(source: str, fname: str, test_params: FioParams) -> Iterator[FioJobSection]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300429 it = parse_all_in_1(source, fname)
430 it = (apply_params(sec, test_params) for sec in it)
431 it = flatmap(process_cycles, it)
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200432 for sec in map(final_process, it):
433 sec.summary = get_test_summary(sec)
434 yield sec
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300435
436
437def parse_args(argv):
438 parser = argparse.ArgumentParser(
439 description="Run fio' and return result")
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300440 parser.add_argument("-p", "--params", nargs="*", metavar="PARAM=VAL",
441 default=[],
442 help="Provide set of pairs PARAM=VAL to" +
443 "format into job description")
444 parser.add_argument("action", choices=['estimate', 'compile', 'num_tests'])
445 parser.add_argument("jobfile")
446 return parser.parse_args(argv)
447
448
449def main(argv):
450 argv_obj = parse_args(argv)
451
452 if argv_obj.jobfile == '-':
453 job_cfg = sys.stdin.read()
454 else:
455 job_cfg = open(argv_obj.jobfile).read()
456
457 params = {}
458 for param_val in argv_obj.params:
459 assert '=' in param_val
460 name, val = param_val.split("=", 1)
461 params[name] = parse_value(val)
462
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300463 sec_it = fio_cfg_compile(job_cfg, argv_obj.jobfile, params)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300464
465 if argv_obj.action == 'estimate':
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300466 print(sec_to_str(sum(map(execution_time, sec_it))))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300467 elif argv_obj.action == 'num_tests':
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300468 print(sum(map(len, map(list, sec_it))))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300469 elif argv_obj.action == 'compile':
470 splitter = "\n#" + "-" * 70 + "\n\n"
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300471 print(splitter.join(map(str, sec_it)))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300472
473 return 0
474
475
476if __name__ == '__main__':
477 exit(main(sys.argv[1:]))