blob: a9e13dcd9916a43500cb65bef7cbdc3c8f7887db [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 sys
kdanylov aka koder13e58452018-07-15 02:51:51 +03005import pathlib
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +03006import argparse
7import itertools
koder aka kdanilovf90de852017-01-20 18:12:27 +02008from typing import Optional, Iterator, Union, Dict, Iterable, List, Tuple, NamedTuple, Any
koder aka kdanilov70227062016-11-26 23:23:21 +02009from collections import OrderedDict
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030010
kdanylov aka koder026e5f22017-05-15 01:04:39 +030011from cephlib.units import ssize2b
12from cephlib.common import flatmap, sec_to_str
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030013
koder aka kdanilovf90de852017-01-20 18:12:27 +020014from .fio_job import Var, FioJobConfig
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030015
16SECTION = 0
17SETTING = 1
18INCLUDE = 2
19
20
koder aka kdanilov70227062016-11-26 23:23:21 +020021CfgLine = NamedTuple('CfgLine',
22 [('fname', str),
23 ('lineno', int),
24 ('oline', str),
25 ('tp', int),
26 ('name', str),
27 ('val', Any)])
koder aka kdanilov7f59d562016-12-26 01:34:23 +020028
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030029
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030030class ParseError(ValueError):
koder aka kdanilov70227062016-11-26 23:23:21 +020031 def __init__(self, msg: str, fname: str, lineno: int, line_cont:Optional[str] = "") -> None:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030032 ValueError.__init__(self, msg)
33 self.file_name = fname
34 self.lineno = lineno
35 self.line_cont = line_cont
36
koder aka kdanilov70227062016-11-26 23:23:21 +020037 def __str__(self) -> str:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030038 msg = "In {0}:{1} ({2}) : {3}"
39 return msg.format(self.file_name,
40 self.lineno,
41 self.line_cont,
42 super(ParseError, self).__str__())
43
44
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030045def is_name(name: str) -> bool:
koder aka kdanilov70227062016-11-26 23:23:21 +020046 return re.match("[a-zA-Z_][a-zA-Z_0-9]*", name) is not None
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030047
48
koder aka kdanilov70227062016-11-26 23:23:21 +020049def parse_value(val: str) -> Union[int, str, float, List, Var]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030050 try:
51 return int(val)
52 except ValueError:
53 pass
54
55 try:
56 return float(val)
57 except ValueError:
58 pass
59
60 if val.startswith('{%'):
61 assert val.endswith("%}")
62 content = val[2:-2]
63 vals = list(i.strip() for i in content.split(','))
koder aka kdanilov70227062016-11-26 23:23:21 +020064 return list(map(parse_value, vals))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030065
66 if val.startswith('{'):
67 assert val.endswith("}")
68 assert is_name(val[1:-1])
69 return Var(val[1:-1])
koder aka kdanilov70227062016-11-26 23:23:21 +020070
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030071 return val
72
73
koder aka kdanilov22d134e2016-11-08 11:33:19 +020074def fio_config_lexer(fio_cfg: str, fname: str) -> Iterator[CfgLine]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030075 for lineno, oline in enumerate(fio_cfg.split("\n")):
76 try:
77 line = oline.strip()
78
79 if line.startswith("#") or line.startswith(";"):
80 continue
81
82 if line == "":
83 continue
84
85 if '#' in line:
86 raise ParseError("# isn't allowed inside line",
87 fname, lineno, oline)
88
89 if line.startswith('['):
kdanylov aka koder026e5f22017-05-15 01:04:39 +030090 yield CfgLine(fname, lineno, oline, SECTION, line[1:-1].strip(), None)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030091 elif '=' in line:
92 opt_name, opt_val = line.split('=', 1)
kdanylov aka koder026e5f22017-05-15 01:04:39 +030093 yield CfgLine(fname, lineno, oline, SETTING, opt_name.strip(), parse_value(opt_val.strip()))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030094 elif line.startswith("include "):
kdanylov aka koder026e5f22017-05-15 01:04:39 +030095 yield CfgLine(fname, lineno, oline, INCLUDE, line.split(" ", 1)[1], None)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030096 else:
97 yield CfgLine(fname, lineno, oline, SETTING, line, '1')
98
99 except Exception as exc:
100 raise ParseError(str(exc), fname, lineno, oline)
101
102
koder aka kdanilovf2865172016-12-30 03:35:11 +0200103def fio_config_parse(lexer_iter: Iterable[CfgLine]) -> Iterator[FioJobConfig]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300104 in_globals = False
105 curr_section = None
kdanylov aka koder13e58452018-07-15 02:51:51 +0300106 glob_vals: Dict[str, Any] = OrderedDict()
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300107 sections_count = 0
108
kdanylov aka koder13e58452018-07-15 02:51:51 +0300109 lexed_lines: List[CfgLine] = list(lexer_iter)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300110 one_more = True
kdanylov aka koder13e58452018-07-15 02:51:51 +0300111 includes: Dict[str, Tuple[str, int]] = {}
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300112
113 while one_more:
kdanylov aka koder13e58452018-07-15 02:51:51 +0300114 new_lines: List[CfgLine] = []
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300115 one_more = False
116 for line in lexed_lines:
117 fname, lineno, oline, tp, name, val = line
118
119 if INCLUDE == tp:
kdanylov aka koder13e58452018-07-15 02:51:51 +0300120 fobj = pathlib.Path(fname)
121 new_fname = (fobj.parent / name) if fobj.exists() else pathlib.Path(name)
122 includes[str(new_fname)] = (fname, lineno)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300123
124 try:
kdanylov aka koder13e58452018-07-15 02:51:51 +0300125 cont = new_fname.open().read()
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300126 except IOError as err:
kdanylov aka koder13e58452018-07-15 02:51:51 +0300127 raise ParseError(f"Error while including file {new_fname}: {err}", fname, lineno, oline)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300128
kdanylov aka koder13e58452018-07-15 02:51:51 +0300129 new_lines.extend(fio_config_lexer(cont, str(new_fname)))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300130 one_more = True
131 else:
132 new_lines.append(line)
133
134 lexed_lines = new_lines
135
koder aka kdanilov108ac362017-01-19 20:17:16 +0200136 suite_section_idx = 0
137
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300138 for fname, lineno, oline, tp, name, val in lexed_lines:
139 if tp == SECTION:
140 if curr_section is not None:
141 yield curr_section
142 curr_section = None
143
144 if name == 'global':
145 if sections_count != 0:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200146 raise ParseError("[global] section should be only one and first", fname, lineno, oline)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300147 in_globals = True
148 else:
149 in_globals = False
koder aka kdanilov108ac362017-01-19 20:17:16 +0200150 curr_section = FioJobConfig(name, idx=suite_section_idx)
151 suite_section_idx += 1
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300152 curr_section.vals = glob_vals.copy()
153 sections_count += 1
154 else:
155 assert tp == SETTING
156 if in_globals:
157 glob_vals[name] = val
158 elif name == name.upper():
kdanylov aka koder13e58452018-07-15 02:51:51 +0300159 raise ParseError(f"Param {name!r} not in [global] section", fname, lineno, oline)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300160 elif curr_section is None:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200161 raise ParseError("Data outside section", fname, lineno, oline)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300162 else:
163 curr_section.vals[name] = val
164
165 if curr_section is not None:
166 yield curr_section
167
168
koder aka kdanilovf2865172016-12-30 03:35:11 +0200169def process_cycles(sec: FioJobConfig) -> Iterator[FioJobConfig]:
kdanylov aka koder13e58452018-07-15 02:51:51 +0300170 cycles: Dict[str, Any] = OrderedDict()
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300171
172 for name, val in sec.vals.items():
173 if isinstance(val, list) and name.upper() != name:
174 cycles[name] = val
175
176 if len(cycles) == 0:
177 yield sec
178 else:
koder aka kdanilov70227062016-11-26 23:23:21 +0200179 # iodepth should changes faster
180 numjobs = cycles.pop('iodepth', None)
181 items = list(cycles.items())
koder aka kdanilov170936a2015-06-27 22:51:17 +0300182
koder aka kdanilov70227062016-11-26 23:23:21 +0200183 if items:
koder aka kdanilov170936a2015-06-27 22:51:17 +0300184 keys, vals = zip(*items)
185 keys = list(keys)
186 vals = list(vals)
187 else:
188 keys = []
189 vals = []
190
191 if numjobs is not None:
192 vals.append(numjobs)
koder aka kdanilov70227062016-11-26 23:23:21 +0200193 keys.append('iodepth')
koder aka kdanilov170936a2015-06-27 22:51:17 +0300194
195 for combination in itertools.product(*vals):
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300196 new_sec = sec.copy()
koder aka kdanilov170936a2015-06-27 22:51:17 +0300197 new_sec.vals.update(zip(keys, combination))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300198 yield new_sec
199
200
kdanylov aka koder13e58452018-07-15 02:51:51 +0300201FioParamsVal = Union[str, Var, int]
koder aka kdanilov70227062016-11-26 23:23:21 +0200202FioParams = Dict[str, FioParamsVal]
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300203
204
koder aka kdanilovf2865172016-12-30 03:35:11 +0200205def apply_params(sec: FioJobConfig, params: FioParams) -> FioJobConfig:
kdanylov aka koder13e58452018-07-15 02:51:51 +0300206 processed_vals: Dict[str, Any] = OrderedDict()
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300207 processed_vals.update(params)
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300208
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300209 for name, val in sec.vals.items():
210 if name in params:
211 continue
212
213 if isinstance(val, Var):
214 if val.name in params:
215 val = params[val.name]
216 elif val.name in processed_vals:
217 val = processed_vals[val.name]
218 processed_vals[name] = val
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300219
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300220 sec = sec.copy()
221 sec.vals = processed_vals
222 return sec
223
224
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300225def abbv_name_to_full(name: str) -> str:
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300226 assert len(name) == 3
227
228 smode = {
229 'a': 'async',
230 's': 'sync',
231 'd': 'direct',
232 'x': 'sync direct'
233 }
234 off_mode = {'s': 'sequential', 'r': 'random'}
koder aka kdanilov7248c7b2015-05-31 22:53:03 +0300235 oper = {'r': 'read', 'w': 'write', 'm': 'mixed'}
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300236 return smode[name[2]] + " " + \
237 off_mode[name[0]] + " " + oper[name[1]]
238
239
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300240MAGIC_OFFSET = 0.1885
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300241
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300242
koder aka kdanilovf2865172016-12-30 03:35:11 +0200243def final_process(sec: FioJobConfig, counter: List[int] = [0]) -> FioJobConfig:
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300244 sec = sec.copy()
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300245
246 sec.vals['unified_rw_reporting'] = '1'
247
koder aka kdanilovbb6d6cd2015-06-20 02:55:07 +0300248 if isinstance(sec.vals['size'], Var):
kdanylov aka koder13e58452018-07-15 02:51:51 +0300249 raise ValueError(f"Variable {sec.vals['size'].name} isn't provided")
koder aka kdanilovbb6d6cd2015-06-20 02:55:07 +0300250
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300251 sz = ssize2b(sec.vals['size'])
252 offset = sz * ((MAGIC_OFFSET * counter[0]) % 1.0)
253 offset = int(offset) // 1024 ** 2
254 new_vars = {'UNIQ_OFFSET': str(offset) + "m"}
255
256 for name, val in sec.vals.items():
257 if isinstance(val, Var):
258 if val.name in new_vars:
259 sec.vals[name] = new_vars[val.name]
260
koder aka kdanilovbb6d6cd2015-06-20 02:55:07 +0300261 for vl in sec.vals.values():
262 if isinstance(vl, Var):
kdanylov aka koder13e58452018-07-15 02:51:51 +0300263 raise ValueError(f"Variable {vl.name} isn't provided")
koder aka kdanilovbb6d6cd2015-06-20 02:55:07 +0300264
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300265 params = sec.vals.copy()
266 params['UNIQ'] = 'UN{0}'.format(counter[0])
267 params['COUNTER'] = str(counter[0])
koder aka kdanilov108ac362017-01-19 20:17:16 +0200268 params['TEST_SUMM'] = sec.summary
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300269 sec.name = sec.name.format(**params)
270 counter[0] += 1
271
272 return sec
273
274
koder aka kdanilovf2865172016-12-30 03:35:11 +0200275def execution_time(sec: FioJobConfig) -> int:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300276 return sec.vals.get('ramp_time', 0) + sec.vals.get('runtime', 0)
277
278
kdanylov aka koder13e58452018-07-15 02:51:51 +0300279def parse_all_in_1(source:str, fname: str) -> Iterator[FioJobConfig]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300280 return fio_config_parse(fio_config_lexer(source, fname))
281
282
koder aka kdanilova732a602017-02-01 20:29:56 +0200283def get_log_files(sec: FioJobConfig, iops: bool = False) -> Iterator[Tuple[str, str, str]]:
kdanylov aka koder45183182017-04-30 23:55:40 +0300284 keys = [('write_bw_log', 'bw', 'KiBps'),
koder aka kdanilova732a602017-02-01 20:29:56 +0200285 ('write_hist_log', 'lat', 'us')]
koder aka kdanilov108ac362017-01-19 20:17:16 +0200286 if iops:
kdanylov aka koder45183182017-04-30 23:55:40 +0300287 keys.append(('write_iops_log', 'iops', 'IOPS'))
koder aka kdanilov108ac362017-01-19 20:17:16 +0200288
koder aka kdanilova732a602017-02-01 20:29:56 +0200289 for key, name, units in keys:
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200290 log = sec.vals.get(key)
291 if log is not None:
koder aka kdanilova732a602017-02-01 20:29:56 +0200292 yield (name, log, units)
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +0200293
294
koder aka kdanilovf2865172016-12-30 03:35:11 +0200295def fio_cfg_compile(source: str, fname: str, test_params: FioParams) -> Iterator[FioJobConfig]:
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300296 test_params = test_params.copy()
297
298 if 'RAMPTIME' not in test_params and 'RUNTIME' in test_params:
kdanylov aka koder13e58452018-07-15 02:51:51 +0300299 ramp = int(int(test_params['RUNTIME']) * 0.05) # type: ignore
300 test_params['RAMPTIME'] = min(30, max(5, ramp)) # type: ignore
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300301
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300302 it = parse_all_in_1(source, fname)
303 it = (apply_params(sec, test_params) for sec in it)
304 it = flatmap(process_cycles, it)
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200305 for sec in map(final_process, it):
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200306 yield sec
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300307
308
309def parse_args(argv):
310 parser = argparse.ArgumentParser(
311 description="Run fio' and return result")
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300312 parser.add_argument("-p", "--params", nargs="*", metavar="PARAM=VAL",
313 default=[],
314 help="Provide set of pairs PARAM=VAL to" +
315 "format into job description")
316 parser.add_argument("action", choices=['estimate', 'compile', 'num_tests'])
317 parser.add_argument("jobfile")
318 return parser.parse_args(argv)
319
320
321def main(argv):
322 argv_obj = parse_args(argv)
323
324 if argv_obj.jobfile == '-':
325 job_cfg = sys.stdin.read()
326 else:
327 job_cfg = open(argv_obj.jobfile).read()
328
329 params = {}
330 for param_val in argv_obj.params:
331 assert '=' in param_val
332 name, val = param_val.split("=", 1)
333 params[name] = parse_value(val)
334
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300335 sec_it = fio_cfg_compile(job_cfg, argv_obj.jobfile, params)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300336
337 if argv_obj.action == 'estimate':
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300338 print(sec_to_str(sum(map(execution_time, sec_it))))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300339 elif argv_obj.action == 'num_tests':
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300340 print(sum(map(len, map(list, sec_it))))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300341 elif argv_obj.action == 'compile':
342 splitter = "\n#" + "-" * 70 + "\n\n"
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300343 print(splitter.join(map(str, sec_it)))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300344
345 return 0
346
347
348if __name__ == '__main__':
349 exit(main(sys.argv[1:]))