blob: 5b91885137c5f51e85acca956214912a806fe8c0 [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
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +03006import os.path
7import argparse
8import itertools
koder aka kdanilovf90de852017-01-20 18:12:27 +02009from typing import Optional, Iterator, Union, Dict, Iterable, List, Tuple, NamedTuple, Any
koder aka kdanilov70227062016-11-26 23:23:21 +020010from collections import OrderedDict
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030011
kdanylov aka koder026e5f22017-05-15 01:04:39 +030012from cephlib.units import ssize2b
13from cephlib.common import flatmap, sec_to_str
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030014
koder aka kdanilovf90de852017-01-20 18:12:27 +020015from .fio_job import Var, FioJobConfig
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030016
17SECTION = 0
18SETTING = 1
19INCLUDE = 2
20
21
koder aka kdanilov70227062016-11-26 23:23:21 +020022CfgLine = NamedTuple('CfgLine',
23 [('fname', str),
24 ('lineno', int),
25 ('oline', str),
26 ('tp', int),
27 ('name', str),
28 ('val', Any)])
koder aka kdanilov7f59d562016-12-26 01:34:23 +020029
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030030
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030031class ParseError(ValueError):
koder aka kdanilov70227062016-11-26 23:23:21 +020032 def __init__(self, msg: str, fname: str, lineno: int, line_cont:Optional[str] = "") -> None:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030033 ValueError.__init__(self, msg)
34 self.file_name = fname
35 self.lineno = lineno
36 self.line_cont = line_cont
37
koder aka kdanilov70227062016-11-26 23:23:21 +020038 def __str__(self) -> str:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030039 msg = "In {0}:{1} ({2}) : {3}"
40 return msg.format(self.file_name,
41 self.lineno,
42 self.line_cont,
43 super(ParseError, self).__str__())
44
45
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +030046def is_name(name: str) -> bool:
koder aka kdanilov70227062016-11-26 23:23:21 +020047 return re.match("[a-zA-Z_][a-zA-Z_0-9]*", name) is not None
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030048
49
koder aka kdanilov70227062016-11-26 23:23:21 +020050def parse_value(val: str) -> Union[int, str, float, List, Var]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030051 try:
52 return int(val)
53 except ValueError:
54 pass
55
56 try:
57 return float(val)
58 except ValueError:
59 pass
60
61 if val.startswith('{%'):
62 assert val.endswith("%}")
63 content = val[2:-2]
64 vals = list(i.strip() for i in content.split(','))
koder aka kdanilov70227062016-11-26 23:23:21 +020065 return list(map(parse_value, vals))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030066
67 if val.startswith('{'):
68 assert val.endswith("}")
69 assert is_name(val[1:-1])
70 return Var(val[1:-1])
koder aka kdanilov70227062016-11-26 23:23:21 +020071
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030072 return val
73
74
koder aka kdanilov22d134e2016-11-08 11:33:19 +020075def fio_config_lexer(fio_cfg: str, fname: str) -> Iterator[CfgLine]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030076 for lineno, oline in enumerate(fio_cfg.split("\n")):
77 try:
78 line = oline.strip()
79
80 if line.startswith("#") or line.startswith(";"):
81 continue
82
83 if line == "":
84 continue
85
86 if '#' in line:
87 raise ParseError("# isn't allowed inside line",
88 fname, lineno, oline)
89
90 if line.startswith('['):
kdanylov aka koder026e5f22017-05-15 01:04:39 +030091 yield CfgLine(fname, lineno, oline, SECTION, line[1:-1].strip(), None)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030092 elif '=' in line:
93 opt_name, opt_val = line.split('=', 1)
kdanylov aka koder026e5f22017-05-15 01:04:39 +030094 yield CfgLine(fname, lineno, oline, SETTING, opt_name.strip(), parse_value(opt_val.strip()))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030095 elif line.startswith("include "):
kdanylov aka koder026e5f22017-05-15 01:04:39 +030096 yield CfgLine(fname, lineno, oline, INCLUDE, line.split(" ", 1)[1], None)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +030097 else:
98 yield CfgLine(fname, lineno, oline, SETTING, line, '1')
99
100 except Exception as exc:
101 raise ParseError(str(exc), fname, lineno, oline)
102
103
koder aka kdanilovf2865172016-12-30 03:35:11 +0200104def fio_config_parse(lexer_iter: Iterable[CfgLine]) -> Iterator[FioJobConfig]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300105 in_globals = False
106 curr_section = None
koder aka kdanilov70227062016-11-26 23:23:21 +0200107 glob_vals = OrderedDict() # type: Dict[str, Any]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300108 sections_count = 0
109
koder aka kdanilov70227062016-11-26 23:23:21 +0200110 lexed_lines = list(lexer_iter) # type: List[CfgLine]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300111 one_more = True
112 includes = {}
113
114 while one_more:
koder aka kdanilov70227062016-11-26 23:23:21 +0200115 new_lines = [] # type: List[CfgLine]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300116 one_more = False
117 for line in lexed_lines:
118 fname, lineno, oline, tp, name, val = line
119
120 if INCLUDE == tp:
121 if not os.path.exists(fname):
122 dirname = '.'
123 else:
124 dirname = os.path.dirname(fname)
125
126 new_fname = os.path.join(dirname, name)
127 includes[new_fname] = (fname, lineno)
128
129 try:
130 cont = open(new_fname).read()
131 except IOError as err:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200132 raise ParseError("Error while including file {}: {}".format(new_fname, err), fname, lineno, oline)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300133
134 new_lines.extend(fio_config_lexer(cont, new_fname))
135 one_more = True
136 else:
137 new_lines.append(line)
138
139 lexed_lines = new_lines
140
koder aka kdanilov108ac362017-01-19 20:17:16 +0200141 suite_section_idx = 0
142
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300143 for fname, lineno, oline, tp, name, val in lexed_lines:
144 if tp == SECTION:
145 if curr_section is not None:
146 yield curr_section
147 curr_section = None
148
149 if name == 'global':
150 if sections_count != 0:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200151 raise ParseError("[global] section should be only one and first", fname, lineno, oline)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300152 in_globals = True
153 else:
154 in_globals = False
koder aka kdanilov108ac362017-01-19 20:17:16 +0200155 curr_section = FioJobConfig(name, idx=suite_section_idx)
156 suite_section_idx += 1
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300157 curr_section.vals = glob_vals.copy()
158 sections_count += 1
159 else:
160 assert tp == SETTING
161 if in_globals:
162 glob_vals[name] = val
163 elif name == name.upper():
koder aka kdanilovf2865172016-12-30 03:35:11 +0200164 raise ParseError("Param {!r} not in [global] section".format(name), fname, lineno, oline)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300165 elif curr_section is None:
koder aka kdanilovf2865172016-12-30 03:35:11 +0200166 raise ParseError("Data outside section", fname, lineno, oline)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300167 else:
168 curr_section.vals[name] = val
169
170 if curr_section is not None:
171 yield curr_section
172
173
koder aka kdanilovf2865172016-12-30 03:35:11 +0200174def process_cycles(sec: FioJobConfig) -> Iterator[FioJobConfig]:
koder aka kdanilov70227062016-11-26 23:23:21 +0200175 cycles = OrderedDict() # type: Dict[str, Any]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300176
177 for name, val in sec.vals.items():
178 if isinstance(val, list) and name.upper() != name:
179 cycles[name] = val
180
181 if len(cycles) == 0:
182 yield sec
183 else:
koder aka kdanilov70227062016-11-26 23:23:21 +0200184 # iodepth should changes faster
185 numjobs = cycles.pop('iodepth', None)
186 items = list(cycles.items())
koder aka kdanilov170936a2015-06-27 22:51:17 +0300187
koder aka kdanilov70227062016-11-26 23:23:21 +0200188 if items:
koder aka kdanilov170936a2015-06-27 22:51:17 +0300189 keys, vals = zip(*items)
190 keys = list(keys)
191 vals = list(vals)
192 else:
193 keys = []
194 vals = []
195
196 if numjobs is not None:
197 vals.append(numjobs)
koder aka kdanilov70227062016-11-26 23:23:21 +0200198 keys.append('iodepth')
koder aka kdanilov170936a2015-06-27 22:51:17 +0300199
200 for combination in itertools.product(*vals):
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300201 new_sec = sec.copy()
koder aka kdanilov170936a2015-06-27 22:51:17 +0300202 new_sec.vals.update(zip(keys, combination))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300203 yield new_sec
204
205
koder aka kdanilov70227062016-11-26 23:23:21 +0200206FioParamsVal = Union[str, Var]
207FioParams = Dict[str, FioParamsVal]
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300208
209
koder aka kdanilovf2865172016-12-30 03:35:11 +0200210def apply_params(sec: FioJobConfig, params: FioParams) -> FioJobConfig:
koder aka kdanilov70227062016-11-26 23:23:21 +0200211 processed_vals = OrderedDict() # type: Dict[str, Any]
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300212 processed_vals.update(params)
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300213
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300214 for name, val in sec.vals.items():
215 if name in params:
216 continue
217
218 if isinstance(val, Var):
219 if val.name in params:
220 val = params[val.name]
221 elif val.name in processed_vals:
222 val = processed_vals[val.name]
223 processed_vals[name] = val
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300224
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300225 sec = sec.copy()
226 sec.vals = processed_vals
227 return sec
228
229
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300230def abbv_name_to_full(name: str) -> str:
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300231 assert len(name) == 3
232
233 smode = {
234 'a': 'async',
235 's': 'sync',
236 'd': 'direct',
237 'x': 'sync direct'
238 }
239 off_mode = {'s': 'sequential', 'r': 'random'}
koder aka kdanilov7248c7b2015-05-31 22:53:03 +0300240 oper = {'r': 'read', 'w': 'write', 'm': 'mixed'}
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300241 return smode[name[2]] + " " + \
242 off_mode[name[0]] + " " + oper[name[1]]
243
244
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300245MAGIC_OFFSET = 0.1885
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300246
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300247
koder aka kdanilovf2865172016-12-30 03:35:11 +0200248def final_process(sec: FioJobConfig, counter: List[int] = [0]) -> FioJobConfig:
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300249 sec = sec.copy()
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300250
251 sec.vals['unified_rw_reporting'] = '1'
252
koder aka kdanilovbb6d6cd2015-06-20 02:55:07 +0300253 if isinstance(sec.vals['size'], Var):
254 raise ValueError("Variable {0} isn't provided".format(
255 sec.vals['size'].name))
256
koder aka kdanilov88407ff2015-05-26 15:35:57 +0300257 sz = ssize2b(sec.vals['size'])
258 offset = sz * ((MAGIC_OFFSET * counter[0]) % 1.0)
259 offset = int(offset) // 1024 ** 2
260 new_vars = {'UNIQ_OFFSET': str(offset) + "m"}
261
262 for name, val in sec.vals.items():
263 if isinstance(val, Var):
264 if val.name in new_vars:
265 sec.vals[name] = new_vars[val.name]
266
koder aka kdanilovbb6d6cd2015-06-20 02:55:07 +0300267 for vl in sec.vals.values():
268 if isinstance(vl, Var):
269 raise ValueError("Variable {0} isn't provided".format(vl.name))
270
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300271 params = sec.vals.copy()
272 params['UNIQ'] = 'UN{0}'.format(counter[0])
273 params['COUNTER'] = str(counter[0])
koder aka kdanilov108ac362017-01-19 20:17:16 +0200274 params['TEST_SUMM'] = sec.summary
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300275 sec.name = sec.name.format(**params)
276 counter[0] += 1
277
278 return sec
279
280
koder aka kdanilovf2865172016-12-30 03:35:11 +0200281def execution_time(sec: FioJobConfig) -> int:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300282 return sec.vals.get('ramp_time', 0) + sec.vals.get('runtime', 0)
283
284
koder aka kdanilovf2865172016-12-30 03:35:11 +0200285def parse_all_in_1(source:str, fname: str = None) -> Iterator[FioJobConfig]:
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300286 return fio_config_parse(fio_config_lexer(source, fname))
287
288
koder aka kdanilova732a602017-02-01 20:29:56 +0200289def get_log_files(sec: FioJobConfig, iops: bool = False) -> Iterator[Tuple[str, str, str]]:
290 res = [] # type: List[Tuple[str, str, str]]
koder aka kdanilov108ac362017-01-19 20:17:16 +0200291
kdanylov aka koder45183182017-04-30 23:55:40 +0300292 keys = [('write_bw_log', 'bw', 'KiBps'),
koder aka kdanilova732a602017-02-01 20:29:56 +0200293 ('write_hist_log', 'lat', 'us')]
koder aka kdanilov108ac362017-01-19 20:17:16 +0200294 if iops:
kdanylov aka koder45183182017-04-30 23:55:40 +0300295 keys.append(('write_iops_log', 'iops', 'IOPS'))
koder aka kdanilov108ac362017-01-19 20:17:16 +0200296
koder aka kdanilova732a602017-02-01 20:29:56 +0200297 for key, name, units in keys:
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200298 log = sec.vals.get(key)
299 if log is not None:
koder aka kdanilova732a602017-02-01 20:29:56 +0200300 yield (name, log, units)
koder aka kdanilovbbbe1dc2016-12-20 01:19:56 +0200301
302
koder aka kdanilovf2865172016-12-30 03:35:11 +0200303def fio_cfg_compile(source: str, fname: str, test_params: FioParams) -> Iterator[FioJobConfig]:
kdanylov aka koder84de1e42017-05-22 14:00:07 +0300304 test_params = test_params.copy()
305
306 if 'RAMPTIME' not in test_params and 'RUNTIME' in test_params:
307 ramp = int(int(test_params['RUNTIME']) * 0.05)
308 test_params['RAMPTIME'] = min(30, max(5, ramp))
309
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300310 it = parse_all_in_1(source, fname)
311 it = (apply_params(sec, test_params) for sec in it)
312 it = flatmap(process_cycles, it)
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200313 for sec in map(final_process, it):
koder aka kdanilov23e6bdf2016-12-24 02:18:54 +0200314 yield sec
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300315
316
317def parse_args(argv):
318 parser = argparse.ArgumentParser(
319 description="Run fio' and return result")
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300320 parser.add_argument("-p", "--params", nargs="*", metavar="PARAM=VAL",
321 default=[],
322 help="Provide set of pairs PARAM=VAL to" +
323 "format into job description")
324 parser.add_argument("action", choices=['estimate', 'compile', 'num_tests'])
325 parser.add_argument("jobfile")
326 return parser.parse_args(argv)
327
328
329def main(argv):
330 argv_obj = parse_args(argv)
331
332 if argv_obj.jobfile == '-':
333 job_cfg = sys.stdin.read()
334 else:
335 job_cfg = open(argv_obj.jobfile).read()
336
337 params = {}
338 for param_val in argv_obj.params:
339 assert '=' in param_val
340 name, val = param_val.split("=", 1)
341 params[name] = parse_value(val)
342
koder aka kdanilovf236b9c2015-06-24 18:17:22 +0300343 sec_it = fio_cfg_compile(job_cfg, argv_obj.jobfile, params)
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300344
345 if argv_obj.action == 'estimate':
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300346 print(sec_to_str(sum(map(execution_time, sec_it))))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300347 elif argv_obj.action == 'num_tests':
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300348 print(sum(map(len, map(list, sec_it))))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300349 elif argv_obj.action == 'compile':
350 splitter = "\n#" + "-" * 70 + "\n\n"
koder aka kdanilov3b4da8b2016-10-17 00:17:53 +0300351 print(splitter.join(map(str, sec_it)))
koder aka kdanilov4af1c1d2015-05-18 15:48:58 +0300352
353 return 0
354
355
356if __name__ == '__main__':
357 exit(main(sys.argv[1:]))