blob: bdcf4a31093e51c97e1201875b7ddfbfc5209bfe [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):
koder aka kdanilov70227062016-11-26 23:23:21 +020042 def __init__(self, name: str) -> None:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030043 self.name = name
koder aka kdanilov70227062016-11-26 23:23:21 +020044 self.vals = OrderedDict() # type: Dict[str, Any]
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +020045 self.summary = None
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030046
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030047 def copy(self) -> 'FioJobSection':
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030048 return copy.deepcopy(self)
49
koder aka kdanilov22d134e2016-11-08 11:33:19 +020050 def required_vars(self) -> Iterator[Tuple[str, Var]]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030051 for name, val in self.vals.items():
52 if isinstance(val, Var):
53 yield name, val
54
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030055 def is_free(self) -> bool:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030056 return len(list(self.required_vars())) == 0
57
koder aka kdanilov70227062016-11-26 23:23:21 +020058 def __str__(self) -> str:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030059 res = "[{0}]\n".format(self.name)
60
61 for name, val in self.vals.items():
62 if name.startswith('_') or name == name.upper():
63 continue
64 if isinstance(val, Var):
65 res += "{0}={{{1}}}\n".format(name, val.name)
66 else:
67 res += "{0}={1}\n".format(name, val)
68
69 return res
70
koder aka kdanilov7f59d562016-12-26 01:34:23 +020071 def raw(self) -> Dict[str, Any]:
72 return {
73 'name': self.name,
74 'vals': list(map(list, self.vals.items())),
75 'summary': self.summary
76 }
77
78 @classmethod
79 def fromraw(cls, data: Dict[str, Any]) -> 'FioJobSection':
80 obj = cls(data['name'])
81 obj.summary = data['summary']
82 obj.vals.update(data['vals'])
83 return obj
84
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030085
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030086class ParseError(ValueError):
koder aka kdanilov70227062016-11-26 23:23:21 +020087 def __init__(self, msg: str, fname: str, lineno: int, line_cont:Optional[str] = "") -> None:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030088 ValueError.__init__(self, msg)
89 self.file_name = fname
90 self.lineno = lineno
91 self.line_cont = line_cont
92
koder aka kdanilov70227062016-11-26 23:23:21 +020093 def __str__(self) -> str:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030094 msg = "In {0}:{1} ({2}) : {3}"
95 return msg.format(self.file_name,
96 self.lineno,
97 self.line_cont,
98 super(ParseError, self).__str__())
99
100
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300101def is_name(name: str) -> bool:
koder aka kdanilov70227062016-11-26 23:23:21 +0200102 return re.match("[a-zA-Z_][a-zA-Z_0-9]*", name) is not None
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300103
104
koder aka kdanilov70227062016-11-26 23:23:21 +0200105def parse_value(val: str) -> Union[int, str, float, List, Var]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300106 try:
107 return int(val)
108 except ValueError:
109 pass
110
111 try:
112 return float(val)
113 except ValueError:
114 pass
115
116 if val.startswith('{%'):
117 assert val.endswith("%}")
118 content = val[2:-2]
119 vals = list(i.strip() for i in content.split(','))
koder aka kdanilov70227062016-11-26 23:23:21 +0200120 return list(map(parse_value, vals))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300121
122 if val.startswith('{'):
123 assert val.endswith("}")
124 assert is_name(val[1:-1])
125 return Var(val[1:-1])
koder aka kdanilov70227062016-11-26 23:23:21 +0200126
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300127 return val
128
129
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200130def fio_config_lexer(fio_cfg: str, fname: str) -> Iterator[CfgLine]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300131 for lineno, oline in enumerate(fio_cfg.split("\n")):
132 try:
133 line = oline.strip()
134
135 if line.startswith("#") or line.startswith(";"):
136 continue
137
138 if line == "":
139 continue
140
141 if '#' in line:
142 raise ParseError("# isn't allowed inside line",
143 fname, lineno, oline)
144
145 if line.startswith('['):
146 yield CfgLine(fname, lineno, oline, SECTION,
147 line[1:-1].strip(), None)
148 elif '=' in line:
149 opt_name, opt_val = line.split('=', 1)
150 yield CfgLine(fname, lineno, oline, SETTING,
151 opt_name.strip(),
152 parse_value(opt_val.strip()))
153 elif line.startswith("include "):
154 yield CfgLine(fname, lineno, oline, INCLUDE,
155 line.split(" ", 1)[1], None)
156 else:
157 yield CfgLine(fname, lineno, oline, SETTING, line, '1')
158
159 except Exception as exc:
160 raise ParseError(str(exc), fname, lineno, oline)
161
162
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200163def fio_config_parse(lexer_iter: Iterable[CfgLine]) -> Iterator[FioJobSection]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300164 in_globals = False
165 curr_section = None
koder aka kdanilov70227062016-11-26 23:23:21 +0200166 glob_vals = OrderedDict() # type: Dict[str, Any]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300167 sections_count = 0
168
koder aka kdanilov70227062016-11-26 23:23:21 +0200169 lexed_lines = list(lexer_iter) # type: List[CfgLine]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300170 one_more = True
171 includes = {}
172
173 while one_more:
koder aka kdanilov70227062016-11-26 23:23:21 +0200174 new_lines = [] # type: List[CfgLine]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300175 one_more = False
176 for line in lexed_lines:
177 fname, lineno, oline, tp, name, val = line
178
179 if INCLUDE == tp:
180 if not os.path.exists(fname):
181 dirname = '.'
182 else:
183 dirname = os.path.dirname(fname)
184
185 new_fname = os.path.join(dirname, name)
186 includes[new_fname] = (fname, lineno)
187
188 try:
189 cont = open(new_fname).read()
190 except IOError as err:
191 msg = "Error while including file {0}: {1}"
192 raise ParseError(msg.format(new_fname, err),
193 fname, lineno, oline)
194
195 new_lines.extend(fio_config_lexer(cont, new_fname))
196 one_more = True
197 else:
198 new_lines.append(line)
199
200 lexed_lines = new_lines
201
202 for fname, lineno, oline, tp, name, val in lexed_lines:
203 if tp == SECTION:
204 if curr_section is not None:
205 yield curr_section
206 curr_section = None
207
208 if name == 'global':
209 if sections_count != 0:
210 raise ParseError("[global] section should" +
211 " be only one and first",
212 fname, lineno, oline)
213 in_globals = True
214 else:
215 in_globals = False
216 curr_section = FioJobSection(name)
217 curr_section.vals = glob_vals.copy()
218 sections_count += 1
219 else:
220 assert tp == SETTING
221 if in_globals:
222 glob_vals[name] = val
223 elif name == name.upper():
224 raise ParseError("Param '" + name +
225 "' not in [global] section",
226 fname, lineno, oline)
227 elif curr_section is None:
228 raise ParseError("Data outside section",
229 fname, lineno, oline)
230 else:
231 curr_section.vals[name] = val
232
233 if curr_section is not None:
234 yield curr_section
235
236
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200237def process_cycles(sec: FioJobSection) -> Iterator[FioJobSection]:
koder aka kdanilov70227062016-11-26 23:23:21 +0200238 cycles = OrderedDict() # type: Dict[str, Any]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300239
240 for name, val in sec.vals.items():
241 if isinstance(val, list) and name.upper() != name:
242 cycles[name] = val
243
244 if len(cycles) == 0:
245 yield sec
246 else:
koder aka kdanilov70227062016-11-26 23:23:21 +0200247 # iodepth should changes faster
248 numjobs = cycles.pop('iodepth', None)
249 items = list(cycles.items())
koder aka kdanilov170936a2015-06-27 22:51:17 +0300250
koder aka kdanilov70227062016-11-26 23:23:21 +0200251 if items:
koder aka kdanilov170936a2015-06-27 22:51:17 +0300252 keys, vals = zip(*items)
253 keys = list(keys)
254 vals = list(vals)
255 else:
256 keys = []
257 vals = []
258
259 if numjobs is not None:
260 vals.append(numjobs)
koder aka kdanilov70227062016-11-26 23:23:21 +0200261 keys.append('iodepth')
koder aka kdanilov170936a2015-06-27 22:51:17 +0300262
263 for combination in itertools.product(*vals):
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300264 new_sec = sec.copy()
koder aka kdanilov170936a2015-06-27 22:51:17 +0300265 new_sec.vals.update(zip(keys, combination))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300266 yield new_sec
267
268
koder aka kdanilov70227062016-11-26 23:23:21 +0200269FioParamsVal = Union[str, Var]
270FioParams = Dict[str, FioParamsVal]
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300271
272
koder aka kdanilov70227062016-11-26 23:23:21 +0200273def apply_params(sec: FioJobSection, params: FioParams) -> FioJobSection:
274 processed_vals = OrderedDict() # type: Dict[str, Any]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300275 processed_vals.update(params)
276 for name, val in sec.vals.items():
277 if name in params:
278 continue
279
280 if isinstance(val, Var):
281 if val.name in params:
282 val = params[val.name]
283 elif val.name in processed_vals:
284 val = processed_vals[val.name]
285 processed_vals[name] = val
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300286
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300287 sec = sec.copy()
288 sec.vals = processed_vals
289 return sec
290
291
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300292def abbv_name_to_full(name: str) -> str:
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300293 assert len(name) == 3
294
295 smode = {
296 'a': 'async',
297 's': 'sync',
298 'd': 'direct',
299 'x': 'sync direct'
300 }
301 off_mode = {'s': 'sequential', 'r': 'random'}
koder aka kdanilov7248c7b2015-05-31 22:53:03 +0300302 oper = {'r': 'read', 'w': 'write', 'm': 'mixed'}
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300303 return smode[name[2]] + " " + \
304 off_mode[name[0]] + " " + oper[name[1]]
305
306
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300307MAGIC_OFFSET = 0.1885
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300308
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300309
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +0200310def final_process(sec: FioJobSection, counter: List[int] = [0]) -> FioJobSection:
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300311 sec = sec.copy()
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300312
313 sec.vals['unified_rw_reporting'] = '1'
314
koder aka kdanilovbb6d6cd2015-06-20 02:55:07 +0300315 if isinstance(sec.vals['size'], Var):
316 raise ValueError("Variable {0} isn't provided".format(
317 sec.vals['size'].name))
318
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300319 sz = ssize2b(sec.vals['size'])
320 offset = sz * ((MAGIC_OFFSET * counter[0]) % 1.0)
321 offset = int(offset) // 1024 ** 2
322 new_vars = {'UNIQ_OFFSET': str(offset) + "m"}
323
324 for name, val in sec.vals.items():
325 if isinstance(val, Var):
326 if val.name in new_vars:
327 sec.vals[name] = new_vars[val.name]
328
koder aka kdanilovbb6d6cd2015-06-20 02:55:07 +0300329 for vl in sec.vals.values():
330 if isinstance(vl, Var):
331 raise ValueError("Variable {0} isn't provided".format(vl.name))
332
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300333 params = sec.vals.copy()
334 params['UNIQ'] = 'UN{0}'.format(counter[0])
335 params['COUNTER'] = str(counter[0])
336 params['TEST_SUMM'] = get_test_summary(sec)
337 sec.name = sec.name.format(**params)
338 counter[0] += 1
339
340 return sec
341
342
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300343def get_test_sync_mode(sec: FioJobSection) -> str:
koder aka kdanilovbc2c8982015-06-13 02:50:43 +0300344 if isinstance(sec, dict):
345 vals = sec
346 else:
347 vals = sec.vals
348
349 is_sync = str(vals.get("sync", "0")) == "1"
350 is_direct = str(vals.get("direct", "0")) == "1"
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300351
352 if is_sync and is_direct:
353 return 'x'
354 elif is_sync:
355 return 's'
356 elif is_direct:
357 return 'd'
358 else:
359 return 'a'
360
361
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200362def get_test_summary_tuple(sec: FioJobSection, vm_count: int = None) -> TestSumm:
koder aka kdanilovbc2c8982015-06-13 02:50:43 +0300363 if isinstance(sec, dict):
364 vals = sec
365 else:
366 vals = sec.vals
367
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300368 rw = {"randread": "rr",
369 "randwrite": "rw",
370 "read": "sr",
koder aka kdanilov7248c7b2015-05-31 22:53:03 +0300371 "write": "sw",
372 "randrw": "rm",
373 "rw": "sm",
koder aka kdanilovbc2c8982015-06-13 02:50:43 +0300374 "readwrite": "sm"}[vals["rw"]]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300375
376 sync_mode = get_test_sync_mode(sec)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300377
koder aka kdanilov170936a2015-06-27 22:51:17 +0300378 return TestSumm(rw,
379 sync_mode,
380 vals['blocksize'],
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +0200381 vals.get('iodepth', '1'),
koder aka kdanilov170936a2015-06-27 22:51:17 +0300382 vm_count)
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300383
384
koder aka kdanilov70227062016-11-26 23:23:21 +0200385def get_test_summary(sec: FioJobSection, vm_count: int = None, noiodepth: bool = False) -> str:
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300386 tpl = get_test_summary_tuple(sec, vm_count)
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300387
388 res = "{0.oper}{0.mode}{0.bsize}".format(tpl)
koder aka kdanilov70227062016-11-26 23:23:21 +0200389 if not noiodepth:
390 res += "qd{}".format(tpl.iodepth)
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300391
392 if tpl.vm_count is not None:
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300393 res += "vm{}".format(tpl.vm_count)
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300394
395 return res
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300396
397
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300398def execution_time(sec: FioJobSection) -> int:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300399 return sec.vals.get('ramp_time', 0) + sec.vals.get('runtime', 0)
400
401
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200402def parse_all_in_1(source:str, fname: str = None) -> Iterator[FioJobSection]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300403 return fio_config_parse(fio_config_lexer(source, fname))
404
405
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300406FM_FUNC_INPUT = TypeVar("FM_FUNC_INPUT")
407FM_FUNC_RES = TypeVar("FM_FUNC_RES")
408
409
410def flatmap(func: Callable[[FM_FUNC_INPUT], Iterable[FM_FUNC_RES]],
koder aka kdanilov22d134e2016-11-08 11:33:19 +0200411 inp_iter: Iterable[FM_FUNC_INPUT]) -> Iterator[FM_FUNC_RES]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300412 for val in inp_iter:
413 for res in func(val):
414 yield res
415
416
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200417def get_log_files(sec: FioJobSection) -> List[Tuple[str, str]]:
418 res = [] # type: List[Tuple[str, str]]
419 for key, name in (('write_iops_log', 'iops'), ('write_bw_log', 'bw'), ('write_hist_log', 'lat')):
420 log = sec.vals.get(key)
421 if log is not None:
422 res.append((name, log))
423 return res
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +0200424
425
koder aka kdanilov70227062016-11-26 23:23:21 +0200426def fio_cfg_compile(source: str, fname: str, test_params: FioParams) -> Iterator[FioJobSection]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300427 it = parse_all_in_1(source, fname)
428 it = (apply_params(sec, test_params) for sec in it)
429 it = flatmap(process_cycles, it)
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200430 for sec in map(final_process, it):
431 sec.summary = get_test_summary(sec)
432 yield sec
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300433
434
435def parse_args(argv):
436 parser = argparse.ArgumentParser(
437 description="Run fio' and return result")
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300438 parser.add_argument("-p", "--params", nargs="*", metavar="PARAM=VAL",
439 default=[],
440 help="Provide set of pairs PARAM=VAL to" +
441 "format into job description")
442 parser.add_argument("action", choices=['estimate', 'compile', 'num_tests'])
443 parser.add_argument("jobfile")
444 return parser.parse_args(argv)
445
446
447def main(argv):
448 argv_obj = parse_args(argv)
449
450 if argv_obj.jobfile == '-':
451 job_cfg = sys.stdin.read()
452 else:
453 job_cfg = open(argv_obj.jobfile).read()
454
455 params = {}
456 for param_val in argv_obj.params:
457 assert '=' in param_val
458 name, val = param_val.split("=", 1)
459 params[name] = parse_value(val)
460
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300461 sec_it = fio_cfg_compile(job_cfg, argv_obj.jobfile, params)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300462
463 if argv_obj.action == 'estimate':
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300464 print(sec_to_str(sum(map(execution_time, sec_it))))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300465 elif argv_obj.action == 'num_tests':
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300466 print(sum(map(len, map(list, sec_it))))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300467 elif argv_obj.action == 'compile':
468 splitter = "\n#" + "-" * 70 + "\n\n"
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300469 print(splitter.join(map(str, sec_it)))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300470
471 return 0
472
473
474if __name__ == '__main__':
475 exit(main(sys.argv[1:]))